diff --git a/README.md b/README.md index a625193..a2bfd07 100644 --- a/README.md +++ b/README.md @@ -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 diff --git a/docs/protocol-unknown-fields.md b/docs/protocol-unknown-fields.md index 61a60f7..884d40e 100644 --- a/docs/protocol-unknown-fields.md +++ b/docs/protocol-unknown-fields.md @@ -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 字节记录 = `` 头(15B)+ 13B 类型数据区 +(`` 时间。 + +**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/创业/科创)相关,待后续验证。 + +**请求监控参数**(`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 # ... diff --git a/src/easy_tdx/__init__.py b/src/easy_tdx/__init__.py index ed10cef..5e39e6e 100644 --- a/src/easy_tdx/__init__.py +++ b/src/easy_tdx/__init__.py @@ -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", diff --git a/src/easy_tdx/mac/commands/__init__.py b/src/easy_tdx/mac/commands/__init__.py index b067bce..994820c 100644 --- a/src/easy_tdx/mac/commands/__init__.py +++ b/src/easy_tdx/mac/commands/__init__.py @@ -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", ] diff --git a/src/easy_tdx/mac/commands/unusual.py b/src/easy_tdx/mac/commands/unusual.py index 56e4d2e..572648b 100644 --- a/src/easy_tdx/mac/commands/unusual.py +++ b/src/easy_tdx/mac/commands/unusual.py @@ -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(" tuple[str, str]: desc = "大单锁盘" val = "" elif unusual_type == 0x13: - desc = "竞价试买" - val = f"{v2:.2f}/{v3:.2f}" + # 竞价试盘(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:.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]]): " bytes: + """构造一条 32 字节异动记录( 头 + 13B 数据区 + 保留 + 时间)。""" + return ( + struct.pack(" bytes: + text = ",".join("测试股" for _ in records) + return struct.pack("