Skip to content

Commit 2210201

Browse files
committed
chore: bump version to 2.12.2
1 parent b784466 commit 2210201

7 files changed

Lines changed: 121 additions & 8 deletions

File tree

CHANGELOG.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,12 @@ All notable changes to this project will be documented in this file.
55
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
66
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
77

8+
## [2.12.2] - 2026-05-27
9+
10+
### Fixed
11+
12+
- **Cross-frequency data fallback**`get_history` and `get_price` no longer return empty results when querying minute/period data during daily backtests. Previously, with `current_dt` at midnight (00:00:00), `searchsorted` returned -1 for minute data starting at 09:31, causing stocks to be silently skipped. Now falls back to 15:00 market close on the same day to locate the latest available bar.
13+
814
## [2.12.1] - 2026-05-23
915

1016
### Fixed

README.de.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
[![Python](https://img.shields.io/badge/Python-3.9+-blue.svg)](https://www.python.org/)
88
[![License](https://img.shields.io/badge/License-AGPL--3.0-blue.svg)](LICENSE)
99
[![License: Commercial](https://img.shields.io/badge/License-Commercial--Available-red)](licenses/LICENSE-COMMERCIAL.md)
10-
[![Version](https://img.shields.io/badge/Version-2.12.0-orange.svg)](#)
10+
[![Version](https://img.shields.io/badge/Version-2.12.2-orange.svg)](#)
1111
[![PyPI](https://img.shields.io/pypi/v/simtradelab.svg)](https://pypi.org/project/simtradelab/)
1212
[![PyPI - Downloads](https://img.shields.io/pypi/dm/simtradelab.svg)](https://pypi.org/project/simtradelab/)
1313

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ English | [中文](README.zh-CN.md) | [Deutsch](README.de.md)
77
[![Python](https://img.shields.io/badge/Python-3.9+-blue.svg)](https://www.python.org/)
88
[![License](https://img.shields.io/badge/License-AGPL--3.0-blue.svg)](LICENSE)
99
[![License: Commercial](https://img.shields.io/badge/License-Commercial--Available-red)](licenses/LICENSE-COMMERCIAL.md)
10-
[![Version](https://img.shields.io/badge/Version-2.12.0-orange.svg)](#)
10+
[![Version](https://img.shields.io/badge/Version-2.12.2-orange.svg)](#)
1111
[![PyPI](https://img.shields.io/pypi/v/simtradelab.svg)](https://pypi.org/project/simtradelab/)
1212
[![PyPI - Downloads](https://img.shields.io/pypi/dm/simtradelab.svg)](https://pypi.org/project/simtradelab/)
1313

README.zh-CN.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
[![Python](https://img.shields.io/badge/Python-3.9+-blue.svg)](https://www.python.org/)
88
[![License](https://img.shields.io/badge/License-AGPL--3.0-blue.svg)](LICENSE)
99
[![License: Commercial](https://img.shields.io/badge/License-Commercial--Available-red)](licenses/LICENSE-COMMERCIAL.md)
10-
[![Version](https://img.shields.io/badge/Version-2.12.0-orange.svg)](#)
10+
[![Version](https://img.shields.io/badge/Version-2.12.2-orange.svg)](#)
1111
[![PyPI](https://img.shields.io/pypi/v/simtradelab.svg)](https://pypi.org/project/simtradelab/)
1212
[![PyPI - Downloads](https://img.shields.io/pypi/dm/simtradelab.svg)](https://pypi.org/project/simtradelab/)
1313

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
# Poetry configuration
22
[tool.poetry]
33
name = "simtradelab"
4-
version = "2.12.1"
4+
version = "2.12.2"
55
description = "Lightweight quantitative backtesting framework with PTrade API simulation | 轻量级量化回测框架"
66
authors = ["kay <kayou@duck.com>"]
77
license = "AGPL-3.0-or-later"

src/simtradelab/ptrade/api.py

Lines changed: 13 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1199,11 +1199,14 @@ def get_price(
11991199

12001200
try:
12011201
if frequency in _MINUTE_FREQ_MINUTES or frequency in _PERIOD_FREQ_RULE:
1202-
# 分钟数据:直接使用index查找
1202+
# 分钟/周期数据:使用searchsorted查找
12031203
# 用 DatetimeIndex.searchsorted 避免 datetime64[us] vs ns 单位不匹配
12041204
idx = stock_df.index.searchsorted(end_dt, side="right") - 1
12051205
if idx < 0:
1206-
continue
1206+
end_of_day = end_dt.normalize() + pd.Timedelta(hours=15)
1207+
idx = stock_df.index.searchsorted(end_of_day, side="right") - 1
1208+
if idx < 0:
1209+
continue
12071210
current_idx = idx
12081211
else:
12091212
current_idx = self._resolve_daily_index(stock, stock_df, end_dt)
@@ -1539,11 +1542,17 @@ def get_history(
15391542
continue
15401543
try:
15411544
if frequency in _MINUTE_FREQ_MINUTES or frequency in _PERIOD_FREQ_RULE:
1542-
# 分钟数据:使用searchsorted查找
1545+
# 分钟/周期数据:使用searchsorted查找
15431546
# 用 DatetimeIndex.searchsorted 避免 datetime64[us] vs ns 单位不匹配
15441547
idx = data_source.index.searchsorted(current_dt, side="right") - 1
15451548
if idx < 0:
1546-
continue
1549+
# 跨频率场景:日线回测取分钟/周期数据时,
1550+
# current_dt 是当天 00:00:00,searchsorted 返回 0 导致 idx=-1。
1551+
# 对齐到当天收盘时间后重试,向前取最近的 bar。
1552+
end_of_day = current_dt.normalize() + pd.Timedelta(hours=15)
1553+
idx = data_source.index.searchsorted(end_of_day, side="right") - 1
1554+
if idx < 0:
1555+
continue
15471556
current_idx = idx
15481557
else:
15491558
current_idx = self._resolve_daily_index(stock, data_source, current_dt)

tests/unit/test_broker_profile_compat.py

Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -252,6 +252,104 @@ def test_get_history_supports_multi_frequency_5m_and_1w(self, ptrade_api, test_d
252252
assert isinstance(result_1w, pd.DataFrame)
253253
assert len(result_1w) > 0
254254

255+
# ── 跨频率回退路径:日线回测 + 分钟/周期数据 ──
256+
257+
def test_daily_context_1m_frequency_get_history(self, ptrade_api):
258+
"""日线回测取1分钟数据 — searchsorted(midnight) 触发 idx<0 回退"""
259+
_set_broker_profile(ptrade_api, "auto")
260+
ptrade_api.context.frequency = "1d"
261+
ptrade_api.context.current_dt = pd.Timestamp("2024-01-04 00:00:00")
262+
263+
idx_1m = pd.date_range("2024-01-04 09:31:00", periods=240, freq="min").union(
264+
pd.date_range("2024-01-04 13:01:00", periods=120, freq="min")
265+
)
266+
ptrade_api.data_context.stock_data_dict_1m = {
267+
"600000.SH": pd.DataFrame(
268+
{
269+
"open": np.linspace(10.0, 10.5, len(idx_1m)),
270+
"close": np.linspace(10.0, 10.5, len(idx_1m)),
271+
"volume": np.ones(len(idx_1m)) * 100.0,
272+
"money": np.ones(len(idx_1m)) * 1000.0,
273+
},
274+
index=idx_1m,
275+
),
276+
}
277+
278+
result = ptrade_api.get_history(
279+
count=1, frequency="1m", field="close", security_list=["600000.SH"]
280+
)
281+
assert isinstance(result, pd.DataFrame)
282+
assert not result.empty, "回退后不应返回空结果"
283+
assert len(result) == 1
284+
285+
def test_daily_context_1m_frequency_get_price(self, ptrade_api):
286+
"""日线回测 get_price 取1分钟数据 — get_price 同名回退路径"""
287+
_set_broker_profile(ptrade_api, "auto")
288+
ptrade_api.context.frequency = "1d"
289+
ptrade_api.context.current_dt = pd.Timestamp("2024-01-04 00:00:00")
290+
291+
idx_1m = pd.date_range("2024-01-04 09:31:00", periods=240, freq="min").union(
292+
pd.date_range("2024-01-04 13:01:00", periods=120, freq="min")
293+
)
294+
ptrade_api.data_context.stock_data_dict_1m = {
295+
"600000.SH": pd.DataFrame(
296+
{
297+
"open": np.linspace(10.0, 10.5, len(idx_1m)),
298+
"close": np.linspace(10.0, 10.5, len(idx_1m)),
299+
"volume": np.ones(len(idx_1m)) * 100.0,
300+
"money": np.ones(len(idx_1m)) * 1000.0,
301+
},
302+
index=idx_1m,
303+
),
304+
}
305+
306+
result = ptrade_api.get_price(
307+
"600000.SH", count=1, frequency="1m", fields="close"
308+
)
309+
assert isinstance(result, pd.DataFrame)
310+
assert not result.empty
311+
312+
def test_daily_context_5m_frequency(self, ptrade_api):
313+
"""日线回测取5分钟数据 — 聚合频率同样经过 searchsorted 回退"""
314+
_set_broker_profile(ptrade_api, "auto")
315+
ptrade_api.context.frequency = "1d"
316+
ptrade_api.context.current_dt = pd.Timestamp("2024-01-04 00:00:00")
317+
318+
idx_1m = pd.date_range("2024-01-04 09:31:00", periods=240, freq="min").union(
319+
pd.date_range("2024-01-04 13:01:00", periods=120, freq="min")
320+
)
321+
ptrade_api.data_context.stock_data_dict_1m = {
322+
"600000.SH": pd.DataFrame(
323+
{
324+
"open": np.linspace(10.0, 10.5, len(idx_1m)),
325+
"close": np.linspace(10.0, 10.5, len(idx_1m)),
326+
"volume": np.ones(len(idx_1m)) * 100.0,
327+
"money": np.ones(len(idx_1m)) * 1000.0,
328+
},
329+
index=idx_1m,
330+
),
331+
}
332+
333+
result = ptrade_api.get_history(
334+
count=3, frequency="5m", field="close", security_list=["600000.SH"]
335+
)
336+
assert isinstance(result, pd.DataFrame)
337+
assert not result.empty
338+
339+
def test_daily_context_no_data_still_skipped(self, ptrade_api):
340+
"""无数据的股票回退后仍正确跳过,不报错"""
341+
_set_broker_profile(ptrade_api, "auto")
342+
ptrade_api.context.frequency = "1d"
343+
ptrade_api.context.current_dt = pd.Timestamp("2024-01-04 00:00:00")
344+
345+
ptrade_api.data_context.stock_data_dict_1m = {}
346+
347+
result = ptrade_api.get_history(
348+
count=1, frequency="1m", field="close", security_list=["000001.SZ"]
349+
)
350+
assert isinstance(result, pd.DataFrame)
351+
assert result.empty
352+
255353
def test_get_history_unlimited_dtype_shanxi(self, ptrade_api, test_dates):
256354
_set_broker_profile(ptrade_api, "shanxi")
257355
ptrade_api.context.current_dt = test_dates[-1]

0 commit comments

Comments
 (0)