microsoft/qlib · error · NotImplementedError

trade_calendar is necessary for getting TradeRangeByTime.

Error message

trade_calendar is necessary for getting TradeRangeByTime.

What it means

TradeRangeByTime.__call__ requires a TradeCalendarManager to convert an intraday time window (e.g. '9:30'-'14:30') into calendar index positions. If the trade_calendar argument is None, there is no way to map wall-clock times onto the trading calendar, so a NotImplementedError is raised. In practice the calendar normally arrives via the 'inner_calendar' kwarg of BaseTradeDecision._get_range_limit, so a None calendar usually means that kwarg was not forwarded.

Source

Thrown at qlib/backtest/decision.py:288

        **NOTE**:
        - It is designed for minute-bar for intra-day trading!!!!!
        - Both start_time and end_time are **closed** in the range

        Parameters
        ----------
        start_time : str | time
            e.g. "9:30"
        end_time : str | time
            e.g. "14:30"
        """
        self.start_time = pd.Timestamp(start_time).time() if isinstance(start_time, str) else start_time
        self.end_time = pd.Timestamp(end_time).time() if isinstance(end_time, str) else end_time
        assert self.start_time < self.end_time

    def __call__(self, trade_calendar: TradeCalendarManager) -> Tuple[int, int]:
        if trade_calendar is None:
            raise NotImplementedError("trade_calendar is necessary for getting TradeRangeByTime.")

        start_date = trade_calendar.start_time.date()
        val_start, val_end = concat_date_time(start_date, self.start_time), concat_date_time(start_date, self.end_time)
        return trade_calendar.get_range_idx(val_start, val_end)

    def clip_time_range(self, start_time: pd.Timestamp, end_time: pd.Timestamp) -> Tuple[pd.Timestamp, pd.Timestamp]:
        start_date = start_time.date()
        val_start, val_end = concat_date_time(start_date, self.start_time), concat_date_time(start_date, self.end_time)
        # NOTE: `end_date` should not be used. Because the `end_date` is for slicing. It may be in the next day
        # Assumption: start_time and end_time is for intra-day trading. So it is OK for only using start_date
        return max(val_start, start_time), min(val_end, end_time)


class BaseTradeDecision(Generic[DecisionType]):
    """
    Trade decisions are made by strategy and executed by executor

    Motivation:

View on GitHub (pinned to 79633dd950)

Solutions

  1. When calling decision.get_range_limit(), pass the inner (nested) calendar: decision.get_range_limit(inner_calendar=inner_executor.trade_calendar)
  2. If invoking the trade range object directly, supply the calendar: trade_range(trade_calendar=trade_calendar_manager)
  3. In custom executors, only request range limits from decisions when you actually have the nested trade calendar to resolve them

Example fix

// before
start, end = decision.get_range_limit()  # inner_calendar missing -> NotImplementedError
// after
start, end = decision.get_range_limit(inner_calendar=inner_executor.trade_calendar)
Defensive patterns

Strategy: validation

Validate before calling

from qlib.backtest.decision import TradeRangeByTime

def resolve_range(decision, inner_calendar=None):
    if inner_calendar is None:
        return None  # caller must supply calendar; do not call trade_range
    return decision.get_range_limit(inner_calendar=inner_calendar)

Type guard

def has_trade_calendar(cal) -> bool:
    return cal is not None and hasattr(cal, 'get_range_idx')

Try / catch

try:
    start, end = trade_range(trade_calendar=cal)
except NotImplementedError:
    # no inner calendar available; fall back to full-step execution
    start, end = 0, total_step - 1

Prevention

When it happens

Trigger: Calling trade_range(trade_calendar=None) directly, or calling decision.get_range_limit()/_get_range_limit() on a decision whose trade_range is a TradeRangeByTime while kwargs lacks 'inner_calendar' (e.g. a custom executor or strategy calling get_range_limit without passing the inner nested calendar).

Common situations: Custom NestedExecutor implementations that call trade_decision.get_range_limit() without inner_calendar; strategies constructing TradeRangeByTime manually and invoking it; refactors that renamed or dropped the inner_calendar kwarg.

Related errors


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