Skip to content
FactorQX
beginnerpythonpandasreturns

Computing Returns and Equity Curves with pandas

Compute simple and log returns from a price series, build a cumulative equity curve with pandas, and understand why log returns add across time.

3 min read

Educational software/research content only — not investment advice, a trading signal, or a recommendation.

Returns are the unit of measurement for almost every backtest. Before you can compute a Sharpe ratio, a drawdown, or a CAGR, you need a clean, correct return series. This article shows how to derive simple and log returns from a price series, why the distinction matters, and how to assemble an equity curve.

Starting from a price series

Assume you have a pandas Series (or a DataFrame column) of closing prices indexed by a DatetimeIndex. The cleanest way to get returns is pct_change, which computes the percent difference between consecutive rows.

returns.py
import numpy as np
import pandas as pd
 
prices = pd.Series(
    [100.0, 102.0, 101.0, 105.0, 103.0],
    index=pd.date_range("2026-01-01", periods=5, freq="D"),
    name="close",
)
 
simple_returns = prices.pct_change()
print(simple_returns)

The first value is NaN because there is no prior price to compare against. That NaN is expected — drop it with .dropna() only when you actually need a complete series, and keep the index aligned with prices otherwise.

Simple vs. log returns

A simple return is the fractional change in price:

r_t = (P_t - P_{t-1}) / P_{t-1} = P_t / P_{t-1} - 1

A log return (also called a continuously compounded return) is the natural log of the price ratio:

log_returns.py
# Two equivalent forms — use whichever fits your pipeline:
log_returns = np.log(prices / prices.shift(1))   # straight from prices
log_returns = np.log1p(prices.pct_change())       # from a returns series

np.log1p(x) computes log(1 + x) with better numerical precision for small returns, so it is the preferred form when you already have a pct_change series.

The two measures are close for small moves but diverge as magnitudes grow. A +10% simple return is a +9.53% log return; a -10% simple return is -10.54%.

Why log returns add

The key property of log returns is time additivity. Simple returns compound multiplicatively, but log returns sum:

log(P_n / P_0) = log(P_1/P_0) + log(P_2/P_1) + ... + log(P_n/P_{n-1})

This means the total log return over a window is just the sum of the per-period log returns. That makes aggregation trivial — a weekly log return is the sum of its daily log returns. It also makes log returns convenient for statistics that assume additivity, such as scaling volatility by the square root of time.

aggregate.py
total_log = log_returns.sum()
total_simple = np.expm1(total_log)   # back to a simple total return
print(f"Total return: {total_simple:.2%}")

Use np.expm1 (which computes exp(x) - 1) to convert a cumulative log return back into a human-readable simple return.

Building an equity curve

An equity curve tracks the growth of one unit of capital over time. With simple returns, you compound via a cumulative product:

equity_curve.py
simple_returns = prices.pct_change().fillna(0)
equity = (1 + simple_returns).cumprod()
print(equity)

Multiplying by a starting capital scales it to dollars:

starting_capital = 10_000
equity_dollars = starting_capital * equity

With log returns, the equivalent is a cumulative sum followed by exponentiation:

equity_from_logs.py
log_returns = np.log1p(prices.pct_change().fillna(0))
equity = np.exp(log_returns.cumsum())

Both produce the same curve up to floating-point rounding. Choose the form that matches the rest of your pipeline: cumulative product reads naturally for reporting, while cumulative sum of logs is often cleaner inside vectorized strategy code.

A note on alignment and bias

When you multiply a return series by a position series (for example, +1 for long, 0 for flat), the position must reflect information available before the return is realized. Computing today's signal from today's close and applying it to today's return silently leaks the future into your backtest. Shift your signals forward by one period and review Avoiding Lookahead Bias in Backtests before trusting any equity curve.

strategy_returns.py
signal = (prices > prices.rolling(3).mean()).astype(int)
strategy_returns = signal.shift(1) * prices.pct_change()
equity = (1 + strategy_returns.fillna(0)).cumprod()

Where to go next

Once you have a reliable return and equity series, the natural next step is to aggregate it to other timeframes — see Resampling OHLCV Data with pandas — and then to summarize performance honestly with Performance Metrics Every Backtest Should Report. Both build directly on the return series you constructed here.

Educational content. This article covers software development and research methods only. It is not investment advice, a trading signal, or a recommendation. See our disclaimer.