mirror of
https://ghfast.top/https://github.com/aeroxw/easy_tdx_max.git
synced 2026-09-12 15:44:18 +08:00
fix(tick_charts): 多日分时图 minutes>=1440 报 ValueError (Issue #10)
This commit is contained in:
@@ -2,6 +2,17 @@
|
||||
|
||||
本文件记录 easy-tdx 的版本变更。格式遵循 [Keep a Changelog](https://keepachangelog.com/zh-CN/)。
|
||||
|
||||
## [1.16.1] — 2026-07-01
|
||||
|
||||
### 修复
|
||||
|
||||
- **多日分时图 `tick --days N` 命令因 `minutes ≥ 1440` 报 `ValueError: hour must be in 0..23`**(`mac/commands/tick_charts.py`,[Issue #10](https://github.com/handsomejustin/easy_tdx/issues/10))— 执行 `easy-tdx tick SH 600519 --days 5` 时崩溃。多日分时图(MAC 协议 `0x123E`)解析 `time(minutes // 60, minutes % 60)` 缺少对 24 取模的保护,当个别服务器 / 数据状态下返回的 `minutes` 值 ≥ 1440(累计或异常值,用户实测出现 `minutes ≈ 62340` 即 `// 60 == 1039`)时,`minutes // 60` 超过 23 触发 `ValueError`。
|
||||
- 修复:改为 `time(minutes // 60 % 24, minutes % 60)`,与单日分时 `SymbolTickChartCmd` 的处理**完全一致**。
|
||||
- 语义自洽:每条 tick 的**日期**取自 `date_ints[d]`(与 `minutes` 无关),`minutes` 字段只承载「日内时刻」,`% 24` 折算成日内时刻是正确的降级。
|
||||
- **对正常数据零行为改变**:抓取多只股票 × {2 天, 5 天} 真实响应逐条对比,新公式与旧公式产出 `time` 对象**完全相同**(`new_vs_old_diffs=0`),所有时刻落在 09–15 交易时段。
|
||||
- 对异常 `minutes` 值,无法仅凭该字段恢复真正时刻(需日边界信息),故产出合法占位时刻,避免崩溃、不污染日期列。
|
||||
- 新增 3 个单元测试(`test_mac_tick_charts.py`:正常分钟 / Issue #10 回归用报错现场原值 62340 / 请求包布局),全量 703 单测通过。
|
||||
|
||||
## [1.16.0] — 2026-06-30
|
||||
|
||||
### 新增
|
||||
|
||||
+1
-1
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
|
||||
|
||||
[project]
|
||||
name = "easy-tdx"
|
||||
version = "1.16.0"
|
||||
version = "1.16.1"
|
||||
description = "通达信 TCP 协议行情数据客户端,支持在线行情、离线数据读取与写入同步"
|
||||
readme = "README.md"
|
||||
requires-python = ">=3.10"
|
||||
|
||||
@@ -72,7 +72,10 @@ class TickChartsCmd(BaseCommand[MacMultiTickChart]):
|
||||
)
|
||||
ticks.append(
|
||||
MacTick(
|
||||
time=time(minutes // 60, minutes % 60),
|
||||
# 多日分时里 minutes 在个别服务器/数据状态下可能 ≥ 1440
|
||||
# (累计或异常值),对 24 取模折算成日内时刻,避免 ValueError。
|
||||
# 与单日分时 SymbolTickChartCmd 的处理保持一致(Issue #10)。
|
||||
time=time(minutes // 60 % 24, minutes % 60),
|
||||
price=price,
|
||||
avg=avg,
|
||||
vol=vol,
|
||||
|
||||
@@ -0,0 +1,111 @@
|
||||
"""MAC 多日分时图(0x123E)解析测试。
|
||||
|
||||
包含 Issue #10 回归:当服务器返回的 minutes ≥ 1440(个别服务器累计或异常值)
|
||||
时,parse_response 不应抛 ValueError,而应按 % 24 折算为日内时刻。
|
||||
"""
|
||||
|
||||
import struct
|
||||
|
||||
from easy_tdx.mac.commands.tick_charts import TickChartsCmd
|
||||
from easy_tdx.models.enums import Market
|
||||
|
||||
|
||||
def _build_multi_tick_body(
|
||||
days: int,
|
||||
page_size: int,
|
||||
minutes_grid: list[list[int]],
|
||||
) -> bytes:
|
||||
"""构造一个多日分时图响应 body。
|
||||
|
||||
Args:
|
||||
days: 天数(写入 count)。
|
||||
page_size: 每天分时点数。
|
||||
minutes_grid: 长度 days 的列表,每个元素是长度 page_size 的 minutes 值列表。
|
||||
"""
|
||||
# 头部: market(2) + code(22)
|
||||
body = bytearray(struct.pack("<H22s", int(Market.SH), b"600519" + b"\x00" * 16))
|
||||
# 5 dates + 5 pre_closes(不足 5 天用 0 填充)at offset 24
|
||||
dates = [20250115, 20250114, 20250113, 20250110, 20250109]
|
||||
pre_closes = [1509.0, 1512.0, 1510.0, 1508.0, 1511.0]
|
||||
body += struct.pack("<5I", *dates)
|
||||
body += struct.pack("<5f", *pre_closes)
|
||||
# count(2) + send_last(1) + page_size(2) + total(2) at offset 64
|
||||
body += struct.pack("<HBHH", days, 1, page_size, days * page_size)
|
||||
# tick records at offset 71, each 14 bytes: <HffHH
|
||||
for day in minutes_grid:
|
||||
assert len(day) == page_size
|
||||
for m in day:
|
||||
body += struct.pack("<HffHH", m, 1510.0, 1510.0, 100, 0)
|
||||
# 尾部元数据: <44sBHf5x2I5ffIf12s2fI
|
||||
body += struct.pack("<44s", "贵州茅台".encode("gbk") + b"\x00" * 32)
|
||||
body += struct.pack("<BHf", 2, 0, 1.0) # decimal, category, vol_unit
|
||||
body += b"\x00" * 5
|
||||
body += struct.pack("<2I", 20250115, 0)
|
||||
body += struct.pack("<5f", 1509.0, 1510.0, 1520.0, 1500.0, 1515.0)
|
||||
body += struct.pack("<fI", 0.0, 100000) # momentum, vol
|
||||
body += struct.pack("<f", 1000000.0) # amount
|
||||
body += b"\x00" * 12
|
||||
body += struct.pack("<2f", 0.5, 1512.0) # turnover, avg
|
||||
body += struct.pack("<I", 0) # industry
|
||||
return bytes(body)
|
||||
|
||||
|
||||
def test_multi_tick_charts_normal_minutes():
|
||||
"""常规数据:minutes 每天 570..689, 780..899,解析正常。"""
|
||||
page_size = 4
|
||||
grid = [
|
||||
[570, 571, 572, 573],
|
||||
[570, 600, 780, 899],
|
||||
[570, 571, 572, 573],
|
||||
]
|
||||
body = _build_multi_tick_body(3, page_size, grid)
|
||||
|
||||
chart = TickChartsCmd(int(Market.SH), "600519", None, 3).parse_response(body)
|
||||
|
||||
assert len(chart.charts) == 3
|
||||
# 第一天第一个点 09:30:00
|
||||
assert str(chart.charts[0].ticks[0].time) == "09:30:00"
|
||||
# 最后一个点 14:59:00
|
||||
assert str(chart.charts[1].ticks[3].time) == "14:59:00"
|
||||
|
||||
|
||||
def test_multi_tick_charts_oversized_minutes_no_crash():
|
||||
"""Issue #10 回归:minutes ≥ 1440(如累计/异常值)不应抛 ValueError。
|
||||
|
||||
用户报告 hour must be in 0..23, not 1039(即 minutes // 60 == 1039)。
|
||||
修复后应按 % 24 折算为日内时刻,保证不崩溃且结果合理。
|
||||
"""
|
||||
page_size = 4
|
||||
# 第一天第 2 个点 minutes=62340(= 报错现场,//60=1039),其余正常
|
||||
grid = [
|
||||
[570, 62340, 572, 573],
|
||||
[570, 600, 780, 899],
|
||||
]
|
||||
body = _build_multi_tick_body(2, page_size, grid)
|
||||
|
||||
chart = TickChartsCmd(int(Market.SH), "600519", None, 2).parse_response(body)
|
||||
|
||||
assert len(chart.charts) == 2
|
||||
# 折算后 62340 -> 62340//60 % 24 = 1039 % 24 = 7 时, 62340 % 60 = 0 分
|
||||
assert str(chart.charts[0].ticks[1].time) == "07:00:00"
|
||||
# 正常点不受影响
|
||||
assert str(chart.charts[0].ticks[0].time) == "09:30:00"
|
||||
assert str(chart.charts[1].ticks[3].time) == "14:59:00"
|
||||
|
||||
|
||||
def test_multi_tick_charts_request_layout():
|
||||
"""验证请求包布局与文档一致:market + code(22) + start_ymd + days + 1。"""
|
||||
from datetime import date as date_cls
|
||||
|
||||
cmd = TickChartsCmd(int(Market.SH), "600519", date_cls(2025, 1, 15), 5)
|
||||
req = cmd.build_request()
|
||||
# MAC 帧前缀由 build_mac_request 生成,body 在其后;校验 body 嵌入其中
|
||||
expected_body = struct.pack(
|
||||
"<H22sIHH6x",
|
||||
int(Market.SH),
|
||||
b"600519" + b"\x00" * 16,
|
||||
20250115,
|
||||
5,
|
||||
1,
|
||||
)
|
||||
assert expected_body in req
|
||||
Reference in New Issue
Block a user