microsoft/qlib · error · NotImplementedError

The decision didn't provide an index range

Error message

The decision didn't provide an index range

What it means

BaseTradeDecision._get_range_limit unconditionally raises NotImplementedError when the decision carries no trade_range, because without a trade range the decision cannot express a unified start/end index pair. This internal method is the primitive that get_range_limit wraps; the public wrapper converts the exception into a default value when one is supplied.

Source

Thrown at qlib/backtest/decision.py:389

        trade_calendar : TradeCalendarManager
            The calendar of the **inner strategy**!!!!!

        Returns
        -------
        BaseTradeDecision:
            New update, use new decision. If no updates, return None (use previous decision (or unavailable))
        """
        # purpose 1)
        self.total_step = trade_calendar.get_trade_len()

        # purpose 2)
        return self.strategy.update_trade_decision(self, trade_calendar)

    def _get_range_limit(self, **kwargs: Any) -> Tuple[int, int]:
        if self.trade_range is not None:
            return self.trade_range(trade_calendar=cast(TradeCalendarManager, kwargs.get("inner_calendar")))
        else:
            raise NotImplementedError("The decision didn't provide an index range")

    def get_range_limit(self, **kwargs: Any) -> Tuple[int, int]:
        """
        return the expected step range for limiting the decision execution time
        Both left and right are **closed**

        if no available trade_range, `default_value` will be returned

        It is only used in `NestedExecutor`
        - The outmost strategy will not follow any range limit (but it may give range_limit)
        - The inner most strategy's range_limit will be useless due to atomic executors don't have such
          features.

        **NOTE**:
        1) This function must be called after `self.update` in following cases(ensured by NestedExecutor):
        - user relies on the auto-clip feature of `self.update`

        2) This function will be called after _init_sub_trading in NestedExecutor.

View on GitHub (pinned to 79633dd950)

Solutions

  1. Call the public decision.get_range_limit(default_value=None) instead, which returns the default instead of raising
  2. Construct the decision with a trade_range, e.g. TradeRangeByTime('9:30', '14:30'), so a range can be resolved
  3. Check decision.trade_range is not None before querying the range limit

Example fix

// before
idx = decision._get_range_limit(inner_calendar=cal)  # raises if trade_range is None
// after
idx = decision.get_range_limit(default_value=None, inner_calendar=cal)
if idx is None:
    idx = 0, total_steps - 1
Defensive patterns

Strategy: validation

Validate before calling

def safe_range_limit(decision, **kwargs):
    if decision.trade_range is None:
        return kwargs.get('default_value')
    return decision._get_range_limit(**kwargs)

Type guard

def has_trade_range(decision) -> bool:
    return getattr(decision, 'trade_range', None) is not None

Try / catch

try:
    idx = decision._get_range_limit(inner_calendar=cal)
except NotImplementedError:
    idx = (0, decision.total_step - 1)

Prevention

When it happens

Trigger: Calling decision._get_range_limit(**kwargs) on any decision whose trade_range attribute is None (e.g. a BaseTradeDecision or a TradeDecisionWCache built without trade_range).

Common situations: Custom code reaching into the private _get_range_limit instead of the public get_range_limit; strategies generating decisions without a trade_range while an executor or the strategy itself probes for a range limit.

Related errors


AI-assisted analysis of microsoft/qlib@79633dd950 (2026-08-15). Data as JSON: /api/errors/b4e496e8ea4629d7. Report an issue: GitHub.