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:
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user