markowitz.backtest¶
markowitz.backtest
¶
markowitz.backtest: walk-forward rebalancing, turnover, and performance attribution.
AlignmentError
¶
Bases: BacktestError
Raised when the universe of assets is inconsistent across inputs.
BacktestError
¶
Bases: Exception
Base class for all errors raised inside markowitz.backtest.
BacktestResult(returns_gross: pd.DataFrame, returns_net: pd.DataFrame, weights: dict[str, pd.DataFrame], turnover: pd.DataFrame, rebalance_dates: list[pd.Timestamp] = list())
dataclass
¶
Aggregated output of a :class:WalkForward run.
Attributes:
| Name | Type | Description |
|---|---|---|
returns_gross |
DataFrame
|
Per-period gross returns, one column per strategy. |
returns_net |
DataFrame
|
Per-period net-of-cost returns. |
weights |
dict[str, DataFrame]
|
Mapping |
turnover |
DataFrame
|
Per-period turnover, one column per strategy. |
rebalance_dates |
list[Timestamp]
|
Sorted list of dates on which weights were recomputed. |
summary(*, ann: int = 12, gamma: float = 5.0) -> pd.DataFrame
¶
Standard performance grid (Sharpe, Sortino, MDD, Calmar, CEQ, TO).
Source code in src/markowitz/backtest/result.py
DegenerateWindowError
¶
Bases: BacktestError
Raised when a rolling window is rank-deficient or non-finite.
GMVSample
¶
Global Minimum Variance with the sample covariance, closed form.
InsufficientHistoryError
¶
Bases: BacktestError
Raised when fewer observations are available than the rolling window.
MaxSharpeNaive
¶
Tangency portfolio with sample mean and sample covariance.
Falls back to :func:markowitz.optimizer.MeanVariance when available,
otherwise uses the closed-form Sigma^{-1} (mu - rf) solution.
MissingMarketCapsError
¶
Bases: BacktestError
Raised when a strategy that requires market caps cannot find them.
OneOverN
¶
Equal-weight benchmark of DeMiguel, Garlappi, Uppal (2009).
RiskParity(*, max_iter: int = 500, tol: float = 1e-08)
¶
Equal-risk-contribution portfolio via cyclical coordinate descent.
Solves the Spinu (2013) / Griveau-Billion-Richard-Roncalli (2013)
convex programme min_x 0.5 x'Sigma x - sum_i b_i log(x_i) with
b_i = 1/n (equal risk targets), then renormalizes to sum to one.
Source code in src/markowitz/backtest/strategies.py
Strategy
¶
Bases: Protocol
Static portfolio-construction protocol used by :class:WalkForward.
WalkForward(returns: pd.DataFrame, strategies: Mapping[str, Strategy], *, rebalance: str = 'M', lookback: int = 120, rf: pd.Series | float | None = None, cost_bps: float = 10.0, debug_no_lookahead: bool = False)
¶
Rolling-window walk-forward backtester.
The engine slides a fixed-length lookback window across returns,
fits every supplied strategy on the strictly in-sample slice, and
holds the resulting weights until the next rebalance date.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
returns
|
DataFrame
|
Wide DataFrame of per-period simple returns indexed by date. |
required |
strategies
|
Mapping[str, Strategy]
|
Mapping |
required |
rebalance
|
str
|
Pandas offset alias. |
'M'
|
lookback
|
int
|
Length of the rolling estimation window, in periods. |
120
|
rf
|
Series | float | None
|
Risk-free rate; scalar or Series aligned to |
None
|
cost_bps
|
float
|
Proportional transaction cost in basis points per unit turnover. |
10.0
|
debug_no_lookahead
|
bool
|
If |
False
|
Source code in src/markowitz/backtest/walk_forward.py
run() -> BacktestResult
¶
Execute the walk-forward loop and return a :class:BacktestResult.
Source code in src/markowitz/backtest/walk_forward.py
84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 | |
apply_transaction_costs(gross_returns: pd.Series, turnover: pd.Series, *, bps: float = 10.0) -> pd.Series
¶
Subtract proportional transaction costs from gross_returns.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
gross_returns
|
Series
|
Per-period simple returns before costs. |
required |
turnover
|
Series
|
Per-period one-sided turnover, aligned to |
required |
bps
|
float
|
Round-trip-equivalent cost in basis points. The deduction applied
to period |
10.0
|
Source code in src/markowitz/backtest/costs.py
calmar(r: pd.Series, ann: int = 12) -> float
¶
Calmar ratio: annualized arithmetic mean / |max drawdown|.
Returns 0.0 when the drawdown is zero.
Source code in src/markowitz/backtest/stats.py
ceq(r: pd.Series, gamma: float) -> float
¶
Per-period certainty-equivalent return under quadratic utility.
CEQ = mu - 0.5 * gamma * sigma^2.
Source code in src/markowitz/backtest/stats.py
compute_turnover(weights_new: np.ndarray, weights_prev: np.ndarray, returns_between: np.ndarray) -> float
¶
Return one-sided drift-adjusted turnover.
Let w_prev be the weights at the start of the period, r the
realized asset returns over that period and w_new the target
weights set at the rebalance date. The portfolio drifts to
w_drift = w_prev * (1 + r) / (1 + w_prev . r)
and the turnover charged on the rebalance is
TO = 0.5 * sum(|w_new - w_drift|).
Source code in src/markowitz/backtest/turnover.py
jobson_korkie_memmel(r_i: pd.Series, r_n: pd.Series) -> tuple[float, float]
¶
Jobson-Korkie test with the Memmel (2003) correction.
Tests :math:H_0: Sharpe(:math:r_i) = Sharpe(:math:r_n) against a
two-sided alternative. Returns (z, p_value).
Identical input series yield z == 0 and p_value == 1.
Source code in src/markowitz/backtest/stats.py
ledoit_wolf_bootstrap_pvalue(r_i: pd.Series, r_n: pd.Series, *, B: int = 999, block_length: int = 5, seed: int = 0) -> float
¶
Stationary-block-bootstrap p-value for the Sharpe-difference test.
Uses Politis-Romano fixed-block sampling under the null that the two
Sharpe ratios are equal. Returns a two-sided p-value in [0, 1].
Source code in src/markowitz/backtest/stats.py
max_drawdown(r: pd.Series) -> float
¶
Maximum drawdown of the wealth path implied by r.
Returns a non-positive number (0.0 if the path is monotonically
non-decreasing).
Source code in src/markowitz/backtest/stats.py
sharpe_ratio(r: pd.Series, rf: pd.Series | float = 0.0, ann: int = 12) -> float
¶
Annualized Sharpe ratio of an excess-return series.
Returns 0.0 if the standard deviation is exactly zero (degenerate case).
Source code in src/markowitz/backtest/stats.py
sortino_ratio(r: pd.Series, rf: pd.Series | float = 0.0, ann: int = 12) -> float
¶
Annualized Sortino ratio using downside semi-deviation (target = 0).