Skip to content

TopkDropoutStrategy ignores get_risk_degree() for position sizing #2276

Description

@zhaow-de

🐛 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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions