Skip to content

Backtest IndexError at the right calendar boundary (get_step_time peeks calendar[index+1]) #2278

Description

@zhaow-de

🐛 Bug Description

TradeCalendarManager.get_step_time returns each trading step's interval as
(calendar[i], epsilon_change(calendar[i + 1])) — it reads the next bar to form the right
endpoint:

# qlib/backtest/utils.py:130-131  (TradeCalendarManager.get_step_time)
calendar_index = self.start_index + trade_step - shift
return self._calendar[calendar_index], epsilon_change(self._calendar[calendar_index + 1])

For the last trading step, calendar_index == self.end_index. If end_time is the dataset's
final calendar bar (end_index == len(self._calendar) - 1), then calendar_index + 1 is out
of bounds and the backtest dies with an opaque:

IndexError: index <N> is out of bounds for axis 0 with size <N>
  File ".../qlib/backtest/utils.py", line 131, in get_step_time
    return self._calendar[calendar_index], epsilon_change(self._calendar[calendar_index + 1])
  File ".../qlib/backtest/backtest.py", in collect_data_loop
    _trade_decision = trade_strategy.generate_trade_decision(_execute_result)
  File ".../qlib/contrib/strategy/signal_strategy.py", in generate_trade_decision
    trade_start_time, trade_end_time = self.trade_calendar.get_step_time(trade_step)

The index N out of bounds for size N form is the tell: the step needs calendar[end_index + 1]
but the calendar has no bar after end_time.

Root cause

The right-endpoint peek assumes there is always at least one bar after end_time. The
calendar is built with future=True:

# qlib/backtest/utils.py:69  (TradeCalendarManager.reset)
_calendar = Cal.calendar(freq=freq, future=True)

future=True is meant to supply a calendar that extends beyond the data so the +1 peek is safe.
But when a dataset has no future calendar configured (the common case for a self-built provider),
future=True silently falls back to the current calendar with only a warning:

WARNING qlib.data [data.py] - load calendar error: freq=day, future=True; return current calendar!
WARNING qlib.data [data.py] - You can get future calendar by referring to ...

So there is no bar after the last data day, and any backtest whose end_time is that last day
crashes at the final step. The failure is opaque (an IndexError deep in get_step_time, far from
the user's backtest(end_time=...) call) and easy to hit (it's simply "backtest through the end of
my data").

To Reproduce

  1. Use a provider whose calendar has no future extension (a self-built qlib dataset; Cal.calendar(future=True) then warns and returns the current calendar).
  2. Call qlib.backtest.backtest(start_time=..., end_time=<the last calendar day>, strategy=..., executor={"class": "SimulatorExecutor", ...}).
  3. The run dies at the final step with IndexError: index N is out of bounds for axis 0 with size N in TradeCalendarManager.get_step_time (utils.py:131).

(Ending one or more bars before the last calendar day does not crash, because calendar[index+1] then exists.)

Expected Behavior

A backtest ending on the last available calendar bar should either succeed or fail with a clear,
actionable message — not an opaque IndexError. Options:

  • In get_step_time, clamp the right endpoint at the boundary, e.g.
    right = epsilon_change(self._calendar[i + 1]) if i + 1 < len(self._calendar) else <end-of-last-bar>
    (mirroring the day-end logic already used elsewhere, e.g. day_start + pd.Timedelta(days=1) at
    utils.py:154), so the final step's interval is well-defined without a future bar.
  • And/or validate up front in backtest() that end_time leaves at least one trailing bar when no
    future calendar is available, raising a clear error (or auto-clamping) instead of crashing mid-loop.
  • And/or promote the future=True fallback warning to a clearer, louder signal that the right
    boundary is unsafe for backtesting through the data end.

Environment

  • Qlib version: 0.9.7 (qlib/backtest/utils.py:131, TradeCalendarManager.get_step_time)
  • Python version: 3.12
  • freq: day; provider: a self-built daily dataset with no future calendar.

Additional context

Discovered while building an out-of-sample walk-forward harness whose last window's end_time was
the dataset's final calendar day. Worked around it by capping end_time a couple of bars before
the calendar end (so calendar[index+1] always exists). A boundary-safe get_step_time (or an
upfront check/clamp in backtest) would remove the need for that workaround and turn a cryptic
IndexError into correct behavior or a clear error.

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions