Files
easy_tdx_max/docs/protocol-unknown-fields.md
Justin Gu 4dde339f26 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)
2026-09-02 10:44:09 +08:00

22 KiB
Raw Permalink Blame History

TDX 协议未知字段逆向分析

本文档追踪 TDX 协议中尚未完全暴露的结构,作为持续逆向的参考。

一、实时行情 unknown 字段(security_quotes

commands/security_quotes.py,每只股票一条变长记录。

字段 编码 状态 含义
unknown_0 get_price 已确认 服务器时间戳(小时+百万分之一小时),_format_server_time() 解码
unknown_1 get_price 已确认 = -price_raw(差分编码负基准值)
unknown_2 get_price 已确认 指数/板块:集合竞价成交金额(decoded * 100 = 元);个股= b_vol + s_vol - vol(舍入残差,≈0
unknown_3 get_price 已确认 个股:集合竞价成交金额(decoded * 100 = 元);指数/板块:无意义(负值)
unknown_4 H (uint16) 已确认 交易状态标志:停牌=32800(0x8020);非停牌值结构已知(见下文)
unknown_5-8 get_price 已确认 恒为 0,保留字段

unknown_2 详细分析(已确认:指数集合竞价成交金额 / 个股舍入残差)

双重含义,取决于品种类型:

  1. 指数/板块IndexOpenAmount — 集合竞价成交金额。decoded * 100 = 元。

    • 上证指数(000001.SH)u2=125,967,040 → 开盘金额 ≈ 1,260 亿元
    • 深证成指(399001.SZ)u2=157,987,536 → 开盘金额 ≈ 1,580 亿元
  2. 个股= b_vol + s_vol - vol(舍入残差)。

    • 验证方法:对 20 只股票逐一计算 s_vol + b_vol - u2,结果与 vol 精确匹配(20/20
    • 涨停时恒为 0(买量=总量,卖量=0);停牌时恒为 0(vol=0);正常股票范围 -25~55

来源:Go 实现 gotdx (quant1x/gotdx) 命名为 IndexOpenAmount,注释"指数有效-集合竞价成交金额=开盘成交金额"。

unknown_3 详细分析(已确认:个股集合竞价成交金额)

unknown_3 = StockOpenAmount — 个股集合竞价(9:15-9:25)成交金额。

公式:开盘成交金额(元) = decoded_u3 * 100 推导:开盘成交量(手) = u3 * 100 / (open_price * 100) = u3 / open_price

验证数据:

股票 u3 开盘金额(万元) 开盘量(手) 占总金额%
平安银行(000001) 73,598 736 6,840 0.81%
京东方A(000725) 4,825,923 48,259 832,056 1.66%
贵州茅台(600519) 754,092 7,541 595 0.71%
比亚迪(002594) 294,144 2,941 3,064 0.85%

合理性:开盘占比 0.22%~1.66%,符合 A 股集合竞价的典型比例。当日固定值(不随盘中变化)。

对指数/板块:u3 为负值,无实际意义。指数的开盘金额由 unknown_2 承载。

排除历史假说

  • 非换手率(R²=0.507
  • 非简单成交额衍生(amount/u3 比值变化 9 倍)
  • 与 amount 高度相关(r=+0.95)因为活跃股的开盘金额与总金额正相关

来源:Go 实现 gotdx (quant1x/gotdx) 命名为 StockOpenAmount,注释"个股有效-集合竞价成交金额=开盘成交金额"。

unknown_4 非停牌值分析

已确认的结构特征

条件 u4 值 说明
停牌 32800 (0x8020) bit15=1,确认为停牌标志
指数 2 (0x0002) 所有指数恒为 2
个股 598~6166 日内固定值(不随交易变化)

字节结构(个股非停牌值):

u4 = hi_byte * 256 + lo_byte
hi_byte: 0~24, 23个唯一值,是 u4 的主变量(r=0.9992
lo_byte: 仅 5 个可能值 → {2, 22, 86, 150, 214}
         低6位恒为 0x16=22, bit6-7 编码 0~3 四个级别

lo_byte 详解:

lo (hex) 十进制 bit7-6 含义推测
0x02 2 - 指数专用
0x16 22 00 个股级别0
0x56 86 01 个股级别1
0x96 150 10 个股级别2
0xD6 214 11 个股级别3

与行情指标的相关性:与所有 MAC 标准字段弱相关(|r|<0.43),最高为 active2 (r=0.42)。 hi_byte 与 active2(成交笔数) 中等相关,但不足以确认映射。

推测:通达信内部计算的日级分类/热度指标。lo_byte 编码股票分类属性,hi_byte 编码具体指标值。 所有已知 TDX 实现(gotdx、pytdx、injoyai/tdx)均标记为"保留/未知",无权威解释。

附:MAC 协议 build_bitmap() Bug

codec/bitmap.pybuild_bitmap() 无法处理 bit 值 >= 128 的字段(如 BID_ASK_DIFF=0x8C), 会引发 OverflowError。这些字段应放入 4 字节控制区(位 128-159),但当前实现未处理这种分离。

二、证券列表 unknown 字段(security_list

commands/security_list.py,每条 29 字节记录:

offset  0: 6s  code
offset  6: H   volunit
offset  8: 8s  name (GBK)
offset 16: I   _unknown1  ← 已确认:排序/分组字段
offset 20: B   decimal_point
offset 21: I   pre_close_raw (实际是 IEEE float32,非通达信自定义浮点)
offset 25: I   _unknown2  ← 已确认:私有时间戳/更新日期

_unknown1(已确认:排序/分组字段)

拆分为两个 uint16 LEu1_lo(offset 16) + u1_hi(offset 18)。

  • u1_hi = 分组 ID,同组内的板块/指数共享相同的 u1_hi 值
  • u1_lo = 组内排序位置,与列表顺序单调递增

按品种统计:

  • SZ 主板000xxx):u1_hi ≈ 0u1_lo 随列表顺序单调递增(630/630 非递减)
  • 板块880xxx):u1_hi 有 27 个不同值(0~154),分组为约 9 个一簇
  • SH 指数000xxx):u1_hi 范围 0~75,类似分组模式
  • 创业板300xxx):u1_hi ≈ 0u1_lo 随列表顺序递增

不是浮点数,不是价格,不是简单计数器——是通达信内部的排序/分组标识。

_unknown2(已确认:私有时间戳)

大部分为 0,非零情况:

  • SZ 主板:631/631 全部非零
  • SH 指数:20/200 非零
  • 板块:20/797 非零
  • 创业板:0/369

字节级分析(offset 25-28 = b25, b26, b27, b28):

  • b28 编码年份:值 2237 对应 20102025(公式:year = b28 + 1988
  • b26 在 SZ 主板中恒为 0x31(49),在 SH 指数中为 0x29~0x31
  • b27 范围 5~254,不是简单的月份或日序
  • 不是 IEEE float32,不是通达信自定义浮点,不是标准日期格式

推测:记录了某种更新/修改日期,但具体编码方式仍有部分未解。

重大发现:pre_close 实际是 IEEE float32

offset 21 的 pre_close_raw 一直被当作通达信自定义浮点编码(_decode_volume)。 实际上它是 标准 IEEE float32。但 _decode_volume() 对价格范围(0.01~99999.99)的 IEEE float32 位模式产生数学上相同的结果——纯属巧合,当前代码正确工作但原因理解有误。

pytdx 同样有此问题(也用 get_volume 解码此字段)。

三、其他待破解字段

3.1 响应帧头 16 字节(已确认)

codec/frame.py:前 12 字节通过 gotdx 交叉验证已完全破解。

字节级结构gotdx StdResponseHeader):

偏移 大小 名称 含义
0-3 uint32 magic 协议魔数,恒为 7654321 (0x0074CBB1)
4 uint8 ZipFlag bit4=1 表示 body 已压缩 (0x1C=压缩, 0x0C=未压缩)
5-8 uint32 SeqID 请求 bytes 1-4 的回显(命令标识匹配)
9 uint8 保留 观察到恒为 0x00
10-11 uint16 Method 请求 bytes 10-11 的回显
12-13 uint16 ZipSize body 压缩后字节数
14-15 uint16 UnZipSize body 解压后字节数

验证数据3 次连接,setup + data 命令):

  • magic 恒为 76543219/9 响应验证)
  • ZipFlag bit4 与 zipsize≠unzipsize 完全一致(4/4 响应验证)
  • SeqID 与请求 bytes 1-4 精确匹配(4/4 响应验证)
  • Method 与请求 bytes 10-11 精确匹配(4/4 响应验证)

兼容说明:代码使用 struct "<IIIHH" 解码,正确提取 ZipSize 和 UnZipSize。FrameHeader 字段已更名为 magic/seq_id/method

来源:gotdx (quant1x/gotdx) quotes/base_message.go StdResponseHeader 结构体。

3.2 分时数据 unknown_1 字段

commands/minute_time.py:57unknown_1pytdx 称 reversed1)。

状态:未确定。所有已知实现(pytdx、gotdx、injoyai/tdx、xmtdx、easy-tdx)均丢弃此字段。

排除的假设(历史分时数据 600000.SH 20250108 验证):

假设 相关系数 r 说明
VWAP (cum_amt/cum_vol) 0.006 完全不匹配
累计成交额 -0.029 完全不匹配
每分钟成交额 -0.200 弱相关
delta_vwap 0.361 最高但仍弱
成交量 vol -0.202 弱相关

多股票交叉验证(历史分时 20250108):

股票 价格范围 u1 范围 u1 非零占比
000001 平安银行 11.40~11.62 -27~138 201/240
000725 京东方A 4.24~4.36 -56~173 165/240
600519 贵州茅台 1426.80~1451.70 -21105~39611 239/240
600000 浦发银行 10.22~10.36 -238~65 185/240

u1 范围与价格非线性质相关(茅台价格 140x → u1 范围 300x),不与任何标准指标匹配。

推测:服务器内部计算的指标(可能与成交笔数、买卖力道、或内部排序有关),所有公开的逆向实现均无法确认其含义。

3.3 逐笔成交尾部字段

commands/transaction.py:73,97unknown_lastpytdx 原来直接丢弃。

3.4 市场异动(0x1237)异动类型与数据区(已确认,Issue #62)

mac/commands/unusual.py:每条 32 字节记录 = <H6sBBBHH> 头(15B)+ 13B 类型数据区 (<B2fIv1, 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:0009:25:02;收盘:86 条全部落在 15:00:0115:00:04SH 52 / SZ 29 / BJ 5),v1 分档规律与早间一致、幅度更小(±0.5%±2.6%)。v2 参考时刻扫描收敛于 09:23:3009:24:0060 样本 MAE 0.11pp,受竞价 3 秒采样粒度限制);v3 略小于最终撮合量(如 600551 v3=40254 vs 终值 44865)。desc 按记录小时区分「竞价/尾盘」前缀
0x16 盘中强势/弱势 v2=触发时涨跌幅;v1=带符号强弱等级(0x010x03 强势 13 级,0xFD0xFF 弱势 13 级,补码);v3≈v2(差值 mean 0.83pp,语义未定) 09:25 的 49 条样本 v2 与当日开盘涨幅(open/pre_close-149/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 种(0x030x0B、0x10、0x11、 0x130x16、0x1D、0x1E),午间、下午、收盘竞价各窗口均无新类型码出现;2026-09-02 盘中(10:27)复查亦为同一 17 种。已知但两个交易日零触发的 2 种:0x0C 区间缩量、 0x12 大单锁盘(语义仍承自 pytdx2 一系,待采样验证)。PC 推送协议特有的「大笔买入/卖出、 主力急入/急出、急速上涨/猛烈下跌、强势封涨停/跌停、涨幅超 10% 整数倍」等信号 PigQuant 逆向的 0x40080cd1+ 消息 ID 体系)在 0x1237 拉取协议中无对应类型码, 两套协议的信号空间不同构:PC 端「逼近/封/打开涨跌停」对应 0x14 子类型、「买一/卖一 剩余大」对应 0x10/0x11、「急速拉升/猛烈打压」对应 0x1D/0x1E。

排除项

  • pytdx2QuantJia/pytdx2 parser/stock.py)把 0x15 标作"尾盘(对倒/拉升)"——只对了一半: 0x15 在开盘竞价(09:25)与收盘(15:00)双时刻触发,他们大概只观测过下午的记录; 其 0x16"盘中弱势/强势"方向语义(v2 符号)与实测一致。
  • 0x000x02、0x0D、0x0E、0x0F、0x170x1C、0x1F+ 全天零出现(未实现或极罕见)。
  • 0x16 的 v1 等级公式未完全破解:等级与 v2 非纯函数关系(L2/L3 区间在 +7.1%~+9.0% 重叠), 疑与涨停幅度(ST/创业/科创)相关,待后续验证。

请求监控参数<HH2xH2xH5H 尾部 6×H,默认 1,200,30,40,50,200)单维扫描 (每位置 15 个候选值):位置 14 完全惰性;位置 0 仅低字节有效(≥0x400 返回空); 位置 5 在 1127 区间致命(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字节响应头部(已确认:请求回显)

位置commands/xdxr_info.py:33

字节结构fixture tests/fixtures/xdxr_info.hex 验证):

偏移 大小 名称 含义
0-1 uint16 prefix 恒为 1(可能是响应类型标识)
2 uint8 market 请求的 market 回显(1=SH, 0=SZ
3-8 6s code 请求的股票代码回显(如 "600000"

验证数据fixture 600000.SH → 01 00 01 36 30 30 30 30 30prefix=1, market=1(SH), code="600000"。

gotdx 同样命名为 Unknownpytdx 直接 pos += 9

4.2 xdxr_info 每条记录1字节 padding(已确认:保留/对齐字节)

位置commands/xdxr_info.py:46

fixture 中 87 条记录全部为 0x00(87/87)。无其他可能值样本,推测为保留/对齐字节。 gotdx 命名为 Unknownpytdx 注释 # noused

4.3 fund_flowCategory 22)——已证伪并移除(Issue #52)

位置commands/fund_flow.py(已删除)

曾有实现假设 0x052D + category=22 为"历史资金流向"指令,并按 "9 字节头 + uint16 num + 36 字节/条"解析。2026-08-26 实测Issue #52): 全部 46 台可达标准行情服务器对该请求仅回 2 字节 body(00002003), 即 0 条记录 / ret_count 撒谎空包,从未在任何环境返回过有效数据。 响应格式属臆造,该命令已删除;资金流改为"日K取日期 + 逐笔成交重算"。

gotdx 未实现此命令。

4.4 板块文件 384 字节头部(未确认)

位置codec/block.py:23

gotdx 命名为 Unknown [384]byte,注释"头信息, 忽略"。所有已知实现均直接跳过。 二进制文件头部,不随网络响应变化,逆向优先级低。

4.5 板块记录 2813 字节(已确认:前13字节有效 + 2800字节股票列表)

位置codec/block.py:39-67

结构:name(9s) + stock_count(H) + block_type(H) + stock_list(400×7s)。 13 字节元数据 + 2800 字节股票代码列表 = 2813 字节。gotdx 结构完全一致。

4.6 扩展行情 skip 字节(未确认)

位置 跳过字节数 说明
ex/get_instrument_quote.py:26 4 字节 market+code 后的 4 字节 unknown
ex/get_instrument_quote_list.py:102 290-140=150 字节 期货/港股记录的剩余字段

扩展行情(端口 7727)协议字段较少公开实现参考,跳过字段含义暂不明确。

五、财务数据 unknown 字段

codec/financial.py.dat 头部 20 字节中有 10 字节未知。 每只股票的 float32 数组缺少完整的字段名映射。

六、完全未实现的命令

  • Level-2 行情(十档买卖、逐笔委托)
  • 集合竞价数据
  • 融资融券明细
  • 大宗交易
  • 龙虎榜原始数据
  • 期权/Greeks 数据
  • 新三板/北交所完整支持

七、逆向进展日志

7.1 实时行情 unknown 字段

2026-05-27 第一轮:基础采样

  • unknown_1 = -price_raw10 只股票验证)
  • unknown_5-8 恒为 0

2026-05-27 第二轮:全市场特殊状态扫描

  • unknown_4 = 交易状态标志(停牌=32800)
  • unknown_2 涨停/停牌时恒为 0
  • unknown_3 与成交量正相关

2026-05-27 第三轮:三方向并行突破

子任务 AMAC 协议对照):

  • unknown_3 与 amount 相关系数 r=+0.974
  • unknown_2/unknown_4 与所有 MAC 标准字段弱相关(|r|<0.36
  • 发现 build_bitmap() 对 bit>=128 字段的 bug

子任务 B(流通股本交叉验证):

  • unknown_2 = b_vol + s_vol - vol20/20 精确匹配)
  • unknown_3 不是换手率(R²=0.507
  • unknown_3 与成交额 R²=0.89,但不成固定比例

2026-05-27 第四轮:穷举+交叉实现突破

穷举候选分析(95 只股票):

  • 最高相关候选:cur_vol*price_rawr=0.91),amountr=0.89
  • 非线性变换排除幂次关系(线性最优 r=0.95)

跨实现发现(gotdx/quant1x Go TDX 库):

  • unknown_2 = IndexOpenAmount(指数集合竞价成交金额),个股时承载舍入残差
  • unknown_3 = StockOpenAmount(个股集合竞价成交金额),公式 decoded * 100 = 元
  • 验证:平安银行开盘 736 万元/6,840 手(占总量 0.81%),贵州茅台开盘 7,541 万元/595 手(0.71%
  • 指数开盘金额:上证 1,260 亿、深证 1,580 亿,均合理

2026-05-27 第四轮(续):unknown_4 非停牌值结构分析

跨实现调查(gotdx、pytdx、injoyai/tdx):

  • 所有实现均命名为 ReversedBytes4,注释"保留/未知"
  • gotdx 用 Open==0 判断停牌,不依赖 u4

结构发现(96 只活跃股票):

  • u4 = hi*256 + lohi=主变量(r=0.9992),范围 0~24
  • lo 仅 5 值:{2(指数), 22(0x16), 86(0x56), 150(0x96), 214(0xD6)},低6位恒 0x16
  • u4 在日内固定不变
  • 与所有标准指标弱相关(最高 active2 r=0.42
  • 结论:通达信内部日级分类指标,具体语义不明

7.2 证券列表 unknown 字段

2026-05-27 第一轮:SH 市场基础统计

  • unknown1 99.9% 非零,自定义浮点解码为 0
  • unknown2 96% 为 0

2026-05-27 第三轮:全品种深度分析

子任务 C(全品种分类分析):

  • unknown1 = 排序/分组字段u1_hi=分组ID, u1_lo=组内排序位置
  • unknown2 = 私有时间戳b28 编码年份(year-1988),完整编码仍有部分未解
  • 重大发现:pre_close_raw 是 IEEE float32_decode_volume 恰好产生相同结果)

7.3 响应帧头 16 字节

2026-05-27gotdx 交叉验证完全破解

gotdx StdResponseHeader 字节级结构对照:

  • magic(4B)=7654321, ZipFlag(1B)=bit4压缩标志, SeqID(4B)=请求回显
  • 保留(1B)=0x00, Method(2B)=请求回显, ZipSize(2B), UnZipSize(2B)
  • 9/9 响应验证通过(magic恒定、SeqID/Method精确匹配请求)

代码更新:codec/frame.py 字段重命名为 magic/seq_id/method,docstring 更新完整字节结构。

7.4 分时数据 unknown_1

2026-05-27:穷举分析后确认为"未确定"

假设穷举(多股票交叉验证):

  • VWAP: r=0.006, 累计成交额: r=-0.029, delta_vwap: r=0.361
  • u1 范围与价格非线性质相关(茅台价格140x → u1范围300x
  • 所有已知实现(pytdx、gotdx、injoyai/tdx、xmtdx)均丢弃此字段
  • xmtdx 确认为 easy-tdx fork,注释"疑似均价"但无验证
  • 结论:服务器内部指标,所有公开逆向实现均无法确认含义

7.5 跳过字节块

2026-05-27xdxr_info / fund_flow / block 分析

xdxr_info 9字节头部(fixture 验证):

  • 01 00 01 36 30 30 30 30 30 = prefix(2B=1) + market(1B=SH) + code(6B="600000")
  • 响应回显请求的 market+codeprefix 可能是类型标识

xdxr_info 每条1字节 padding

  • 87 条记录全部为 0x00,保留/对齐字节

fund_flow 9字节头部:同 xdxr_info 格式 2026-08-26 证伪(见 4.3,命令已删除)

block 384字节头部:gotdx 命名"头信息, 忽略",所有实现跳过

已知 Bug 清单

  1. codec/bitmap.py build_bitmap() — bit>=128 字段溢出(不影响现有功能)
  2. commands/security_list.py — pre_close 用 _decode_volume 而非 IEEE float32(结果正确但语义有误)