From 6322780ed0d2a58516d11bdee33735d13511b026 Mon Sep 17 00:00:00 2001 From: Kevin-Li-2025 <2242139@qq.com> Date: Tue, 23 Jun 2026 18:53:22 +0100 Subject: [PATCH] Handle calendar right boundary in backtest steps --- qlib/backtest/utils.py | 12 +++++-- tests/backtest/test_trade_calendar_manager.py | 35 +++++++++++++++++++ 2 files changed, 44 insertions(+), 3 deletions(-) create mode 100644 tests/backtest/test_trade_calendar_manager.py diff --git a/qlib/backtest/utils.py b/qlib/backtest/utils.py index 4210c9548a8..9db65e805bf 100644 --- a/qlib/backtest/utils.py +++ b/qlib/backtest/utils.py @@ -8,7 +8,7 @@ import numpy as np -from qlib.utils.time import epsilon_change +from qlib.utils.time import Freq, epsilon_change if TYPE_CHECKING: from qlib.backtest.decision import BaseTradeDecision @@ -86,7 +86,7 @@ def finished(self) -> bool: def step(self) -> None: if self.finished(): - raise RuntimeError(f"The calendar is finished, please reset it if you want to call it!") + raise RuntimeError("The calendar is finished, please reset it if you want to call it!") self.trade_step += 1 def get_freq(self) -> str: @@ -128,7 +128,13 @@ def get_step_time(self, trade_step: int | None = None, shift: int = 0) -> Tuple[ if trade_step is None: trade_step = self.get_trade_step() calendar_index = self.start_index + trade_step - shift - return self._calendar[calendar_index], epsilon_change(self._calendar[calendar_index + 1]) + step_start = pd.Timestamp(self._calendar[calendar_index]) + if calendar_index + 1 < len(self._calendar): + step_end = self._calendar[calendar_index + 1] + else: + freq = Freq(self.freq) + step_end = step_start + Freq.get_timedelta(freq.count, freq.base) + return step_start, epsilon_change(pd.Timestamp(step_end)) def get_data_cal_range(self, rtype: str = "full") -> Tuple[int, int]: """ diff --git a/tests/backtest/test_trade_calendar_manager.py b/tests/backtest/test_trade_calendar_manager.py new file mode 100644 index 00000000000..538b6f24322 --- /dev/null +++ b/tests/backtest/test_trade_calendar_manager.py @@ -0,0 +1,35 @@ +import numpy as np +import pandas as pd + +from qlib.backtest.utils import TradeCalendarManager + + +def _make_calendar_manager(): + trade_calendar = TradeCalendarManager.__new__(TradeCalendarManager) + trade_calendar.freq = "day" + trade_calendar.start_index = 0 + trade_calendar.end_index = 1 + trade_calendar.trade_len = 2 + trade_calendar.trade_step = 1 + trade_calendar._calendar = np.array( + [pd.Timestamp("2020-01-01"), pd.Timestamp("2020-01-02")] + ) + return trade_calendar + + +def test_get_step_time_uses_next_calendar_when_available(): + trade_calendar = _make_calendar_manager() + + start_time, end_time = trade_calendar.get_step_time(trade_step=0) + + assert start_time == pd.Timestamp("2020-01-01") + assert end_time == pd.Timestamp("2020-01-01 23:59:59") + + +def test_get_step_time_infers_right_boundary_for_last_calendar_bar(): + trade_calendar = _make_calendar_manager() + + start_time, end_time = trade_calendar.get_step_time() + + assert start_time == pd.Timestamp("2020-01-02") + assert end_time == pd.Timestamp("2020-01-02 23:59:59")