From ca933e43e45b4137ebdec053f07723a646236ae4 Mon Sep 17 00:00:00 2001 From: aszerW Date: Sat, 6 Jun 2026 23:16:51 +0800 Subject: [PATCH] fix: lock position weights on rebalance only, not daily ranking changes MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Previously, position weights were recalculated every day in _generate_signals, causing weights to change even when holdings didn't change (only ranking order shifted). This was incorrect - weights should be locked at rebalance and remain stable until the next rebalance. Changes: - _generate_signals now computes _pending_weights (for signal generation only) - run() maintains active_weights, updated only on is_rebalance or first day - _calculate_daily_return uses the locked active_weights - daily_records stores active_weights in position_weights field Result: 391 → 318 rebalances, 25.63% → 26.38% CAGR --- rotation/simple_rotation.py | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/rotation/simple_rotation.py b/rotation/simple_rotation.py index 6743f72..81d500d 100644 --- a/rotation/simple_rotation.py +++ b/rotation/simple_rotation.py @@ -583,8 +583,10 @@ class SimpleRotationStrategy: n_slots = self.select_num - len(ranked_holdings) ranked_holdings.extend([self.bond_code] * n_slots) - # Compute position weights via configured scheme - self._position_weights = compute_position_weights( + # Compute position weights via configured scheme. + # These are *pending* weights; the caller (run) locks them in + # only when an actual rebalance occurs. + self._pending_weights = compute_position_weights( ranked_holdings, self.weight_type, ) @@ -708,6 +710,7 @@ class SimpleRotationStrategy: nav = 1.0 rebalance_count = 0 entry_info: Dict[str, dict] = {} # signal_code -> {entry_date, entry_price_etf, entry_price_idx} + active_weights: Dict[str, float] = {} # locked-in weights, updated only on rebalance for i, date in enumerate(self.trading_calendar): # Signal timing: 9:00 AM on day T @@ -732,6 +735,11 @@ class SimpleRotationStrategy: is_rebalance = (sorted(new_holdings) != sorted(current_holdings)) and len(current_holdings) > 0 + # Lock in position weights only on rebalance (or first day) + if is_rebalance or not current_holdings: + active_weights = dict(self._pending_weights) + self._position_weights = active_weights + # Return uses T's ETF prices (open for buy/sell, close for hold) daily_return = self._calculate_daily_return( current_holdings, new_holdings, date, is_rebalance @@ -774,7 +782,7 @@ class SimpleRotationStrategy: 'removed': sorted(removed), 'factors': {k: round(v, 6) for k, v in factors.items()}, 'threshold': threshold_val, - 'position_weights': {k: round(v, 6) for k, v in self._position_weights.items()}, + 'position_weights': {k: round(v, 6) for k, v in active_weights.items()}, }) current_holdings = new_holdings