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:
GitHub
2026-06-09 18:21:25 +08:00
co-authored by Claude Opus 4.8
parent fc0777533e
commit 04c2be1d7f
11 changed files with 2724 additions and 29 deletions
+5 -1
View File
@@ -72,7 +72,11 @@
"Bash(gh auth *)", "Bash(gh auth *)",
"Bash(rm -rf easy_tdx.wiki)", "Bash(rm -rf easy_tdx.wiki)",
"Bash(gh repo *)", "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
+144
View File
@@ -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字节) — zipsizebody 实际长度)
偏移 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, # 命令 IDK线数据的标识)
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 重写。
+384
View File
@@ -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 ≈ 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 编码年份**:值 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 恒为 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: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_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 - vol**20/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 格式(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 -4
View File
@@ -3,8 +3,8 @@
from __future__ import annotations from __future__ import annotations
import importlib.util import importlib.util
import json
from pathlib import Path from pathlib import Path
from typing import Any
import click import click
@@ -53,9 +53,9 @@ def backtest(
easy-tdx backtest SZ 000001 --strategy-file my_strategy.py --indicators MACD,KDJ 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.conn import get_mac_client
from ..cli.parsers import parse_adjust, parse_market, parse_period from ..cli.parsers import parse_adjust, parse_market, parse_period
from ..backtest.engine import BacktestEngine
from ..indicator import compute_indicators from ..indicator import compute_indicators
# 1. 加载策略 # 1. 加载策略
@@ -114,7 +114,6 @@ def _load_strategy(
Returns: Returns:
Strategy 子类 Strategy 子类
""" """
from ..backtest.strategy import Strategy
if strategy_file: if strategy_file:
return _load_strategy_from_file(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] return strategy_classes[0]
def _print_table(result) -> None: def _print_table(result: Any) -> None:
"""以表格形式输出回测结果。""" """以表格形式输出回测结果。"""
perf = result.performance perf = result.performance
config = result.config config = result.config
+8 -6
View File
@@ -10,6 +10,7 @@ from collections.abc import Callable
from typing import Any from typing import Any
import numpy as np import numpy as np
from numpy.typing import NDArray
from .strategy import Strategy from .strategy import Strategy
@@ -31,23 +32,24 @@ def dsl_strategy(func: Callable[..., Any]) -> type[Strategy]:
class DSLStrategy(Strategy): class DSLStrategy(Strategy):
_signal_func = staticmethod(func) _signal_func = staticmethod(func)
_buy_mask: np.ndarray | None = None _buy_mask: NDArray[np.bool_] | None = None
_sell_mask: np.ndarray | None = None _sell_mask: NDArray[np.bool_] | None = None
def init(self) -> None: def init(self) -> None:
pass pass
def next(self) -> None: 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 return
idx = self._bar_index 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) 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) self.sell(size=0)
DSLStrategy.__name__ = func.__name__ DSLStrategy.__name__ = func.__name__
DSLStrategy.__qualname__ = func.__qualname__ DSLStrategy.__qualname__ = func.__qualname__
DSLStrategy._signal_func = func # type: ignore[attr-defined]
return DSLStrategy return DSLStrategy
-2
View File
@@ -5,8 +5,6 @@
from __future__ import annotations from __future__ import annotations
from typing import Any
import numpy as np import numpy as np
import pandas as pd import pandas as pd
+3 -3
View File
@@ -100,7 +100,7 @@ class StrategyDataProxy:
if col == "datetime": if col == "datetime":
continue continue
arr = df[col].to_numpy() 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) # datetime 列转为 int (YYYYMMDD)
self._arrays[col] = _datetime_to_int(arr) self._arrays[col] = _datetime_to_int(arr)
else: else:
@@ -250,7 +250,7 @@ class Strategy(ABC):
# ── 指标注册 ─────────────────────────────────────────────────────────────── # ── 指标注册 ───────────────────────────────────────────────────────────────
def I( def I( # noqa: E743
self, func: Callable[..., NDArray], *args: Any, **kwargs: Any self, func: Callable[..., NDArray], *args: Any, **kwargs: Any
) -> NDArray: ) -> NDArray:
"""注册指标。 """注册指标。
@@ -441,7 +441,7 @@ def _datetime_to_int(arr: NDArray) -> NDArray:
""" """
result = np.zeros(len(arr), dtype=np.float64) result = np.zeros(len(arr), dtype=np.float64)
for i, val in enumerate(arr): for i, val in enumerate(arr):
if isinstance(val, (np.datetime64, pd.Timestamp)): if isinstance(val, np.datetime64 | pd.Timestamp):
ts = pd.Timestamp(val) ts = pd.Timestamp(val)
result[i] = float(ts.strftime("%Y%m%d")) result[i] = float(ts.strftime("%Y%m%d"))
else: else:
-4
View File
@@ -2,15 +2,11 @@
from __future__ import annotations from __future__ import annotations
import warnings
import pandas as pd import pandas as pd
import pytest
from easy_tdx.backtest.orders import OrderSimulator from easy_tdx.backtest.orders import OrderSimulator
from easy_tdx.backtest.types import Signal from easy_tdx.backtest.types import Signal
# ── Test Fixtures ───────────────────────────────────────────────────────────── # ── Test Fixtures ─────────────────────────────────────────────────────────────
+7 -9
View File
@@ -12,8 +12,6 @@ from easy_tdx.backtest.strategy import (
_SeriesAccessor, _SeriesAccessor,
crossover, crossover,
) )
from easy_tdx.backtest.types import Signal
# ── 辅助函数 ───────────────────────────────────────────────────────────────────── # ── 辅助函数 ─────────────────────────────────────────────────────────────────────
@@ -186,12 +184,12 @@ class TestCrossover:
mask = crossover(a, b) mask = crossover(a, b)
# 索引 3 处:a[2]=3 <= b[2]=3, a[3]=4 > b[3]=2 → 金叉 # 索引 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 not mask[0]
assert mask[1] == False assert not mask[1]
assert mask[2] == False assert not mask[2]
assert mask[4] == False assert not mask[4]
def test_crossover_false_no_cross(self) -> None: def test_crossover_false_no_cross(self) -> None:
"""测试无金叉情况(a 全在 b 下方)。""" """测试无金叉情况(a 全在 b 下方)。"""
@@ -208,7 +206,7 @@ class TestCrossover:
b = pd.Series([5, 4, 3, 2, 1]) b = pd.Series([5, 4, 3, 2, 1])
mask = crossover(a, b) mask = crossover(a, b)
assert mask[3] == True assert mask[3]
def test_crossover_with_accessor(self) -> None: def test_crossover_with_accessor(self) -> None:
"""测试接受 _SeriesAccessor 参数。""" """测试接受 _SeriesAccessor 参数。"""
@@ -218,7 +216,7 @@ class TestCrossover:
acc_b = _SeriesAccessor(arr_b, bar_index=4) acc_b = _SeriesAccessor(arr_b, bar_index=4)
mask = crossover(acc_a, acc_b) mask = crossover(acc_a, acc_b)
assert mask[3] == True assert mask[3]
# ── TestStrategyBase ────────────────────────────────────────────────────────────── # ── TestStrategyBase ──────────────────────────────────────────────────────────────