mirror of
https://ghfast.top/https://github.com/aeroxw/easy_tdx_max.git
synced 2026-09-12 16:54:20 +08:00
fix(mac): 异动 0x1237 补齐 0x15/0x16/0x1D/0x1E 解析并修正 0x13 试盘方向(#62)
- 0x16 盘中强势/弱势:09:25 样本 v2 与开盘涨幅 49/49 精确一致,v1 为带符号 ±1~3 级强弱等级 - 0x15 竞价/尾盘异动:09:25 与 15:00 双时刻触发,desc 按时刻区分竞价/尾盘前缀 - 0x1D/0x1E 急速拉升/急速下跌:阈值下限 ±0.6% - 0x13 竞价试盘:v1=0x00 试买/0x01 试卖(552 条对照昨收 549 条一致),旧实现试卖方向描述错误 - 新增顶层常量 UNUSUAL_TYPE_NAMES(19 种类型码→名称) - 协议探索结论与完整证据链文档化 docs/protocol-unknown-fields.md §3.4 - 测试 test_unusual.py 21 例(全部真实抓包字节 fixture)
This commit is contained in:
@@ -1474,6 +1474,16 @@ with MacClient.from_best_host() as c:
|
||||
df = c.get_server_info() # 服务器交易时段
|
||||
```
|
||||
|
||||
`get_unusual` 返回列含 `unusual_type`(类型码)与 `desc`(中文描述),共 19 种类型
|
||||
(主力买卖/加速拉升/急速拉升/盘中强弱/竞价异动/涨跌停/大单盘口等)。类型码→名称
|
||||
可用顶层常量映射:
|
||||
|
||||
```python
|
||||
from easy_tdx import UNUSUAL_TYPE_NAMES
|
||||
|
||||
df["type_name"] = df["unusual_type"].map(UNUSUAL_TYPE_NAMES)
|
||||
```
|
||||
|
||||
### 扩展市场
|
||||
|
||||
```python
|
||||
|
||||
@@ -206,6 +206,56 @@ u1 范围与价格非线性质相关(茅台价格 140x → u1 范围 300x)
|
||||
|
||||
`commands/transaction.py:73,97`:`unknown_last`,pytdx 原来直接丢弃。
|
||||
|
||||
### 3.4 市场异动(0x1237)异动类型与数据区(已确认,Issue #62)
|
||||
|
||||
`mac/commands/unusual.py`:每条 32 字节记录 = `<H6sBBBHH>` 头(15B)+ 13B 类型数据区
|
||||
(`<B2fI`:v1, v2, v3, v4)+ offset 28 保留字节(全类型实测恒 0x00)+ `<BH>` 时间。
|
||||
|
||||
**2026-09-01 全天跟踪实测**(09:25~15:05 每 10 分钟一轮全量拉取 SH/SZ/BJ,收盘 17848 条)
|
||||
补齐 4 个缺失类型:
|
||||
|
||||
| 类型 | 语义 | 数据区语义 | 证据 |
|
||||
|------|------|-----------|------|
|
||||
| 0x13 | 竞价试盘(试买/试卖) | v1=0x00 试买(申报价高于昨收)/ 0x01 试卖(低于昨收);v2=申报价;v3=竞价量(手) | 2026-09-02 全量 552 条对照昨收:v1 方向规律 549 条一致(2 条恰等于昨收 + 1 条异常);触发窗口 09:15~09:20。旧实现一律显示「竞价试买」,试卖方向描述错误 |
|
||||
| 0x15 | 竞价/尾盘异动(拉升/下跌/平稳),**双时刻信号** | v1=0x02/0x03/0x01 方向档(±0.5% 分档);v2=时段尾段价格变动(相对昨收 pp);v3=该时段成交量(手) | 开盘竞价:早间 1191 条全部落在 09:25:00~09:25:02;收盘:86 条全部落在 15:00:01~15:00:04(SH 52 / SZ 29 / BJ 5),v1 分档规律与早间一致、幅度更小(±0.5%~±2.6%)。v2 参考时刻扫描收敛于 09:23:30~09:24:00(60 样本 MAE 0.11pp,受竞价 3 秒采样粒度限制);v3 略小于最终撮合量(如 600551 v3=40254 vs 终值 44865)。desc 按记录小时区分「竞价/尾盘」前缀 |
|
||||
| 0x16 | 盘中强势/弱势 | v2=触发时涨跌幅;v1=带符号强弱等级(0x01~0x03 强势 1~3 级,0xFD~0xFF 弱势 1~3 级,补码);v3≈v2(差值 mean 0.83pp,语义未定) | 09:25 的 49 条样本 v2 与当日开盘涨幅(open/pre_close-1)**49/49 精确一致**;全时段与收盘涨跌幅符号一致率 97%~100%;v1 六组对应 v2 区间互不重叠且单调(0x03: +7.1%~+245.9%、0xFD: -10.0%~-9.0% 等) |
|
||||
| 0x1D | 急速拉升 | v1 恒 0x00(方向);v2=短窗涨幅(阈值下限 +0.6%);v3 恒 0 | 全天 1127 条,v2 恒正(+0.6%~+3.9%) |
|
||||
| 0x1E | 急速下跌 | v1 恒 0x01(方向);v2 恒负(-0.6%~-4.0%);v3 恒 0 | 全天 586 条 |
|
||||
|
||||
**0x14 的竞价试盘子族**:09:15:00~09:25 期间撮合参考价触板/近板即触发(SH 早间 531 条),
|
||||
与 PC 客户端推送协议的「涨停试盘/跌停试盘」对应。数据区布局与盘中 0x14 相同
|
||||
(v1 方向、data[1] sub_type、float 价/量),现有解析器无需改动——如 600551 09:15:00
|
||||
竞价参考价 9.08(+10.06%,恰在涨停价)→「封涨停板」;600127 09:15:00 参考价 10.84
|
||||
(-9.97%)→「逼近跌停」。
|
||||
|
||||
**全天类型普查结论**:收盘全量 17848 条中类型集恒为 17 种(0x03~0x0B、0x10、0x11、
|
||||
0x13~0x16、0x1D、0x1E),午间、下午、收盘竞价各窗口均无新类型码出现;2026-09-02
|
||||
盘中(10:27)复查亦为同一 17 种。已知但两个交易日零触发的 2 种:0x0C 区间缩量、
|
||||
0x12 大单锁盘(语义仍承自 pytdx2 一系,待采样验证)。PC 推送协议特有的「大笔买入/卖出、
|
||||
主力急入/急出、急速上涨/猛烈下跌、强势封涨停/跌停、涨幅超 10% 整数倍」等信号
|
||||
(PigQuant 逆向的 0x40080cd1+ 消息 ID 体系)在 0x1237 拉取协议中**无对应类型码**,
|
||||
两套协议的信号空间不同构:PC 端「逼近/封/打开涨跌停」对应 0x14 子类型、「买一/卖一
|
||||
剩余大」对应 0x10/0x11、「急速拉升/猛烈打压」对应 0x1D/0x1E。
|
||||
|
||||
**排除项**:
|
||||
- pytdx2(QuantJia/pytdx2 `parser/stock.py`)把 0x15 标作"尾盘(对倒/拉升)"——只对了一半:
|
||||
0x15 在开盘竞价(09:25)与收盘(15:00)双时刻触发,他们大概只观测过下午的记录;
|
||||
其 0x16"盘中弱势/强势"方向语义(v2 符号)与实测一致。
|
||||
- 0x00~0x02、0x0D、0x0E、0x0F、0x17~0x1C、0x1F+ 全天零出现(未实现或极罕见)。
|
||||
- 0x16 的 v1 等级公式未完全破解:等级与 v2 非纯函数关系(L2/L3 区间在 +7.1%~+9.0% 重叠),
|
||||
疑与涨停幅度(ST/创业/科创)相关,待后续验证。
|
||||
|
||||
**请求监控参数**(`<HH2xH2xH5H` 尾部 6×H,默认 1,200,30,40,50,200)单维扫描
|
||||
(每位置 15 个候选值):位置 1~4 完全惰性;位置 0 仅低字节有效(≥0x400 返回空);
|
||||
位置 5 在 1~127 区间致命(bit7 或 0 才有效)。所有有效变体返回类型集完全一致——
|
||||
**这 6 个参数不是信号类型开关**,真实字段划分可能并非 6 个独立 H。
|
||||
|
||||
**其他观察**:
|
||||
- 北交所(Market.BJ=2)同样支持 0x1237(当日 1030 条,类型集相同)。
|
||||
- 异动列表非静态快照:午间休市期间计数仍在漂移(0x07: 499→604),服务端持续重算/改写记录。
|
||||
- 数据区第 4 槽(offset 25~28)本库按 uint32 解(`<B2fI`),pytdx2 按 float 解
|
||||
(`<Bfff`);对既有类型仅 0x10 大单托盘使用该槽,两种解法在该场景下显示等价,语义归属待定。
|
||||
|
||||
## 四、硬跳过的字节块
|
||||
|
||||
### 4.1 xdxr_info 9字节响应头部(已确认:请求回显)
|
||||
|
||||
@@ -13,9 +13,21 @@ UnusualItem dataclass 字段:
|
||||
code str 证券代码
|
||||
name str 证券名称
|
||||
time time 异动时间
|
||||
desc str 异动描述(如 "5分钟涨幅>3%"、"快速拉升"、"大笔买入")
|
||||
value str 异动数值(如 "3.52%"、"5000手")
|
||||
unusual_type int 异动类型代码(1=5分钟涨幅, 2=5分钟跌幅, 3=快速拉升, 4=大笔成交等)
|
||||
desc str 异动描述(如 "盘中强势"、"竞价拉升"、"急速下跌")
|
||||
value str 异动数值(如 "5.82%"、"10.05%/64310手")
|
||||
unusual_type int 异动类型代码(见下表)
|
||||
|
||||
异动类型代码表(0x1237,2026-09-01 实测锚定,可用 easy_tdx.UNUSUAL_TYPE_NAMES 映射):
|
||||
0x03 主力买入/卖出 0x10 大单托盘
|
||||
0x04 加速拉升 0x11 大单压盘
|
||||
0x05 加速下跌 0x12 大单锁盘
|
||||
0x06 低位反弹 0x13 竞价试盘(试买/试卖,09:15~09:20 触发)
|
||||
0x07 高位回落 0x14 涨跌停(逼近/封板/封大减/打开)
|
||||
0x08 撑杆跳高 0x15 竞价/尾盘异动(拉升/下跌/平稳,09:25 与 15:00 双时刻触发)
|
||||
0x09 平台跳水 0x16 盘中强势/弱势(v1 为 1~3 级强弱等级)
|
||||
0x0A 单笔冲涨/冲跌 0x1D 急速拉升
|
||||
0x0B 区间放量涨/跌/平 0x1E 急速下跌
|
||||
0x0C 区间缩量
|
||||
|
||||
返回 DataFrame 列说明:
|
||||
index int 异动序号
|
||||
@@ -59,9 +71,10 @@ with MacClient.from_best_host() as c:
|
||||
print("暂无异动数据。")
|
||||
|
||||
# 示例输出:
|
||||
# 共获取 1843 条异动(4 页)
|
||||
# 共获取 12871 条异动(22 页)
|
||||
# index market code name time desc value unusual_type
|
||||
# 1 1 600XXX XX科技 09:45:00 5分钟涨幅>3% 3.52% 1
|
||||
# 2 1 601XXX XX银行 09:52:00 5分钟涨幅>3% 3.15% 1
|
||||
# 3 1 600XXX XX能源 10:05:00 5分钟跌幅>3% -3.28% 2
|
||||
# 1 1 600551 时代出版 09:25:00 竞价下跌 -1.21%/40254手 21
|
||||
# 2 1 600551 时代出版 09:25:00 盘中强势 5.82% 22
|
||||
# 3 1 600295 鄂尔多斯 09:25:00 竞价拉升 2.88%/533手 21
|
||||
# 4 1 605365 立达信 09:35:08 急速拉升 1.62% 29
|
||||
# ...
|
||||
|
||||
@@ -27,6 +27,7 @@ from .ex.mac_client import AsyncMacExClient, MacExClient
|
||||
from .ex.models import KNOWN_EX_HOSTS
|
||||
from .exceptions import TdxCommandError, TdxConnectionError, TdxDecodeError, TdxError
|
||||
from .mac.client import AsyncMacClient, MacClient
|
||||
from .mac.commands import UNUSUAL_TYPE_NAMES
|
||||
from .mac.enums import (
|
||||
Adjust,
|
||||
BoardSortColumn,
|
||||
@@ -90,6 +91,7 @@ __all__ = [
|
||||
"TransactionRecord",
|
||||
"XdxrRecord",
|
||||
"XDXR_CATEGORY_NAMES",
|
||||
"UNUSUAL_TYPE_NAMES",
|
||||
"FinanceInfo",
|
||||
"CompanyInfoCategory",
|
||||
"FinancialFileInfo",
|
||||
|
||||
@@ -13,7 +13,7 @@ from .symbol_quotes import SymbolQuotesCmd
|
||||
from .symbol_tick_chart import SymbolTickChartCmd
|
||||
from .symbol_transaction import SymbolTransactionCmd
|
||||
from .tick_charts import TickChartsCmd
|
||||
from .unusual import UnusualCmd
|
||||
from .unusual import UNUSUAL_TYPE_NAMES, UnusualCmd
|
||||
|
||||
__all__ = [
|
||||
"BoardListCmd",
|
||||
@@ -29,5 +29,6 @@ __all__ = [
|
||||
"SymbolTickChartCmd",
|
||||
"SymbolTransactionCmd",
|
||||
"TickChartsCmd",
|
||||
"UNUSUAL_TYPE_NAMES",
|
||||
"UnusualCmd",
|
||||
]
|
||||
|
||||
@@ -8,9 +8,34 @@ from ...codec.mac_frame import build_mac_request
|
||||
from ...commands.base import BaseCommand
|
||||
from ..models import UnusualItem
|
||||
|
||||
# 异动类型 → 粗粒度名称映射(Issue #62)。
|
||||
# 0x15/0x16/0x1D/0x1E 语义由 2026-09-01 全市场 12871 条实测锚定,详见
|
||||
# docs/protocol-unknown-fields.md「市场异动(0x1237)异动类型」一节。
|
||||
UNUSUAL_TYPE_NAMES: dict[int, str] = {
|
||||
0x03: "主力买入卖出",
|
||||
0x04: "加速拉升",
|
||||
0x05: "加速下跌",
|
||||
0x06: "低位反弹",
|
||||
0x07: "高位回落",
|
||||
0x08: "撑杆跳高",
|
||||
0x09: "平台跳水",
|
||||
0x0A: "单笔冲涨跌",
|
||||
0x0B: "区间放量",
|
||||
0x0C: "区间缩量",
|
||||
0x10: "大单托盘",
|
||||
0x11: "大单压盘",
|
||||
0x12: "大单锁盘",
|
||||
0x13: "竞价试盘",
|
||||
0x14: "涨跌停",
|
||||
0x15: "竞价/尾盘异动",
|
||||
0x16: "盘中强势弱势",
|
||||
0x1D: "急速拉升",
|
||||
0x1E: "急速下跌",
|
||||
}
|
||||
|
||||
def _describe_unusual(unusual_type: int, data: bytes) -> tuple[str, str]:
|
||||
"""根据异动类型解析描述和数值。"""
|
||||
|
||||
def _describe_unusual(unusual_type: int, data: bytes, hour: int = 9) -> tuple[str, str]:
|
||||
"""根据异动类型解析描述和数值。hour 用于区分竞价/尾盘双时刻信号(0x15)。"""
|
||||
if len(data) < 13:
|
||||
return "", ""
|
||||
v1, v2, v3, v4 = struct.unpack_from("<B2fI", data)
|
||||
@@ -56,8 +81,14 @@ def _describe_unusual(unusual_type: int, data: bytes) -> tuple[str, str]:
|
||||
desc = "大单锁盘"
|
||||
val = ""
|
||||
elif unusual_type == 0x13:
|
||||
# 竞价试盘(09:15~09:20 触发):v1=0x00 试买(申报价高于昨收)/ 0x01 试卖
|
||||
# (低于昨收);v2 为申报价,v3 为竞价量(手)。方向规律 2026-09-02
|
||||
# 全量 552 条对照昨收 549 条一致(2 条恰等于昨收的边界 + 1 条异常)。
|
||||
if v1 == 0x01:
|
||||
desc = "竞价试卖"
|
||||
else:
|
||||
desc = "竞价试买"
|
||||
val = f"{v2:.2f}/{v3:.2f}"
|
||||
val = f"{v2:.2f}/{v3:.0f}手"
|
||||
elif unusual_type == 0x14:
|
||||
direction = "涨" if v1 == 0x00 else "跌"
|
||||
if len(data) >= 10:
|
||||
@@ -75,6 +106,31 @@ def _describe_unusual(unusual_type: int, data: bytes) -> tuple[str, str]:
|
||||
else:
|
||||
desc = f"涨跌停({direction})"
|
||||
val = f"{v2_alt:.2f}/{v3_alt:.2f}"
|
||||
elif unusual_type == 0x15:
|
||||
# 竞价/尾盘异动:开盘竞价(09:25)与收盘(15:00)两个撮合时刻都会触发。
|
||||
# v1=0x02 拉升 / 0x03 下跌 / 0x01 平稳(±0.5% 分档);v2 为时段尾段价格
|
||||
# 变动(相对昨收),v3 为该时段成交量(手)。
|
||||
stage = "竞价" if hour < 12 else "尾盘"
|
||||
if v1 == 0x02:
|
||||
desc = f"{stage}拉升"
|
||||
elif v1 == 0x03:
|
||||
desc = f"{stage}下跌"
|
||||
elif v1 == 0x01:
|
||||
desc = f"{stage}平稳"
|
||||
else:
|
||||
desc = f"{stage}异动"
|
||||
val = f"{v2 * 100:.2f}%/{v3:.0f}手"
|
||||
elif unusual_type == 0x16:
|
||||
# 盘中强势/弱势:v2 = 触发时涨跌幅(09:25 样本与开盘涨幅 49/49 精确一致),
|
||||
# v1 为带符号强弱等级(0x01~0x03 强势 1~3 级,0xFD~0xFF 弱势 1~3 级)。
|
||||
desc = "盘中强势" if v2 >= 0 else "盘中弱势"
|
||||
val = f"{v2 * 100:.2f}%"
|
||||
elif unusual_type == 0x1D:
|
||||
desc = "急速拉升"
|
||||
val = f"{v2 * 100:.2f}%"
|
||||
elif unusual_type == 0x1E:
|
||||
desc = "急速下跌"
|
||||
val = f"{v2 * 100:.2f}%"
|
||||
else:
|
||||
desc = f"异动类型{unusual_type:#04x}"
|
||||
val = ""
|
||||
@@ -129,10 +185,10 @@ class UnusualCmd(BaseCommand[list[UnusualItem]]):
|
||||
"<H6sBBBHH", body, offset, f"unusual record[{i}]"
|
||||
)
|
||||
|
||||
desc, value = _describe_unusual(unusual_type, body[offset + 15 : offset + 28])
|
||||
|
||||
hour, minute_sec = unpack_from("<BH", body, offset + 29, f"unusual time[{i}]")
|
||||
|
||||
desc, value = _describe_unusual(unusual_type, body[offset + 15 : offset + 28], hour)
|
||||
|
||||
results.append(
|
||||
UnusualItem(
|
||||
index=index,
|
||||
|
||||
@@ -72,6 +72,7 @@ _EXPECTED_KIND: dict[str, str] = {
|
||||
"CALC_HOSTS": "constant",
|
||||
"MAC_HOSTS": "constant",
|
||||
"XDXR_CATEGORY_NAMES": "constant",
|
||||
"UNUSUAL_TYPE_NAMES": "constant",
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,221 @@
|
||||
"""市场异动(0x1237)异动类型解析测试(Issue #62)。
|
||||
|
||||
背景实测(2026-09-01 午间,全市场 SH+SZ 共 12871 条):
|
||||
- 0x15 仅出现在 09:25:00~09:25:02(竞价撮合时刻),v1 为方向档
|
||||
(0x02 拉升 / 0x03 下跌 / 0x01 平稳,±0.5% 分档),v2 为竞价尾段
|
||||
价格变动(相对昨收,参考时刻收敛于 09:23:30~09:24:00),v3 为竞价
|
||||
匹配量(手,略小于最终撮合量)。pytdx2 把 0x15 标作"尾盘"与实测矛盾,
|
||||
系对 PC 推送协议枚举的错误类推。
|
||||
- 0x16 全天分布,v2 为触发时涨跌幅:09:25 的 49 条样本与当日开盘涨幅
|
||||
(open/pre_close-1)49/49 精确一致;全时段与收盘涨跌幅符号一致率
|
||||
97%~100%。v1 为带符号强弱等级(0x01~0x03 强势、0xFD~0xFF 弱势,
|
||||
六组 v2 区间互不重叠且单调)。
|
||||
- 0x1D / 0x1E 全天分布,v1 恒为 0x00 / 0x01(方向),v2 恒正 / 恒负
|
||||
(阈值下限 ±0.6%),v3 恒 0。
|
||||
"""
|
||||
|
||||
import struct
|
||||
from datetime import time
|
||||
|
||||
from easy_tdx import UNUSUAL_TYPE_NAMES
|
||||
from easy_tdx.mac.commands.unusual import UnusualCmd, _describe_unusual
|
||||
|
||||
|
||||
def _record(
|
||||
utype: int,
|
||||
data_hex: str,
|
||||
hour: int = 9,
|
||||
minute_sec: int = 2500,
|
||||
market: int = 1,
|
||||
code: str = "600551",
|
||||
) -> bytes:
|
||||
"""构造一条 32 字节异动记录(<H6sBBBHH> 头 + 13B 数据区 + 保留 + <BH> 时间)。"""
|
||||
return (
|
||||
struct.pack("<H6sBBBHH", market, code.encode("gbk"), 0, utype, 0, 1, 0)
|
||||
+ bytes.fromhex(data_hex)
|
||||
+ b"\x00" # offset 28:全类型实测恒 0
|
||||
+ struct.pack("<BH", hour, minute_sec)
|
||||
)
|
||||
|
||||
|
||||
def _body(records: list[bytes]) -> bytes:
|
||||
text = ",".join("测试股" for _ in records)
|
||||
return struct.pack("<H", len(records)) + b"".join(records) + text.encode("gbk")
|
||||
|
||||
|
||||
class TestDescribeUnusualKnownTypes:
|
||||
"""既有 15 种类型(0x03~0x0C、0x10~0x14)解析不回归。"""
|
||||
|
||||
def test_type_0x04(self):
|
||||
# 真实样本:605365 立达信 2026-09-01 09:35:11
|
||||
desc, val = _describe_unusual(0x04, bytes.fromhex("00b8d73d3d0000000000000000"))
|
||||
assert desc == "加速拉升"
|
||||
assert val == "4.63%"
|
||||
|
||||
def test_unknown_type_fallback(self):
|
||||
desc, val = _describe_unusual(0x42, bytes.fromhex("00" * 13))
|
||||
assert desc == "异动类型0x42"
|
||||
assert val == ""
|
||||
|
||||
|
||||
class TestType0x15:
|
||||
"""0x15 竞价/尾盘异动(Issue #62)。
|
||||
|
||||
双时刻信号:开盘竞价 09:25(当日 1191 条)与收盘 15:00:01~04(当日 86 条,
|
||||
SH 52 / SZ 29 / BJ 5)都触发;desc 按记录小时区分「竞价/尾盘」前缀。
|
||||
"""
|
||||
|
||||
def test_auction_drop(self):
|
||||
# 真实样本:600551 时代出版 09:25:00,v1=0x03 竞价下跌
|
||||
desc, val = _describe_unusual(0x15, bytes.fromhex("030c9846bc003e1d4700000000"))
|
||||
assert desc == "竞价下跌"
|
||||
assert val == "-1.21%/40254手"
|
||||
|
||||
def test_auction_rise(self):
|
||||
# 真实样本:600127 金健米业 09:25:00,v1=0x02 竞价拉升(尾段自 10.84 冲至 12.05)
|
||||
desc, val = _describe_unusual(0x15, bytes.fromhex("0213d2cd3d00367b4700000000"))
|
||||
assert desc == "竞价拉升"
|
||||
assert val == "10.05%/64310手"
|
||||
|
||||
def test_auction_flat(self):
|
||||
# 真实样本:600410 华胜天成 09:25:01,v1=0x01 竞价平稳(尾段价格未动)
|
||||
desc, val = _describe_unusual(0x15, bytes.fromhex("01000000000098a54500000000"))
|
||||
assert desc == "竞价平稳"
|
||||
assert val == "0.00%/5299手"
|
||||
|
||||
def test_close_rise_uses_tail_prefix(self):
|
||||
# 真实样本:600123 15:00:01(收盘撮合时刻),v1=0x02 尾盘拉升
|
||||
desc, val = _describe_unusual(0x15, bytes.fromhex("027bb4dd3b0004a84500000000"), 15)
|
||||
assert desc == "尾盘拉升"
|
||||
assert val == "0.68%/5376手"
|
||||
|
||||
def test_close_drop_uses_tail_prefix(self):
|
||||
# 真实样本:600221 15:00:01,v1=0x03 尾盘下跌
|
||||
desc, val = _describe_unusual(0x15, bytes.fromhex("03c10ffcbb839c274800000000"), 15)
|
||||
assert desc == "尾盘下跌"
|
||||
assert val == "-0.77%/171634手"
|
||||
|
||||
def test_unknown_sub_type_falls_back(self):
|
||||
desc, _ = _describe_unusual(0x15, struct.pack("<B2fI", 0x77, 0.0, 100.0, 0))
|
||||
assert desc == "竞价异动"
|
||||
desc, _ = _describe_unusual(0x15, struct.pack("<B2fI", 0x77, 0.0, 100.0, 0), 15)
|
||||
assert desc == "尾盘异动"
|
||||
|
||||
|
||||
class TestType0x16:
|
||||
"""0x16 盘中强势/弱势(Issue #62 主体)。"""
|
||||
|
||||
def test_strong_at_auction(self):
|
||||
# 真实样本:600551 时代出版 09:25:00,v2=+5.82% 与当日开盘涨幅精确一致
|
||||
desc, val = _describe_unusual(0x16, bytes.fromhex("010f506e3dcb846e3d00000000"))
|
||||
assert desc == "盘中强势"
|
||||
assert val == "5.82%"
|
||||
|
||||
def test_weak_at_auction(self):
|
||||
# 真实样本:600683 京投发展 09:25:01,v1=0xFF(弱势 1 级),v2=-6.40%
|
||||
desc, val = _describe_unusual(0x16, bytes.fromhex("ffc71d83bd690383bd00000000"))
|
||||
assert desc == "盘中弱势"
|
||||
assert val == "-6.40%"
|
||||
|
||||
def test_new_stock_no_limit(self):
|
||||
# 真实样本:601123 N马矿 09:25:00,新股无涨跌幅限制,v2=+245.86%
|
||||
desc, val = _describe_unusual(0x16, bytes.fromhex("03775a1d404a5b1d4000000000"))
|
||||
assert desc == "盘中强势"
|
||||
assert val == "245.86%"
|
||||
|
||||
|
||||
class TestType0x13:
|
||||
"""0x13 竞价试盘(2026-09-02 破译)。
|
||||
|
||||
v1 为方向:0x00 试买(申报价高于昨收)/ 0x01 试卖(低于昨收)——552 条对照
|
||||
昨收 549 条一致;v2 为申报价、v3 为竞价量(手)。旧实现一律显示「竞价试买」,
|
||||
方向相反的一半记录描述错误。
|
||||
"""
|
||||
|
||||
def test_auction_test_buy(self):
|
||||
# 真实样本:603980 09:15:14,申报价 8.71 高于昨收 7.92(往上试)
|
||||
desc, val = _describe_unusual(0x13, bytes.fromhex("00295c0b41006c354600000000"))
|
||||
assert desc == "竞价试买"
|
||||
assert val == "8.71/11611手"
|
||||
|
||||
def test_auction_test_sell(self):
|
||||
# 真实样本:603900 09:15:17,申报价 6.46 低于昨收 7.17(往下试)
|
||||
desc, val = _describe_unusual(0x13, bytes.fromhex("0152b8ce400000c94300000000"))
|
||||
assert desc == "竞价试卖"
|
||||
assert val == "6.46/402手"
|
||||
|
||||
|
||||
class TestType0x1D0x1E:
|
||||
"""0x1D 急速拉升 / 0x1E 急速下跌(Issue #62 顺带补齐)。"""
|
||||
|
||||
def test_fast_rise(self):
|
||||
# 真实样本:605365 立达信 09:35:08
|
||||
desc, val = _describe_unusual(0x1D, bytes.fromhex("009d50843c0000000000000000"))
|
||||
assert desc == "急速拉升"
|
||||
assert val == "1.62%"
|
||||
|
||||
def test_fast_fall(self):
|
||||
# 真实样本:601123 N马矿 09:35:03
|
||||
desc, val = _describe_unusual(0x1E, bytes.fromhex("019cd393bc0000000000000000"))
|
||||
assert desc == "急速下跌"
|
||||
assert val == "-1.80%"
|
||||
|
||||
|
||||
class TestUnusualCmdParseResponse:
|
||||
def test_parse_new_types_end_to_end(self):
|
||||
body = _body(
|
||||
[
|
||||
_record(0x16, "010f506e3dcb846e3d00000000", 9, 2500),
|
||||
_record(0x15, "030c9846bc003e1d4700000000", 9, 2500),
|
||||
_record(0x1D, "009d50843c0000000000000000", 9, 3508),
|
||||
_record(0x1E, "019cd393bc0000000000000000", 9, 3503),
|
||||
]
|
||||
)
|
||||
items = UnusualCmd(1, 0, 600).parse_response(body)
|
||||
assert len(items) == 4
|
||||
assert [i.desc for i in items] == ["盘中强势", "竞价下跌", "急速拉升", "急速下跌"]
|
||||
assert items[0].value == "5.82%"
|
||||
assert items[1].value == "-1.21%/40254手"
|
||||
assert items[0].time == time(9, 25, 0)
|
||||
assert items[2].time == time(9, 35, 8)
|
||||
assert items[0].unusual_type == 0x16
|
||||
assert all(i.name == "测试股" for i in items)
|
||||
|
||||
def test_parse_close_record_names_tail_prefix(self):
|
||||
"""15:00 的 0x15 记录端到端应得到「尾盘拉升」(真实收盘样本 600123)。"""
|
||||
rec = _record(0x15, "027bb4dd3b0004a84500000000", 15, 1)
|
||||
items = UnusualCmd(1, 0, 600).parse_response(_body([rec]))
|
||||
assert items[0].desc == "尾盘拉升"
|
||||
assert items[0].value == "0.68%/5376手"
|
||||
assert items[0].time == time(15, 0, 1)
|
||||
|
||||
def test_record_layout_unchanged(self):
|
||||
"""记录仍为 32 字节定长,时间槽位于 offset 29。"""
|
||||
rec = _record(0x16, "010f506e3dcb846e3d00000000", 14, 5701)
|
||||
assert len(rec) == 32
|
||||
items = UnusualCmd(1, 0, 600).parse_response(_body([rec]))
|
||||
assert items[0].time == time(14, 57, 1)
|
||||
|
||||
|
||||
class TestTypeNames:
|
||||
def test_names_cover_all_described_types(self):
|
||||
"""映射表应覆盖 _describe_unusual 的全部分支(0x03~0x0C、0x10~0x16、0x1D、0x1E)。"""
|
||||
assert set(UNUSUAL_TYPE_NAMES) == {
|
||||
*range(0x03, 0x0D),
|
||||
*range(0x10, 0x17),
|
||||
0x1D,
|
||||
0x1E,
|
||||
}
|
||||
|
||||
def test_mapped_types_produce_named_desc(self):
|
||||
"""映射表中的类型不应落入"异动类型0x??"兜底分支。"""
|
||||
zeros = bytes.fromhex("00" * 13)
|
||||
for utype in UNUSUAL_TYPE_NAMES:
|
||||
desc, _ = _describe_unusual(utype, zeros)
|
||||
assert not desc.startswith("异动类型"), f"0x{utype:02X} 未实现解析分支"
|
||||
|
||||
def test_top_level_export(self):
|
||||
import easy_tdx
|
||||
|
||||
assert easy_tdx.UNUSUAL_TYPE_NAMES is UNUSUAL_TYPE_NAMES
|
||||
assert UNUSUAL_TYPE_NAMES[0x16] == "盘中强势弱势"
|
||||
Reference in New Issue
Block a user