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
- When calling decision.get_range_limit(), pass the inner (nested) calendar: decision.get_range_limit(inner_calendar=inner_executor.trade_calendar)
- If invoking the trade range object directly, supply the calendar: trade_range(trade_calendar=trade_calendar_manager)
- 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
- Always pass inner_calendar when calling get_range_limit inside nested executors
- Guard calls with `if cal is not None` before invoking a TradeRangeByTime
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
- The decision didn't provide an index range
- There is no trade_range in this case
- Please implement the `get_data` method
- InfPosition doesn't support calculating value
- InfPosition doesn't support stock list position
AI-assisted analysis of microsoft/qlib@79633dd950 (2026-08-15).
Data as JSON: /api/errors/655243b770b439c2.
Report an issue: GitHub.