From 6c7fbe0ba47395c8d2d2139d593a6d94d4957786 Mon Sep 17 00:00:00 2001 From: Nitin Tony Paul <108007300+nitintonypaul@users.noreply.github.com> Date: Mon, 26 Jan 2026 18:31:05 +0530 Subject: [PATCH] [FIX] backtester bug fix (#4) * [ENH] Added `HierarchicalRiskParity` - Added HRP optimizer - fixed minor bug which returns portfolio weights reference instead of copying - Improved documentation with minimal import syntax - Updated README/Documentation * [MNT] Improving documentation Improving docstrings and markdown documentation website. * [ENH] Improved `Backtester` Decoupled `rebalance_freq` and `reopt_freq` enabling users to customize the portfolio style. Also refactored `Backtester` class for better readability. Updated documentation and Readme with changes. * [FIX] `backtest` bug in `Backtester` Fixed backtesting bug where the return data was misaligned for computing drifted weights. --- opes/backtester.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/opes/backtester.py b/opes/backtester.py index 0fa6209..330a305 100644 --- a/opes/backtester.py +++ b/opes/backtester.py @@ -274,6 +274,7 @@ def backtest( - `optimize` must output weights for the timestep. !!! note "Note" + - The backtest assumes portfolio weights are applied at the open of each timestep, with zero execution delay. - Re-optimization does not automatically imply rebalancing. When the portfolio is re-optimized at a given timestep, weights may or may not be updated depending on the value of `rebalance_freq`. - To ensure a coherent backtest, a common practice is to choose frequencies such that `reopt_freq % rebalance_freq == 0`. This guarantees that whenever optimization occurs, a rebalance is also performed. - Also note that within a given timestep, rebalancing, if it occurs, is performed after optimization when optimization is scheduled for that timestep. @@ -401,8 +402,11 @@ def backtest( # ---------- REBALANCING BLOCK ---------- # Computing drifted weights # This is necessary for turnover and slippage modelling + # NOTE: weights and returns of the previous timestep are passed in to compute drifted weights + # This is because weights[0], which is to be set on the beginning of the zeroth day is separately computed + # Therefore, as the loop starts from 1, the return from the zeroth day will cause the first drifted weights on the start of the first day (end of zeroth day) drifted_weights = self._compute_drifted_weights( - weights[t - 1], test_data[t] + weights[t - 1], test_data[t - 1] ) # Assigning computed weights to weight array