fix: lock position weights on rebalance only, not daily ranking changes

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
This commit is contained in:
2026-06-06 23:16:51 +08:00
parent 8d8fd71149
commit ca933e43e4

View File

@@ -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