🐛 Bug Description
get_risk_degree() is documented as the dynamic market-timing hook ("Dynamically risk_degree will result in Market timing.", qlib/contrib/strategy/signal_strategy.py:69). But TopkDropoutStrategy.generate_trade_decision sizes new buys with the raw attribute self.risk_degree, not the getter:
# qlib/contrib/strategy/signal_strategy.py:266
value = cash * self.risk_degree / len(buy) if len(buy) > 0 else 0
It is the only use of risk_degree in TopkDropoutStrategy, and it bypasses get_risk_degree(). So a subclass that overrides get_risk_degree() to return a time-varying multiplier (the documented way to implement market timing) has zero effect on a TopkDropout book — the override is never called. (By contrast, WeightStrategyBase at line 365 and its subclass EnhancedIndexingStrategy at line 482 do call self.get_risk_degree(...), so the inconsistency is within the same module.)
To Reproduce
- Subclass
TopkDropoutStrategy and override get_risk_degree() to return, say, 0.0 on some dates (a regime gate to cash) and self.risk_degree otherwise.
- Backtest it vs. the un-subclassed
TopkDropoutStrategy on the same signal.
- The two produce identical trades/returns — the overridden
get_risk_degree() is never consulted for buy sizing.
Expected Behavior
TopkDropoutStrategy should size buys with self.get_risk_degree(trade_step) (as WeightStrategyBase does), so that overriding get_risk_degree() actually enables market timing as documented:
value = cash * self.get_risk_degree(trade_step) / len(buy) if len(buy) > 0 else 0
Environment
- Qlib version: 0.9.7 (
qlib/contrib/strategy/signal_strategy.py (TopkDropoutStrategy.generate_trade_decision, line ~266)
- Python version: 3.12
Additional context
Discovered while building a BTC-trend regime overlay (RegimeGatedTopkStrategy(TopkDropoutStrategy)) that gates gross exposure via get_risk_degree(). The overlay had no effect on backtests until we worked around it by pushing the gated value onto self.risk_degree per step before delegating to super().generate_trade_decision(). A two-line fix in TopkDropoutStrategy (use the getter) would remove the need for the workaround.
🐛 Bug Description
get_risk_degree()is documented as the dynamic market-timing hook ("Dynamically risk_degree will result in Market timing.",qlib/contrib/strategy/signal_strategy.py:69). ButTopkDropoutStrategy.generate_trade_decisionsizes new buys with the raw attributeself.risk_degree, not the getter:It is the only use of risk_degree in
TopkDropoutStrategy, and it bypassesget_risk_degree(). So a subclass that overridesget_risk_degree()to return a time-varying multiplier (the documented way to implement market timing) has zero effect on a TopkDropout book — the override is never called. (By contrast,WeightStrategyBaseat line 365 and its subclassEnhancedIndexingStrategyat line 482 do callself.get_risk_degree(...), so the inconsistency is within the same module.)To Reproduce
TopkDropoutStrategyand overrideget_risk_degree()to return, say,0.0on some dates (a regime gate to cash) andself.risk_degreeotherwise.TopkDropoutStrategyon the same signal.get_risk_degree()is never consulted for buy sizing.Expected Behavior
TopkDropoutStrategyshould size buys withself.get_risk_degree(trade_step)(asWeightStrategyBasedoes), so that overridingget_risk_degree()actually enables market timing as documented:Environment
qlib/contrib/strategy/signal_strategy.py(TopkDropoutStrategy.generate_trade_decision, line ~266)Additional context
Discovered while building a BTC-trend regime overlay (
RegimeGatedTopkStrategy(TopkDropoutStrategy)) that gates gross exposure viaget_risk_degree(). The overlay had no effect on backtests until we worked around it by pushing the gated value ontoself.risk_degreeper step before delegating tosuper().generate_trade_decision(). A two-line fix inTopkDropoutStrategy(use the getter) would remove the need for the workaround.