mirror of
https://ghfast.top/https://github.com/aeroxw/easy-tdx.git
synced 2026-09-12 13:24:15 +08:00
fix(backtest): resolve mypy and ruff lint issues
- dsl.py: use NDArray type annotations, fix None narrowing - cli.py: add type annotations, fix import sorting - strategy.py: fix UP038 isinstance, add noqa for I() method name - tests: fix E712 bool comparison assertions Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.8
parent
fc0777533e
commit
04c2be1d7f
@@ -72,7 +72,11 @@
|
||||
"Bash(gh auth *)",
|
||||
"Bash(rm -rf easy_tdx.wiki)",
|
||||
"Bash(gh repo *)",
|
||||
"Bash(gh api *)"
|
||||
"Bash(gh api *)",
|
||||
"Bash(mypy src/easy_tdx/backtest/types.py)",
|
||||
"Bash(mypy tests/unit/test_backtest_types.py)",
|
||||
"Bash(mypy src/easy_tdx/backtest/types.py tests/unit/test_backtest_types.py)",
|
||||
"Bash(mypy *)"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
[ 12502ms] [ERROR] Failed to load resource: the server responded with a status of 503 () @ https://collector.github.com/github/collect:0
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,144 @@
|
||||
# TDX 协议逆向工程全景
|
||||
|
||||
本文记录通达信 (TDX) 私有二进制 TCP 协议的逆向研究过程,帮助理解 easy-tdx 项目中各层协议实现的设计原理。
|
||||
|
||||
## 第一层:抓包 — 发现帧结构
|
||||
|
||||
通达信客户端和服务器之间是裸 TCP(端口 `7709`),没有 HTTP 包装。研究者通过 Wireshark 抓包发现了一个固定的帧格式:
|
||||
|
||||
```
|
||||
响应帧 = 16字节固定头 + 可变长度 body
|
||||
```
|
||||
|
||||
`codec/frame.py` 记录了这个发现:
|
||||
|
||||
```
|
||||
偏移 0: I (4字节) — 未知
|
||||
偏移 4: I (4字节) — 未知
|
||||
偏移 8: I (4字节) — 未知
|
||||
偏移 12: H (2字节) — zipsize(body 实际长度)
|
||||
偏移 14: H (2字节) — unzipsize(解压后长度)
|
||||
```
|
||||
|
||||
关键发现:**当 zipsize == unzipsize 时 body 未压缩,否则需要 zlib 解压**。这是通过对比抓包数据和 zlib 解压结果验证的。
|
||||
|
||||
## 第二层:握手 — 三条神秘命令
|
||||
|
||||
`commands/setup.py` 中的三条握手命令是硬编码的原始字节:
|
||||
|
||||
```python
|
||||
SETUP_CMD1: "0c0218930001030003000d0001"
|
||||
SETUP_CMD2: "0c0218940001030003000d0002"
|
||||
SETUP_CMD3: "0c031899000120002000db0f..." # 30字节,含GBK编码的"申银万国"
|
||||
```
|
||||
|
||||
注释写得很清楚:"从 pytdx 源码移植,已在真实服务器验证"。这说明握手命令不是这个项目原创的,而是从开源项目 [pytdx](https://github.com/rainx/pytdx) 继承来的。pytdx 的作者 rainx 通过**逆向通达信 Windows 客户端**(用 IDA Pro 等反汇编工具)得到了这些字节序列。
|
||||
|
||||
第三条命令特别有意思,里面包含了 `d5d0c9ccd6a4a8af`,这是 GBK 编码的"申银万国"(一家券商名),说明这条握手命令里包含了**客户端标识信息**。
|
||||
|
||||
## 第三层:请求构造 — 猜解命令格式
|
||||
|
||||
每个命令的请求都是一个固定格式的二进制包。以 `security_list.py` 为例:
|
||||
|
||||
```python
|
||||
header = bytes.fromhex("0c0118640101060006005004")
|
||||
payload = struct.pack("<HHH", market, start, 0)
|
||||
```
|
||||
|
||||
这些 magic number(如 `0c0118640101060006005004`)是怎么来的?有两种途径:
|
||||
|
||||
1. **反汇编客户端**:在通达信的 exe/dll 中找到构建这些请求包的函数
|
||||
2. **协议模糊测试**:修改已知命令的某些字节,观察服务器响应变化
|
||||
|
||||
比如 K 线命令 `security_bars.py` 的头部:
|
||||
|
||||
```python
|
||||
struct.pack("<HIHHHH6sHHHHIIH",
|
||||
0x010C, # 固定魔数
|
||||
0x01016408, # 命令 ID(K线数据的标识)
|
||||
0x001C, # payload 长度 = 28
|
||||
0x001C, # 重复的 payload 长度
|
||||
0x052D, # 子命令/协议版本
|
||||
market, # 市场
|
||||
code, # 股票代码(6字节)
|
||||
category, # K线周期
|
||||
1, # 未知,可能是数据类型
|
||||
start, # 起始位置
|
||||
count, # 请求数量
|
||||
0, 0, 0, # 三个保留字段
|
||||
)
|
||||
```
|
||||
|
||||
通过逐个修改这些字段发送给服务器,观察返回数据的变化,就能推断每个字段的含义。
|
||||
|
||||
## 第四层:响应解析 — 最难的部分
|
||||
|
||||
响应的解析才是真正的逆向难点。以实时行情 (`security_quotes.py`) 为例:
|
||||
|
||||
**差分编码**:服务器返回的不是绝对价格,而是差值(delta)。所有价格字段都是相对于 `price_raw` 的差分:
|
||||
|
||||
```python
|
||||
pre_close = (price_raw + last_close_diff) / 100.0
|
||||
open = (price_raw + open_diff) / 100.0
|
||||
high = (price_raw + high_diff) / 100.0
|
||||
# 五档买卖也是差分:
|
||||
bid1 = (price_raw + bid1_d) / 100.0
|
||||
```
|
||||
|
||||
K 线数据更复杂,使用了累积差分 (`security_bars.py`):
|
||||
|
||||
```python
|
||||
open_abs = open_diff + pre_diff_base
|
||||
close_abs = open_abs + close_diff
|
||||
high_abs = open_abs + high_diff
|
||||
low_abs = open_abs + low_diff
|
||||
pre_diff_base = open_abs + close_diff # 下一根K线的基准
|
||||
```
|
||||
|
||||
## 第五层:编解码 — 自定义数据格式
|
||||
|
||||
### 价格编码 (`codec/price.py`) — 类 LEB128 变长有符号整数
|
||||
|
||||
```
|
||||
首字节: [bit7=继续][bit6=符号][bit5-0=低6位数据]
|
||||
后续字节: [bit7=继续][bit6-0=7位数据]
|
||||
```
|
||||
|
||||
这不是标准 LEB128,而是通达信自己的变体。推断过程:
|
||||
|
||||
1. 抓包看到价格附近的数据长度不固定(1-4字节)
|
||||
2. 注意到 bit7 似乎是"还有更多字节"的标记
|
||||
3. 发现 bit6 是符号位
|
||||
4. 通过已知价格验证:发送请求时知道某个价格是 15.50,在响应中找到对应的字节序列,反推编码规则
|
||||
|
||||
### 成交量编码 (`codec/volume.py`) — 完全自定义的 4 字节浮点
|
||||
|
||||
```python
|
||||
logpoint = (ivol >> 24) & 0xFF # 指数
|
||||
hleax = (ivol >> 16) & 0xFF # 高精度
|
||||
lheax = (ivol >> 8) & 0xFF # 中精度
|
||||
lleax = ivol & 0xFF # 低精度
|
||||
```
|
||||
|
||||
这个格式完全不是 IEEE 754,是通达信自创的。逆向方法:拿到已知成交量的 hex dump,用不同的拆分方式穷举,直到找到能还原正确数值的公式。
|
||||
|
||||
## 第六层:未知字段 — 逆向的痕迹
|
||||
|
||||
代码中大量保留了 `unknown_0` 到 `unknown_8` 的字段,这本身就是逆向过程的痕迹——解析出格式但不确定含义的字段。比如:
|
||||
|
||||
- `unknown_0` 后来被发现是服务器时间戳("小时 + 百万分之一小时"编码)
|
||||
- `unknown_1` 推测等于 `-price_raw`
|
||||
- 指数 K 线比股票 K 线多 4 字节(上涨/下跌家数),是通过对比同一市场的指数和股票响应长度差异发现的
|
||||
|
||||
## 总结:逆向方法论
|
||||
|
||||
这个项目体现的逆向研究路线是:
|
||||
|
||||
1. **抓包** (Wireshark) → 发现 TCP 帧结构、握手序列
|
||||
2. **反汇编** (IDA Pro) → 得到请求构造函数、编解码算法
|
||||
3. **黑盒验证** → 修改参数发请求,对比响应确认字段含义
|
||||
4. **差分分析** → 利用已知数据(如已知价格)反推编码公式
|
||||
5. **对比分析** → 指数 vs 股票、不同市场、不同周期的响应差异
|
||||
6. **开源传承** → pytdx 打下基础,easy-tdx 在此之上修复 bug 并重构
|
||||
|
||||
核心依赖是 **pytdx 项目**做的基础逆向工作(反汇编通达信客户端),easy-tdx 则是在已有协议知识上的高质量 Python 重写。
|
||||
@@ -0,0 +1,384 @@
|
||||
# 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.py` 中 `build_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 LE:`u1_lo`(offset 16) + `u1_hi`(offset 18)。
|
||||
|
||||
- **u1_hi** = 分组 ID,同组内的板块/指数共享相同的 u1_hi 值
|
||||
- **u1_lo** = 组内排序位置,与列表顺序单调递增
|
||||
|
||||
按品种统计:
|
||||
- **SZ 主板**(000xxx):u1_hi ≈ 0,u1_lo 随列表顺序单调递增(630/630 非递减)
|
||||
- **板块**(880xxx):u1_hi 有 27 个不同值(0~154),分组为约 9 个一簇
|
||||
- **SH 指数**(000xxx):u1_hi 范围 0~75,类似分组模式
|
||||
- **创业板**(300xxx):u1_hi ≈ 0,u1_lo 随列表顺序递增
|
||||
|
||||
不是浮点数,不是价格,不是简单计数器——是通达信内部的排序/分组标识。
|
||||
|
||||
### _unknown2(已确认:私有时间戳)
|
||||
|
||||
大部分为 0,非零情况:
|
||||
- SZ 主板:631/631 全部非零
|
||||
- SH 指数:20/200 非零
|
||||
- 板块:20/797 非零
|
||||
- 创业板:0/369
|
||||
|
||||
字节级分析(offset 25-28 = b25, b26, b27, b28):
|
||||
- **b28 编码年份**:值 22~37 对应 2010~2025(公式: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 恒为 7654321(9/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:57`:`unknown_1`(pytdx 称 `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,97`:`unknown_last`,pytdx 原来直接丢弃。
|
||||
|
||||
## 四、硬跳过的字节块
|
||||
|
||||
### 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 30`,prefix=1, market=1(SH), code="600000"。
|
||||
|
||||
gotdx 同样命名为 `Unknown`,pytdx 直接 `pos += 9`。
|
||||
|
||||
### 4.2 xdxr_info 每条记录1字节 padding(已确认:保留/对齐字节)
|
||||
|
||||
**位置**:`commands/xdxr_info.py:46`
|
||||
|
||||
fixture 中 87 条记录全部为 0x00(87/87)。无其他可能值样本,推测为保留/对齐字节。
|
||||
gotdx 命名为 `Unknown`,pytdx 注释 `# noused`。
|
||||
|
||||
### 4.3 fund_flow 9字节响应头部(已确认:同 xdxr_info 格式)
|
||||
|
||||
**位置**:`commands/fund_flow.py:45`
|
||||
|
||||
与 xdxr_info 相同的 9 字节头部格式:prefix(2) + market(1) + code(6)。
|
||||
后接 uint16 num(记录数量),然后是 36 字节/条的固定记录。
|
||||
|
||||
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_raw(10 只股票验证)
|
||||
- unknown_5-8 恒为 0
|
||||
|
||||
**2026-05-27 第二轮:全市场特殊状态扫描**
|
||||
- unknown_4 = 交易状态标志(停牌=32800)
|
||||
- unknown_2 涨停/停牌时恒为 0
|
||||
- unknown_3 与成交量正相关
|
||||
|
||||
**2026-05-27 第三轮:三方向并行突破**
|
||||
|
||||
子任务 A(MAC 协议对照):
|
||||
- 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 - vol**(20/20 精确匹配)
|
||||
- unknown_3 不是换手率(R²=0.507)
|
||||
- unknown_3 与成交额 R²=0.89,但不成固定比例
|
||||
|
||||
**2026-05-27 第四轮:穷举+交叉实现突破**
|
||||
|
||||
穷举候选分析(95 只股票):
|
||||
- 最高相关候选:cur_vol*price_raw(r=0.91),amount(r=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 + lo,hi=主变量(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-27:gotdx 交叉验证完全破解**
|
||||
|
||||
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-27:xdxr_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+code,prefix 可能是类型标识
|
||||
|
||||
xdxr_info 每条1字节 padding:
|
||||
- 87 条记录全部为 0x00,保留/对齐字节
|
||||
|
||||
fund_flow 9字节头部:同 xdxr_info 格式(prefix + market + code)
|
||||
|
||||
block 384字节头部:gotdx 命名"头信息, 忽略",所有实现跳过
|
||||
|
||||
### 已知 Bug 清单
|
||||
|
||||
1. `codec/bitmap.py build_bitmap()` — bit>=128 字段溢出(不影响现有功能)
|
||||
2. `commands/security_list.py` — pre_close 用 `_decode_volume` 而非 IEEE float32(结果正确但语义有误)
|
||||
@@ -3,8 +3,8 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import importlib.util
|
||||
import json
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
import click
|
||||
|
||||
@@ -53,9 +53,9 @@ def backtest(
|
||||
|
||||
easy-tdx backtest SZ 000001 --strategy-file my_strategy.py --indicators MACD,KDJ
|
||||
"""
|
||||
from ..backtest.engine import BacktestEngine
|
||||
from ..cli.conn import get_mac_client
|
||||
from ..cli.parsers import parse_adjust, parse_market, parse_period
|
||||
from ..backtest.engine import BacktestEngine
|
||||
from ..indicator import compute_indicators
|
||||
|
||||
# 1. 加载策略
|
||||
@@ -114,7 +114,6 @@ def _load_strategy(
|
||||
Returns:
|
||||
Strategy 子类
|
||||
"""
|
||||
from ..backtest.strategy import Strategy
|
||||
|
||||
if strategy_file:
|
||||
return _load_strategy_from_file(strategy_file)
|
||||
@@ -174,7 +173,7 @@ def _load_strategy_from_file(path: str) -> type:
|
||||
return strategy_classes[0]
|
||||
|
||||
|
||||
def _print_table(result) -> None:
|
||||
def _print_table(result: Any) -> None:
|
||||
"""以表格形式输出回测结果。"""
|
||||
perf = result.performance
|
||||
config = result.config
|
||||
|
||||
@@ -10,6 +10,7 @@ from collections.abc import Callable
|
||||
from typing import Any
|
||||
|
||||
import numpy as np
|
||||
from numpy.typing import NDArray
|
||||
|
||||
from .strategy import Strategy
|
||||
|
||||
@@ -31,23 +32,24 @@ def dsl_strategy(func: Callable[..., Any]) -> type[Strategy]:
|
||||
|
||||
class DSLStrategy(Strategy):
|
||||
_signal_func = staticmethod(func)
|
||||
_buy_mask: np.ndarray | None = None
|
||||
_sell_mask: np.ndarray | None = None
|
||||
_buy_mask: NDArray[np.bool_] | None = None
|
||||
_sell_mask: NDArray[np.bool_] | None = None
|
||||
|
||||
def init(self) -> None:
|
||||
pass
|
||||
|
||||
def next(self) -> None:
|
||||
if self._buy_mask is None:
|
||||
buy = self._buy_mask
|
||||
sell = self._sell_mask
|
||||
if buy is None or sell is None:
|
||||
return
|
||||
idx = self._bar_index
|
||||
if idx < len(self._buy_mask) and self._buy_mask[idx]:
|
||||
if idx < len(buy) and buy[idx]:
|
||||
self.buy(size=0)
|
||||
elif idx < len(self._sell_mask) and self._sell_mask[idx]:
|
||||
elif idx < len(sell) and sell[idx]:
|
||||
self.sell(size=0)
|
||||
|
||||
DSLStrategy.__name__ = func.__name__
|
||||
DSLStrategy.__qualname__ = func.__qualname__
|
||||
DSLStrategy._signal_func = func # type: ignore[attr-defined]
|
||||
|
||||
return DSLStrategy
|
||||
|
||||
@@ -5,8 +5,6 @@
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Any
|
||||
|
||||
import numpy as np
|
||||
import pandas as pd
|
||||
|
||||
|
||||
@@ -100,7 +100,7 @@ class StrategyDataProxy:
|
||||
if col == "datetime":
|
||||
continue
|
||||
arr = df[col].to_numpy()
|
||||
if len(arr) > 0 and isinstance(arr[0], (np.datetime64, pd.Timestamp)):
|
||||
if len(arr) > 0 and isinstance(arr[0], np.datetime64 | pd.Timestamp):
|
||||
# datetime 列转为 int (YYYYMMDD)
|
||||
self._arrays[col] = _datetime_to_int(arr)
|
||||
else:
|
||||
@@ -250,7 +250,7 @@ class Strategy(ABC):
|
||||
|
||||
# ── 指标注册 ───────────────────────────────────────────────────────────────
|
||||
|
||||
def I(
|
||||
def I( # noqa: E743
|
||||
self, func: Callable[..., NDArray], *args: Any, **kwargs: Any
|
||||
) -> NDArray:
|
||||
"""注册指标。
|
||||
@@ -441,7 +441,7 @@ def _datetime_to_int(arr: NDArray) -> NDArray:
|
||||
"""
|
||||
result = np.zeros(len(arr), dtype=np.float64)
|
||||
for i, val in enumerate(arr):
|
||||
if isinstance(val, (np.datetime64, pd.Timestamp)):
|
||||
if isinstance(val, np.datetime64 | pd.Timestamp):
|
||||
ts = pd.Timestamp(val)
|
||||
result[i] = float(ts.strftime("%Y%m%d"))
|
||||
else:
|
||||
|
||||
@@ -2,15 +2,11 @@
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import warnings
|
||||
|
||||
import pandas as pd
|
||||
import pytest
|
||||
|
||||
from easy_tdx.backtest.orders import OrderSimulator
|
||||
from easy_tdx.backtest.types import Signal
|
||||
|
||||
|
||||
# ── Test Fixtures ─────────────────────────────────────────────────────────────
|
||||
|
||||
|
||||
|
||||
@@ -12,8 +12,6 @@ from easy_tdx.backtest.strategy import (
|
||||
_SeriesAccessor,
|
||||
crossover,
|
||||
)
|
||||
from easy_tdx.backtest.types import Signal
|
||||
|
||||
|
||||
# ── 辅助函数 ─────────────────────────────────────────────────────────────────────
|
||||
|
||||
@@ -186,12 +184,12 @@ class TestCrossover:
|
||||
mask = crossover(a, b)
|
||||
|
||||
# 索引 3 处:a[2]=3 <= b[2]=3, a[3]=4 > b[3]=2 → 金叉
|
||||
assert mask[3] == True
|
||||
assert mask[3]
|
||||
# 其他位置无金叉
|
||||
assert mask[0] == False
|
||||
assert mask[1] == False
|
||||
assert mask[2] == False
|
||||
assert mask[4] == False
|
||||
assert not mask[0]
|
||||
assert not mask[1]
|
||||
assert not mask[2]
|
||||
assert not mask[4]
|
||||
|
||||
def test_crossover_false_no_cross(self) -> None:
|
||||
"""测试无金叉情况(a 全在 b 下方)。"""
|
||||
@@ -208,7 +206,7 @@ class TestCrossover:
|
||||
b = pd.Series([5, 4, 3, 2, 1])
|
||||
mask = crossover(a, b)
|
||||
|
||||
assert mask[3] == True
|
||||
assert mask[3]
|
||||
|
||||
def test_crossover_with_accessor(self) -> None:
|
||||
"""测试接受 _SeriesAccessor 参数。"""
|
||||
@@ -218,7 +216,7 @@ class TestCrossover:
|
||||
acc_b = _SeriesAccessor(arr_b, bar_index=4)
|
||||
|
||||
mask = crossover(acc_a, acc_b)
|
||||
assert mask[3] == True
|
||||
assert mask[3]
|
||||
|
||||
|
||||
# ── TestStrategyBase ──────────────────────────────────────────────────────────────
|
||||
|
||||
Reference in New Issue
Block a user