mirror of
https://ghfast.top/https://github.com/aeroxw/easy_tdx_max.git
synced 2026-09-12 16:54:20 +08:00
fix!: 扩展日线槽位语义——第6槽为 float32 成交额,移除误导字段 hk_stock_amount(#57) (#61)
read_ex_daily_bars 原把第7槽(成交量)同时赋给 amount 与 vol(amount=vol), 真实成交额藏在第6槽 float32 重解释值里、以 hk_stock_amount 之名暴露。 实测对照:47#IF300 2023-09-11 第6槽 float32=186871758848(元)、第7槽 uint32=105358016(手),与标准市场 sh000300.day 同日 amount/vol 完全一致。 - _EX_DAILY_FMT: <IffffIIf → <IfffffIf,第6槽正名为 float32 成交额 - ExDailyBar.amount: int → float;移除语义错误的 hk_stock_amount 字段 - encode 端沿新 fmt 自动正确:大成交额按 float32 编码,不再溢出 uint32 - CLI offline ex-daily 输出新增 amount 列 - 新增真实样本回归测试(47#IF300 原始记录)+ 补齐缺失的 amount 往返断言 Co-authored-by: GitHub <action@github.com>
This commit is contained in:
@@ -2,6 +2,31 @@
|
||||
|
||||
本文件记录 easy-tdx 的版本变更。格式遵循 [Keep a Changelog](https://keepachangelog.com/zh-CN/)。
|
||||
|
||||
## [1.21.0] — 2026-08-31
|
||||
|
||||
**扩展日线(vipdoc/ds `*.day`)解析槽位错误**(Issue #57,含破坏性 API 变更)——`read_ex_daily_bars` 把第 7 槽(成交量)同时赋给 `amount` 与 `vol`(`amount=vol`),真正的成交额藏在第 6 槽 float32 重解释值里、以误导性字段名 `hk_stock_amount` 暴露。实测铁证:扩展市场 `47#IF300`(沪深300)2023-09-11 第 6 槽 float32 = 186,871,758,848、第 7 槽 uint32 = 105,358,016,与标准市场 `sh000300.day` 同日 amount(元)/ vol(手)**完全一致**,证明第 6 槽是 float32 成交额、第 7 槽是 uint32 成交量。`_EX_DAILY_FMT` 旧声明 `<IffffIIf` 还使写端把成交额按 uint32 编码——真实成交额动辄超 42.9 亿上限(如上述 1868 亿),`struct.pack('I')` 必然溢出报错。
|
||||
|
||||
### 修复(破坏性变更)
|
||||
|
||||
- **`offline/ex_daily_bar.py`**——记录格式改为 `<IfffffIf`:第 6 槽正名为 float32 成交额,直接解进 `amount`;`ExDailyBar.amount` 类型 `int → float`;**移除语义错误的 `hk_stock_amount` 字段**(其值本就是成交额,并非"港股数量")。
|
||||
- **`offline/write_ex_daily.py`**——无需改动即自动正确:encode 沿用同一 fmt,成交额现在按 float32 编码,超 uint32 的大额不再溢出。
|
||||
- **`cli/cmd_offline.py`**——`offline ex-daily` 输出新增 `amount` 列(此前该数据完全不可见)。
|
||||
|
||||
### 测试
|
||||
|
||||
- 新增真实样本回归 `TestRealSampleSlots`(2 例):`47#IF300` 2023-09-11 原始 32 字节记录断言 `amount=186871758848.0`、`vol=105358016`、`amount != vol`(防串槽回归);18.69 万倍 uint32 上限的大成交额编码-解码往返不溢出。
|
||||
- 补上此前缺失的 round-trip `amount` 断言(旧实现 amount=vol 恰好双双等于 vol,往返测试无法暴露,这是 bug 长期潜伏的原因)。
|
||||
- 全套 1052 个单测通过。
|
||||
|
||||
## [1.20.13] — 2026-08-31
|
||||
|
||||
**`offline sync-daily` 大盘股成交量 uint32 溢出**(PR #60,社区贡献者 @awayings)——K 线协议返回的成交量单位是**股**,而 `encode_daily_bar` 按 `vol / vol_coeff`(A 股 vol_coeff=0.01 即 ×100)写入 .day,期望输入为**手**。原 `_sync_one_daily` 直接把股喂给 `append_daily_bars`,单日成交 > 4295 万股的股票(招商银行 2026-08-31 单日 1.14 亿股等)编码后超出 uint32 上限,`struct.error` 直接失败;低成交量股票不触发,故长期未被发现。
|
||||
|
||||
### 修复
|
||||
|
||||
- **`cli/cmd_offline.py` `_sync_one_daily`**——对 `vol_coeff == 0.01` 的证券类型(A/B 股、深市基金等)写入前换算 股→手(`vol /= 100`),与读取端 `read_daily_bars` 的 `vol × 0.01` 方向对称。作者用服务器原生 .day 文件(0x06B9 下载)实测验证:浦发银行 2026-08-31 原始 vol 字段 99,682,464(股)与协议 API 完全一致。
|
||||
- 已知边界(未处理):单日成交 > 42.9 亿股的极端天量换算后仍超上限;实测通达信官方 .day 对该 bar 亦降级存储,如需对齐另行讨论。
|
||||
|
||||
## [1.20.12] — 2026-08-28
|
||||
|
||||
**`ex tick --date` 传 YYYYMMDD 整数直接崩溃**(PR #56,社区贡献者 @Harveyliu007)——CLI 的 `--date` 选项传入 `YYYYMMDD` 整数,而 `MacExClient.goods_tick_chart()` 只接受 `datetime.date`(内部直接 `query_date.year` 编码),实跑必抛 `AttributeError: 'int' object has no attribute 'year'`;`cmd_ex.py` 的调用点长期带 `# type: ignore[arg-type]`,类型系统没能拦住。A 股侧 `MacClient.get_tick_chart()` 早已支持 int 日期(内部转换),ex 侧漏了同类处理。
|
||||
|
||||
+1
-1
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
|
||||
|
||||
[project]
|
||||
name = "easy-tdx"
|
||||
version = "1.20.13"
|
||||
version = "1.21.0"
|
||||
description = "通达信 TCP 协议行情数据客户端,支持在线行情、离线数据读取与写入同步"
|
||||
readme = "README.md"
|
||||
requires-python = ">=3.10"
|
||||
|
||||
@@ -280,6 +280,7 @@ def ex_daily(
|
||||
"high": b.high,
|
||||
"low": b.low,
|
||||
"close": b.close,
|
||||
"amount": b.amount,
|
||||
"vol": b.vol,
|
||||
"settlement": b.settlement,
|
||||
}
|
||||
|
||||
@@ -6,22 +6,26 @@ from pathlib import Path
|
||||
|
||||
from ..exceptions import TdxFileNotFoundError
|
||||
|
||||
# 日期(4B) 开盘(4Bf) 最高(4Bf) 最低(4Bf) 收盘(4Bf) 成交额(4B) 成交量(4B) 结算价(4Bf)
|
||||
_EX_DAILY_FMT = struct.Struct("<IffffIIf")
|
||||
# 日期(4B) 开盘(4Bf) 最高(4Bf) 最低(4Bf) 收盘(4Bf) 成交额(4Bf) 成交量(4B) 结算价(4Bf)
|
||||
# 第 6 槽为 float32 成交额(元):与标准市场 sh000300.day 实测对照一致
|
||||
# (47#IF300 2023-09-11 第 6 槽 = 186871758848.0 元 = sh000300 同日 amount)。
|
||||
_EX_DAILY_FMT = struct.Struct("<IfffffIf")
|
||||
|
||||
|
||||
@dataclass
|
||||
class ExDailyBar:
|
||||
"""扩展市场日线(期货/港股等,含结算价)。"""
|
||||
"""扩展市场日线(期货/港股等,含结算价)。
|
||||
|
||||
amount 为成交额(元,float32),vol 为成交量(手,uint32)。
|
||||
"""
|
||||
|
||||
open: float
|
||||
high: float
|
||||
low: float
|
||||
close: float
|
||||
amount: int
|
||||
amount: float
|
||||
vol: int
|
||||
settlement: float
|
||||
hk_stock_amount: float
|
||||
year: int
|
||||
month: int
|
||||
day: int
|
||||
@@ -52,11 +56,7 @@ def read_ex_daily_bars(filepath: str | Path) -> list[ExDailyBar]:
|
||||
|
||||
for offset in range(0, len(data) - record_size + 1, record_size):
|
||||
raw = data[offset : offset + record_size]
|
||||
date_int, op, hi, lo, cl, amt, vol, settlement = _EX_DAILY_FMT.unpack(raw)
|
||||
|
||||
# 第 5 个字段(成交额位置)重新解释为 float 作为港股量
|
||||
hk_bytes = struct.pack("<I", amt)
|
||||
(hk_stock_amount,) = struct.unpack("<f", hk_bytes)
|
||||
date_int, op, hi, lo, cl, amount, vol, settlement = _EX_DAILY_FMT.unpack(raw)
|
||||
|
||||
year = date_int // 10000
|
||||
month = (date_int % 10000) // 100
|
||||
@@ -68,10 +68,9 @@ def read_ex_daily_bars(filepath: str | Path) -> list[ExDailyBar]:
|
||||
high=hi,
|
||||
low=lo,
|
||||
close=cl,
|
||||
amount=vol,
|
||||
amount=amount,
|
||||
vol=vol,
|
||||
settlement=settlement,
|
||||
hk_stock_amount=hk_stock_amount,
|
||||
year=year,
|
||||
month=month,
|
||||
day=day,
|
||||
|
||||
@@ -26,7 +26,7 @@ def _make_ex_bar(
|
||||
high: float = 3550.0,
|
||||
low: float = 3480.0,
|
||||
close: float = 3520.0,
|
||||
amount: int = 123456,
|
||||
amount: float = 123456.0,
|
||||
vol: int = 98765,
|
||||
settlement: float = 3510.0,
|
||||
) -> ExDailyBar:
|
||||
@@ -38,7 +38,6 @@ def _make_ex_bar(
|
||||
amount=amount,
|
||||
vol=vol,
|
||||
settlement=settlement,
|
||||
hk_stock_amount=0.0,
|
||||
year=year,
|
||||
month=month,
|
||||
day=day,
|
||||
@@ -53,7 +52,7 @@ def _make_raw_ex_bar(
|
||||
high: float = 3550.0,
|
||||
low: float = 3480.0,
|
||||
close: float = 3520.0,
|
||||
amount: int = 123456,
|
||||
amount: float = 123456.0,
|
||||
vol: int = 98765,
|
||||
settlement: float = 3510.0,
|
||||
) -> bytes:
|
||||
@@ -101,7 +100,7 @@ class TestEncodeExDailyBar:
|
||||
|
||||
class TestExRoundTrip:
|
||||
def test_single_bar_round_trip(self, tmp_path: Path) -> None:
|
||||
bar = _make_ex_bar(open_=3500.0, close=3520.0, vol=98765)
|
||||
bar = _make_ex_bar(open_=3500.0, close=3520.0, amount=123456.0, vol=98765)
|
||||
encoded = encode_ex_daily_bar(bar)
|
||||
|
||||
filepath = tmp_path / "29#A1801.day"
|
||||
@@ -113,6 +112,7 @@ class TestExRoundTrip:
|
||||
assert b.year == 2026 and b.month == 6 and b.day == 6
|
||||
assert abs(b.open - 3500.0) < 0.01
|
||||
assert abs(b.close - 3520.0) < 0.01
|
||||
assert abs(b.amount - 123456.0) < 0.01
|
||||
assert b.vol == 98765
|
||||
|
||||
def test_multiple_bars_round_trip(self, tmp_path: Path) -> None:
|
||||
@@ -188,3 +188,41 @@ class TestAppendExDailyBars:
|
||||
|
||||
bars = read_ex_daily_bars(filepath)
|
||||
assert len(bars) == 2
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# 真实样本回归(issue #57):槽位语义
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
# 47#IF300(扩展市场沪深300 指数)2023-09-11 的真实记录。
|
||||
# 与标准市场 sh000300.day 同日对照:vol=105358016(手)、amount=186871758848.0(元)完全一致,
|
||||
# 证明第 6 槽是 float32 成交额,第 7 槽是 uint32 成交量。
|
||||
_REAL_IF300_20230911 = bytes.fromhex(
|
||||
"ffb2340148c969451fc56c45856b6945a4786b45b3092e52c0a2470600000000"
|
||||
)
|
||||
|
||||
|
||||
class TestRealSampleSlots:
|
||||
def test_amount_is_float32_turnover(self, tmp_path: Path) -> None:
|
||||
filepath = tmp_path / "47#IF300.day"
|
||||
filepath.write_bytes(_REAL_IF300_20230911)
|
||||
|
||||
bars = read_ex_daily_bars(filepath)
|
||||
assert len(bars) == 1
|
||||
b = bars[0]
|
||||
assert (b.year, b.month, b.day) == (2023, 9, 11)
|
||||
assert b.amount == 186871758848.0
|
||||
assert b.vol == 105358016
|
||||
assert abs(b.close - 3767.54) < 0.01
|
||||
# amount 与 vol 不得串槽(回归:旧实现 amount=vol)
|
||||
assert b.amount != b.vol
|
||||
|
||||
def test_large_amount_does_not_overflow(self, tmp_path: Path) -> None:
|
||||
# 真实成交额动辄超 uint32 上限,第 6 槽必须按 float32 编码
|
||||
bar = _make_ex_bar(amount=186871758848.0)
|
||||
encoded = encode_ex_daily_bar(bar)
|
||||
filepath = tmp_path / "47#IF300.day"
|
||||
filepath.write_bytes(encoded)
|
||||
|
||||
(b,) = read_ex_daily_bars(filepath)
|
||||
assert b.amount == 186871758848.0
|
||||
|
||||
Reference in New Issue
Block a user