fix: backtest IndexError at the right calendar boundary (get_step_time peeks calendar[index+1])#2279
Open
zhaow-de wants to merge 2 commits into
Open
Conversation
…t calendar bar get_step_time formed each step's right endpoint by peeking the next calendar bar (`self._calendar[calendar_index + 1]`). On the final step `calendar_index == end_index`; when `end_time` is the last bar of the (future) calendar — e.g. a self-built dataset with no future calendar, where `Cal.calendar(future=True)` falls back to the current calendar — that peek indexed out of bounds and the backtest died with an opaque `IndexError: index N is out of bounds for axis 0 with size N` deep inside get_step_time. Clamp the right endpoint at the boundary: when no next bar exists, use the end of the current bar's period (`left + one freq unit`), mirroring the day-end logic already used in get_data_cal_range. The non-boundary path is unchanged. Add a regression test asserting get_step_time at the last calendar bar returns a well-defined single-bar interval instead of raising.
…loop) Expand the get_step_time boundary regression suite to the acceptance criteria raised on the issue: - day and minute frequencies at the final bar (same off-by-one at both granularities) - shift, which moves the effective boundary (shift=1 stays on the normal peek path) - the trade_step=None / current-step path - finished()/full-loop behaviour: exactly trade_len steps, no extra iteration - pre-boundary steps remain byte-for-byte identical to the old peek Verified these fail with the original IndexError on the pre-fix code and pass after. Also confirms the fix resolves the same-shape report in microsoft#1063.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Description
TradeCalendarManager.get_step_timeformed each step's right endpoint by peeking the next calendar bar (qlib/backtest/utils.py:131):On the final step
calendar_index == end_index. Whenend_timeis the last bar ofCal.calendar(future=True)— which is exactly what happens when a dataset has no future calendar configured, sincefuture=Truethen silently falls back to the current calendar —calendar_index + 1indexes out of bounds and the backtest dies with an opaqueIndexError: index N is out of bounds for axis 0 with size N, deep insideget_step_timeand far from the user'sbacktest(end_time=...)call.This PR clamps the right endpoint at the boundary: when no bar exists after the current one, it uses the end of the current bar's period (
left + one freq unit), mirroring the day-end logic already used inget_data_cal_range(utils.py:154):epsilon_changeis applied once to whichever right endpoint is chosen, so the closed-interval convention is identical on both paths. The final step stays a single bar (end - start = 1·freq - 1s < freq, sois_single_valueremains true), and every non-final step is unchanged (the original peek path is preserved exactly).Motivation and Context
Related issues: closes #2278, closes #1063 (same
IndexError: index 4132 is out of bounds for axis 0 with size 4132, same root cause; the old workaround was to setend_timebefore the last calendar date).get_step_timeis called on every backtest step, and the right-endpoint peek assumes there is always at least one bar afterend_time. That assumption holds only when a future calendar extends past the data. For a self-built provider without a future calendar,Cal.calendar(future=True)warns and returns the current calendar, so any backtest whoseend_timeis the last calendar day crashes at the final step. The failure is easy to hit ("backtest through the end of my data") and hard to diagnose. This makes such a backtest produce a well-defined final interval instead of crashing.How Has This Been Tested?
pytest qlib/tests/test_all_pipeline.pyunder upper directory ofqlib.New regression tests live in
tests/backtest/test_calendar_boundary.pyand map to the acceptance criteria discussed on #2278:IndexErrorat the final bartest_get_step_time_at_last_calendar_bar(real future calendar) /test_boundary_fallback_dayepsilon_change(calendar[i+1])test_non_boundary_step_unchangedtest_pre_boundary_steps_identical_to_old_peekdayand intraday (minute)test_boundary_fallback_day,test_boundary_fallback_minuteshiftmoves the effective boundary (shift=1 stays on the normal path)test_shift_moves_effective_boundarytrade_step=None/ current-step pathtest_current_step_pathfinished()correct — exactlytrade_lensteps, no extra iterationtest_full_loop_finished_and_step_countEach boundary-touching test was confirmed to fail with the original
IndexErroron the pre-fix code and to pass after the fix.Screenshots of Test Results (if appropriate):
tests/test_all_pipeline.py, full backtest exercisingget_step_timeon every step):tests/backtest/test_calendar_boundary.py):Types of changes