backtester/portfolio/calc/returns.py returns.py:
The return and NAV normalization layer.
This file defines the first clean contract in the analytics stack: prices become aligned return matrices, and return matrices become NAV paths. Risk, rolling stats, plots and reports should not each invent their own return convention.
from backtester.portfolio.calc import returns When To Read This
- 01You need to know whether a downstream metric expects simple returns, log returns or levels.
- 02You are wiring a new data source and want the same date x instrument shape as the rest of the framework.
- 03You are building plots or PDF tables that require a normalized NAV path.
File Anatomy
- Return type handling: relative, log, difference, level and lagged-level return modes.
- Frequency normalization: optional resampling through the compatibility DataFrequency layer.
- Missing data policy: explicit forward-fill and first-row handling rather than implicit pandas defaults.
- NAV reconstruction: simple-return compounding, log-return exponentiation and optional terminal or initial scaling.
Data Contract
Inputs
- prices: pandas DataFrame indexed by trading dates, columns are instruments.
- returns: pandas DataFrame, Series or ndarray for NAV conversion helpers.
- return_type / is_log_returns / freq / fill flags decide the return convention at the boundary.
Outputs
- pd.DataFrame of per-period returns or levels with the same instrument columns.
- pd.Series of total or annualized return per column.
- pd.DataFrame NAV path suitable for equity curves, reports and Monte Carlo inputs.
Invariants
- Annualization uses the number of trading observations, not calendar-day distance.
- Functions are stateless and do not mutate strategy or portfolio objects.
- Simple returns and log returns must not be mixed downstream.
Public API And Key Internals
compute_periodic_returns
functioncompute_periodic_returns(prices, *, is_log_returns=False, return_type=ReturnTypes.RELATIVE, freq=None, include_start_date=False, include_end_date=False, ffill_nans=True, drop_first=False, is_first_zero=False) Converts a price matrix into the selected return representation. This is the canonical place to decide relative vs log vs difference returns.
Returns
pd.DataFrame aligned to the original or resampled index.
Implementation Notes
- Uses DataFrequency.resample_to_frequency when freq is provided.
- drop_first removes the natural NaN row after pct-change style calculations.
compute_total_returns
functioncompute_total_returns(returns) Compounds simple returns into total return per instrument.
Returns
pd.Series indexed by instrument.
compute_annualized_returns
functioncompute_annualized_returns(returns, *, days_per_year=252) Annualizes compounded total return using trading observations. This avoids calendar-day drift in daily backtests.
Returns
pd.Series indexed by instrument.
Implementation Notes
- The module deliberately keeps return creation separate from risk metrics. That makes the return convention visible and testable.
- The first period is configurable because some report contexts want a zero first row while others prefer dropping it.
- NAV scaling supports both initial-capital charts and terminal-value normalization without changing the original return matrix.
Code Walkthrough
Typical import and usage
One source price matrix becomes the shared contract for annualization and NAV charts.
import pandas as pd
from backtester.portfolio.calc import returns
prices = pd.DataFrame(
{
"AAPL": [180.0, 181.5, 179.0, 184.0],
"MSFT": [410.0, 412.0, 416.0, 415.0],
},
index=pd.date_range("2025-01-02", periods=4, freq="B"),
)
asset_returns = returns.compute_periodic_returns(
prices,
drop_first=True,
)
annualized = returns.compute_annualized_returns(asset_returns)
nav = returns.convert_returns_to_nav(asset_returns, init_value=100_000) Key implementation: return convention switch
This is the section that prevents the rest of the system from guessing what "returns" means.
if return_type == ReturnTypes.LOG or is_log_returns:
rets = np.log(df) - np.log(df.shift(1))
elif return_type == ReturnTypes.RELATIVE:
rets = df.divide(df.shift(1)).subtract(1.0)
elif return_type == ReturnTypes.DIFFERENCE:
rets = df.subtract(df.shift(1))
elif return_type == ReturnTypes.LEVEL:
rets = df
elif return_type == ReturnTypes.LEVEL0:
rets = df.shift(1)
else:
raise NotImplementedError(f"Unsupported return type: {return_type}")