#2276·qlib

TopkDropoutStrategy ignores get_risk_degree() for position sizing

Author: zhaow-deCreated Jun 21, 2026Updated Jun 21, 2026
Labelsbug

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

  1. 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.
  2. Backtest it vs. the un-subclassed TopkDropoutStrategy on the same signal.
  3. 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.