Backtest IndexError at the right calendar boundary (get_step_time peeks calendar[index+1])
Nobody has claimed this yet.
- Dominant language
- Python
- Stars
- 48.7k
- Forks
- 7.7k
- PR merge metrics
- No merged PRs in 30d
Description
🐛 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
- Use a provider whose calendar has no future extension (a self-built
qlibdataset;Cal.calendar(future=True)then warns and returns the current calendar). - Call
qlib.backtest.backtest(start_time=..., end_time=<the last calendar day>, strategy=..., executor={"class": "SimulatorExecutor", ...}). - The run dies at the final step with
IndexError: index N is out of bounds for axis 0 with size NinTradeCalendarManager.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()thatend_timeleaves 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=Truefallback 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.
Contributor guide
No contributing guide indexed for this repository
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
Research direction
Start in qlib/backtest/utils.py at TradeCalendarManager.get_step_time around lines 130-131, then trace how reset loads Cal.calendar(freq=freq, future=True). Reproduce a backtest ending on the final calendar bar with a provider lacking a future calendar, and verify that the boundary case completes safely or produces a clear actionable error instead of IndexError.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- python
- Domain
- data
- Issue type
- Bug
- Difficulty
- 3/5
- Estimated time
- 1-2 days
- Activity status
- Quiet
- Clarity
- Mostly clear
- Newbie friendliness
- 62/100