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)
|
n_slots = self.select_num - len(ranked_holdings)
|
||||||
ranked_holdings.extend([self.bond_code] * n_slots)
|
ranked_holdings.extend([self.bond_code] * n_slots)
|
||||||
|
|
||||||
# Compute position weights via configured scheme
|
# Compute position weights via configured scheme.
|
||||||
self._position_weights = compute_position_weights(
|
# 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,
|
ranked_holdings, self.weight_type,
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -708,6 +710,7 @@ class SimpleRotationStrategy:
|
|||||||
nav = 1.0
|
nav = 1.0
|
||||||
rebalance_count = 0
|
rebalance_count = 0
|
||||||
entry_info: Dict[str, dict] = {} # signal_code -> {entry_date, entry_price_etf, entry_price_idx}
|
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):
|
for i, date in enumerate(self.trading_calendar):
|
||||||
# Signal timing: 9:00 AM on day T
|
# 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
|
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)
|
# Return uses T's ETF prices (open for buy/sell, close for hold)
|
||||||
daily_return = self._calculate_daily_return(
|
daily_return = self._calculate_daily_return(
|
||||||
current_holdings, new_holdings, date, is_rebalance
|
current_holdings, new_holdings, date, is_rebalance
|
||||||
@@ -774,7 +782,7 @@ class SimpleRotationStrategy:
|
|||||||
'removed': sorted(removed),
|
'removed': sorted(removed),
|
||||||
'factors': {k: round(v, 6) for k, v in factors.items()},
|
'factors': {k: round(v, 6) for k, v in factors.items()},
|
||||||
'threshold': threshold_val,
|
'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
|
current_holdings = new_holdings
|
||||||
|
|||||||
Reference in New Issue
Block a user