backtester/portfolio/calc/risk.py risk.py:
Column-wise risk metrics from the same return matrix.
This file turns the return contract into reportable risk: volatility, drawdowns, historical VaR/CVaR, downside metrics and risk-adjusted ratios. It is intentionally functional, so the same calculations can be reused in reports, notebooks and strategy pages.
from backtester.portfolio.calc import risk When To Read This
- 01You need to know exactly how Sharpe, Sortino, Calmar, Omega or drawdown are computed.
- 02You are adding a report metric and want it to match the Python API.
- 03You are comparing strategy columns and benchmark columns with the same risk definition.
File Anatomy
- Path risk: drawdown paths, max drawdown, drawdown duration and conditional drawdown at risk.
- Distribution risk: volatility, historical VaR, CVaR and expected shortfall.
- Risk-adjusted ratios: Sharpe, Sortino, Calmar, Omega, serenity, gain-to-pain and smart ratios.
- Compatibility hooks: selected metrics delegate to backtester.portfolio._compat wrappers.
Data Contract
Inputs
- returns: simple period returns, DataFrame with dates x instruments or strategy columns.
- benchmark_returns: Series for information ratio when benchmark-relative risk is required.
- risk_free_rate, target_return, confidence and days_per_year parameters.
Outputs
- pd.DataFrame paths for volatility and drawdowns.
- pd.Series per return column for most scalar metrics.
- float for aggregate conditional drawdown at risk.
Invariants
- Drawdowns are computed from cumulative simple-return NAV: (1 + returns).cumprod().
- VaR/CVaR are historical quantile calculations, not parametric model estimates.
- Zero denominators are converted to NaN where a ratio would otherwise be misleading.
Public API And Key Internals
compute_drawdowns
functioncompute_drawdowns(returns) Builds underwater paths from cumulative NAV.
Returns
pd.DataFrame with negative or zero drawdown values.
compute_max_drawdown
functioncompute_max_drawdown(returns) Returns the minimum drawdown per column.
Returns
pd.Series.
compute_var / compute_cvar / compute_expected_shortfall
functioncompute_cvar(returns, confidence=0.95) Computes historical tail threshold and average loss beyond that threshold.
Returns
pd.Series.
sharpe_ratio / sortino_ratio / information_ratio
functionsharpe_ratio(returns, *, risk_free_rate=0.0, days_per_year=252, annualize=True) Risk-adjusted return metrics with daily risk-free conversion.
Returns
pd.Series.
smart_sharpe_ratio / smart_sortino_ratio / smart_calmar_ratio
functionsmart_sharpe_ratio(returns, *, risk_free_rate=0.0, days_per_year=252, trim_frac=0.02) Trimmed variants intended to reduce sensitivity to extreme observations.
Returns
pd.Series.
sampled_volatility
functionsampled_volatility(returns, *, freq_vol="M", freq_return=None, days_per_year=252) Resamples volatility observations for monthly or quarterly review tables.
Returns
pd.DataFrame.
Implementation Notes
- The API is column-wise: a single call can evaluate strategy, benchmark and variants side by side.
- Calmar and smart Calmar import annualized returns from returns.py, so report CAGR and Calmar stay consistent.
- The module includes both common metrics and less common pain metrics because PDF tear-sheets need more than Sharpe.
Code Walkthrough
Build a risk packet for a report section
The same returns DataFrame feeds every metric. That makes comparison tables mechanically consistent.
from backtester.portfolio.calc import risk
risk_packet = {
"max_drawdown": risk.compute_max_drawdown(strategy_returns),
"var_95": risk.compute_var(strategy_returns, confidence=0.95),
"cvar_95": risk.compute_cvar(strategy_returns, confidence=0.95),
"sharpe": risk.sharpe_ratio(strategy_returns, risk_free_rate=0.02),
"sortino": risk.sortino_ratio(strategy_returns, risk_free_rate=0.02),
"omega": risk.omega_ratio(strategy_returns),
} Key implementation: drawdown path and max drawdown
This is the base path-risk calculation reused by other risk functions.
def compute_drawdowns(returns: pd.DataFrame) -> pd.DataFrame:
nav = (1 + returns).cumprod()
peak = nav.cummax()
return (nav - peak) / peak
def compute_max_drawdown(returns: pd.DataFrame) -> pd.Series:
dd = compute_drawdowns(returns)
return dd.min() Key implementation: trimmed smart Sharpe
The smart variant trims stacked returns before estimating mean and standard deviation.
ex = excess.stack().sort_values()
n = len(ex)
k = int(n * trim_frac)
ex_t = ex.iloc[k : n - k] if n - 2 * k > 0 else ex
ex_df = ex_t.unstack()
mu = ex_df.mean()
sd = ex_df.std().replace(0.0, np.nan)
return mu / sd * np.sqrt(days_per_year)