fix: rank-based allocator + momentum signal (reversal->momentum flip)

This commit is contained in:
Yuxuan Yan
2026-06-07 09:50:08 +08:00
parent da01312292
commit bd0605072f
7 changed files with 117 additions and 44 deletions
+39 -9
View File
@@ -1,5 +1,6 @@
"""Map signal values to discrete position actions."""
from dataclasses import dataclass
from typing import Optional
@dataclass
@@ -24,17 +25,46 @@ class ThresholdBuilder:
self.size_pct = size_pct
def build(self, signal_value: float, in_position: bool) -> PositionAction:
"""Decide what to do given the current signal and position state.
Args:
signal_value: Latest signal value for the stock.
in_position: Whether the portfolio currently holds the stock.
Returns:
The action to take ("buy", "sell", or "hold").
"""
if not in_position and signal_value >= self.buy_threshold:
return PositionAction("buy", self.size_pct)
if in_position and signal_value <= self.sell_threshold:
return PositionAction("sell", 0.0)
return PositionAction("hold", 0.0)
class RankEqualWeightBuilder:
"""Rank all stocks by signal. Buy top N at equal weight. Sell if drops out.
Called once per bar with ALL stock signals. Returns per-stock actions.
"""
def __init__(self, top_n: int = 5, min_signal: Optional[float] = None):
self.top_n = top_n
self.min_signal = min_signal # optional floor — skip stocks with signal below this
def build(self, signals: dict[str, float]) -> dict[str, PositionAction]:
"""Rank stocks by signal (descending). Top N get 'buy', rest get 'sell' if held.
Args:
signals: {symbol: signal_value} for all stocks on this bar.
Returns:
{symbol: PositionAction}
"""
# Filter by min_signal if set
if self.min_signal is not None:
signals = {s: v for s, v in signals.items() if v >= self.min_signal}
# Sort by signal descending
ranked = sorted(signals.items(), key=lambda x: x[1], reverse=True)
top_symbols = set(sym for sym, _ in ranked[:self.top_n])
size_pct = 1.0 / self.top_n if self.top_n > 0 else 0.0
actions = {}
for sym in signals:
if sym in top_symbols:
actions[sym] = PositionAction("buy", size_pct)
else:
actions[sym] = PositionAction("sell", 0.0)
return actions