change: rank allocator top_n=5 -> top_pct=0.2 (20% of universe)
This commit is contained in:
+12
-13
@@ -33,24 +33,17 @@ class ThresholdBuilder:
|
|||||||
|
|
||||||
|
|
||||||
class RankEqualWeightBuilder:
|
class RankEqualWeightBuilder:
|
||||||
"""Rank all stocks by signal. Buy top N at equal weight. Sell if drops out.
|
"""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.
|
Called once per bar with ALL stock signals. Returns per-stock actions.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self, top_n: int = 5, min_signal: Optional[float] = None):
|
def __init__(self, top_n: Optional[int] = None, top_pct: float = 0.2, min_signal: Optional[float] = None):
|
||||||
self.top_n = top_n
|
self.top_n = top_n
|
||||||
self.min_signal = min_signal # optional floor — skip stocks with signal below this
|
self.top_pct = top_pct
|
||||||
|
self.min_signal = min_signal
|
||||||
|
|
||||||
def build(self, signals: dict[str, float]) -> dict[str, PositionAction]:
|
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
|
# Filter by min_signal if set
|
||||||
if self.min_signal is not None:
|
if self.min_signal is not None:
|
||||||
signals = {s: v for s, v in signals.items() if v >= self.min_signal}
|
signals = {s: v for s, v in signals.items() if v >= self.min_signal}
|
||||||
@@ -58,8 +51,14 @@ class RankEqualWeightBuilder:
|
|||||||
# Sort by signal descending
|
# Sort by signal descending
|
||||||
ranked = sorted(signals.items(), key=lambda x: x[1], reverse=True)
|
ranked = sorted(signals.items(), key=lambda x: x[1], reverse=True)
|
||||||
|
|
||||||
top_symbols = set(sym for sym, _ in ranked[:self.top_n])
|
# Determine top N: explicit count or percentage of available stocks
|
||||||
size_pct = 1.0 / self.top_n if self.top_n > 0 else 0.0
|
if self.top_n is not None:
|
||||||
|
n = self.top_n
|
||||||
|
else:
|
||||||
|
n = max(1, int(len(signals) * self.top_pct))
|
||||||
|
|
||||||
|
top_symbols = set(sym for sym, _ in ranked[:n])
|
||||||
|
size_pct = 1.0 / n if n > 0 else 0.0
|
||||||
|
|
||||||
actions = {}
|
actions = {}
|
||||||
for sym in signals:
|
for sym in signals:
|
||||||
|
|||||||
Binary file not shown.
|
Before Width: | Height: | Size: 58 KiB After Width: | Height: | Size: 59 KiB |
+8
-8
@@ -1,13 +1,13 @@
|
|||||||
BACKTEST SUMMARY
|
BACKTEST SUMMARY
|
||||||
========================================
|
========================================
|
||||||
sharpe: -0.2136540474098261
|
sharpe: 0.12450603119200966
|
||||||
max_drawdown: 22.08204523900236
|
max_drawdown: 18.40327026827532
|
||||||
max_drawdown_len: 445
|
max_drawdown_len: 251
|
||||||
total_return: -0.011700103856604784
|
total_return: 0.04945463098329521
|
||||||
avg_return: -2.4173768298770213e-05
|
avg_return: 0.00010217898963490745
|
||||||
total_trades: 586
|
total_trades: 695
|
||||||
won_trades: 287
|
won_trades: 357
|
||||||
lost_trades: 295
|
lost_trades: 333
|
||||||
|
|
||||||
SIGNAL IC
|
SIGNAL IC
|
||||||
========================================
|
========================================
|
||||||
|
|||||||
+1
-1
@@ -83,7 +83,7 @@ def main(forward_horizon: int = 5, universe: str = "csi500", signal_name: str =
|
|||||||
sizer_percent=0.95,
|
sizer_percent=0.95,
|
||||||
)
|
)
|
||||||
runner = BacktestRunner(config)
|
runner = BacktestRunner(config)
|
||||||
builder = RankEqualWeightBuilder(top_n=5)
|
builder = RankEqualWeightBuilder(top_pct=0.2)
|
||||||
for sym, df in data.items():
|
for sym, df in data.items():
|
||||||
df = df.copy()
|
df = df.copy()
|
||||||
df["signal"] = signal.compute(df).values
|
df["signal"] = signal.compute(df).values
|
||||||
|
|||||||
Reference in New Issue
Block a user