Files
big-qmt/docs/AUDIT_AND_REMEDIATION.md
2026-08-29 00:44:41 +08:00

373 lines
11 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# big-qmt 项目审计与整改建议
- 审计日期2026-08-28
- 审计范围:服务端 `api/`、客户端 `py-client/`
- 审计方式:静态代码检查、调用链核对、只读语法编译
- 当前状态:仅供人工确认,尚未实施代码整改
## 一、总体结论
当前版本不建议直接进入实盘运行。
服务端存在文件编码导致的启动级错误,且所有 QMT 调用和大对象序列化都在 Tornado 主线程同步执行。客户端的持仓管理、止盈、补仓、撤单和下单确认链存在多处必现错误或状态不一致风险。
建议按以下顺序处理:
1. 恢复服务端和客户端的基本可运行性。
2. 修复交易安全相关的订单确认、撤单和持仓数据模型。
3. 为止盈、补仓、订单状态机建立测试。
4. 在确认 QMT 线程约束后优化服务端响应速度。
5. 最后进行结构简化和重复代码清理。
---
## 二、P0启动及交易安全问题
### 2.1 服务端文件编码不一致,程序无法正常编译
位置:`api/QMT_API.py:1`
现状:
```python
# -*- coding: gbk -*-
```
文件实际内容包含 UTF-8 字节,只读编译时报错:
```text
SyntaxError: 'gbk' codec can't decode byte ...
```
影响:服务端可能在载入阶段直接退出,所有 API 不可用。
解决方案:
如果运行环境强制要求 GBK则必须把整个文件真实转换为 GBK不能只修改声明。
验收标准:
- 中文日志和错误响应无乱码。
### 2.2 客户端持仓对象被错误当成字典和二元组使用
位置:`py-client/strategy/trend/positions.py:32-40`
现状:`client.positions()` 返回 `list[Position]`,但代码同时使用:
```python
for idx, pos in positions:
code = pos["stock_code"]
avg_price = pos.get("avg_price", 0)
```
行情结果同样是 `Tick` dataclass却使用字典的 `.get()`
影响:进入持仓管理后必然抛出 `TypeError``AttributeError`,止盈和补仓完全无法执行。
解决方案:
全项目统一使用 SDK dataclass + __slots__,不再混用原始字典。
验收标准:
- 使用真实 `Position``Tick` 对象执行一轮不抛异常。
### 2.3 `handle_profit` 调用参数和函数签名不一致
位置:
- 调用:`py-client/strategy/trend/positions.py:58`
- 定义:`py-client/strategy/trend/positions.py:73`
影响:修复持仓遍历后,下一步仍会立即触发 `TypeError`
解决方案:
删除未使用的 `open_price``strategy_name` 或把它们纳入统一模型。
推荐接口:
```python
def handle_profit(
runtime: Runtime,
position: Position,
tick: Tick,
pnl_rate: float,
) -> ProfitDecision:
...
```
验收标准:
- 静态类型检查能够发现参数数量错误。
- ARMED、RAISED、STEADY、RETREAT 四种状态都有测试。
### 2.4 补仓流程存在多处必现错误
位置:`py-client/strategy/trend/positions.py:118-159`
问题包括:
- `StateItem` 被当成字典调用 `.get()`
- `orders.busy()` 多传入一个 `run` 参数。
- 某些分支只返回 `False`,调用方却解包两个值。
- 使用不存在的 `run.state.STATUS_ING`
- `state.added_num = +1` 每次都赋值为 1并非累加。
- `LOSS_TIERS[added_num]` 可能数组越界。
- 下单后没有扣减本轮剩余预算,多持仓可能超额补仓。
解决方案:
1. 所有 `StateItem` 字段改为属性访问。
2. `orders.busy(code, "BUY")` 使用正确签名。
3. 所有返回分支统一返回结构,推荐使用 dataclass + __slots__
```python
@dataclass(frozen=True)
class TradeDecision:
submitted: bool
message: str = ""
reserved_cash: float = 0.0
```
4. 使用模块常量 `STATUS_ING`,或把状态定义成 `Enum`
5. 补仓次数使用 `state.added_num += 1`
6.`added_num >= len(LOSS_TIERS)` 时明确禁止继续补仓。
7. `RunOnce` 创建本轮 `remaining_cash`,每次成功提交补仓后立即扣减。
验收标准:
- 第 0、1、2 次补仓边界均有测试。
- 超过最大补仓次数不会抛异常或继续下单。
### 2.5 止盈跟踪器每轮重建,无法形成跨轮回撤
位置:`py-client/strategy/trend/positions.py:31`
影响:每轮都会清空最高盈利网格,止盈状态无法从 ARMED/RAISED 演进至 RETREAT。
解决方案:
1. `GridTrailingTracker` 应作为 `Runtime` 字段,在策略启动时只创建一次。
2. 检查是否有定时清理的功能
验收标准:
- 连续输入 2.1%、3.1%、2.9% 能产生 ARMED、RAISED、RETREAT。
- 相同股票不同账户的峰值互不污染。
- 清仓后重新建仓不会继承旧峰值。
### 2.6 “取消过期订单”只查询可撤状态,没有执行撤单
位置:
- 客户端:`py-client/strategy/trend/order.py:74-88`
- 服务端:`api/QMT_API.py:962-969`
影响:过期订单一直保留,订单锁可能长期阻止新交易。
解决方案:
方案 A推荐新增按真实委托号撤单接口。
```text
POST /api/order/cancel_by_id
body: {order_id, account_type}
```
服务端先执行 `can_cancel_order()`,可撤时调用真正的 `cancel()`,并返回撤单请求结果。
验收标准:
- 暂时不做验证,后期验证
### 2.7 低现金资金闸同时跳过卖出管理
位置:`py-client/strategy/trend/boot.py:126-134`
影响:可用资金不足时直接结束整轮流程,持仓止盈和风险退出也被禁止。
解决方案:
把“是否允许新开仓/补仓”和“是否允许卖出”拆成不同条件。
验收标准:
- 可用现金低于阈值时不开仓、可补仓。
- 同一情况下满足止盈条件的持仓仍然能够提交卖单。
### 2.9 客户端订单标签未真正传给 QMT
位置:
- 客户端发送:`py-client/sdk/trade.py:10-18`
- 服务端丢弃:`api/QMT_API.py:663`
现状:客户端发送 `strategyName`,服务端调用 `passorder()` 时却硬编码为 `qmt`
解决方案:
1. 统一strategy_name 为信号的key,m_strRemark为本地业务订单号。
2. 同时修改QMT_API.py
验收标准:
- 下单后在 QMT 委托明细中可以看到客户端标签。
- 能从本地订单 ID 追踪到真实委托号和最终成交。
---
## 三、P1服务端响应速度整改
验收标准:
- 一轮策略账户查询由三次以上 QMT 调用下降为一次快照调用。
- 下单后下一次快照不会返回过期的订单状态。
### 3.3 大量使用 `dir()` 和 `getattr()` 反射序列化
位置:
- `api/QMT_API.py:920-930`
- `api/QMT_API.py:943-950`
- `api/QMT_API.py:979-1031`
- `api/QMT_API.py:1454-1473`
影响对每个对象遍历全部属性、捕获异常并转字符串CPU 开销大,返回字段也不稳定。
解决方案:
1. 为订单、成交、资产、持仓等类型定义固定字段映射。
2. 只返回客户端实际使用的字段。
3. 使用统一的轻量转换函数,不在每个 Handler 复制反射循环。
4. 对未知扩展类型单独保留调试接口,不进入高频生产路径。
验收标准:
- 高频订单查询不再调用 `dir()`
- 返回 JSON 字段固定并有接口契约测试。
- 相同数据量下序列化 CPU 时间明显下降。
### 3.5 回调同步写 JSON 文件
位置:`api/QMT_API.py:1475-1516`
影响:目录创建、反射序列化和格式化写盘可能阻塞 QMT 回调线程。
解决方案:
3. 生产环境关闭 `indent=4`
4. 使用临时文件替换,避免半写文件。
验收标准:
- 回调函数本身在毫秒级返回。
- 磁盘慢或不可写时不会阻塞交易回调。
- 写入失败可监控且不会静默丢失。
### 3.6 客户端 HTTP 没有连接池
位置:`py-client/sdk/client.py:28-47`
影响:每次 `urlopen()` 都可能新建连接,高频轮询产生额外 TCP 开销。
解决方案:
1. 改用支持连接池的 HTTP 客户端,如 `httpx.Client``requests.Session`
2. 整个策略生命周期复用一个 Client。
3. 设置连接、读取和总超时,不只设置单一 timeout。
4. 只对幂等查询配置有限重试;下单和撤单不能自动盲重试。
验收标准:
- 连续请求复用 TCP 连接。
- 查询超时能重试,下单超时进入“结果未知、需对账”状态而不是重复下单。
---
## 四、P1客户端其他逻辑与可靠性问题
### 4.1 新开仓订单锁可能在同一轮失效
位置:
- `py-client/strategy/trend/open.py:24`
- `py-client/strategy/trend/order.py:61-65`
- `py-client/strategy/trend/order.py:99-101`
现状:开仓检查 `busy()`,该方法只查看 `data`;新下单后只把键加入 `index`,没有加入 `data`
解决方案:
1. 统一锁判断,只保留一个权威接口。
2. 下单成功后立即插入本地 pending `OrderItem`
3. 信号进入处理前按证券代码去重。
4. 每轮刷新券商订单后用真实订单覆盖本地 pending 状态。
验收标准:同一轮两个来源返回同一证券信号时最多提交一笔买单。
### 4.2 `Runtime` 文档和字段不一致
位置:`py-client/strategy/trend/runtime.py`
现状:文档描述 `peak_grids`,实际 dataclass 没有该字段;持仓代码仍可能访问它。
解决方案:
2. 如果统一使用 `GridTrailingTracker`,删除 `peak_grids` 及所有引用。
3. 不应同时保留两套止盈峰值实现。
验收标准:项目中只有一种网格峰值状态来源。
### 4.3 `ping_api_host()` 参数无效且吞掉退出信号
位置:`py-client/main.py:59-76`
问题:
- `rpc_host` 参数没有使用。
- `connect_timeout` 参数没有使用。
- 使用裸 `except:`,会捕获 `KeyboardInterrupt``SystemExit`
- 无限重试没有最大日志节流或取消事件。
解决方案:
1. 函数重命名为 `wait_for_qmt_api()`,删除无用参数。
2. 仅捕获网络类异常和 `APIError`
3. 允许 `KeyboardInterrupt` 正常终止。
4. 使用 `threading.Event.wait()` 或可取消等待。
验收标准API 不可用时可以通过 Ctrl+C 立即退出。
### 4.5 状态文件缺少完整对账和生命周期
位置:`py-client/strategy/trend/state.py`
解决方案:
1. 启动时用真实持仓、订单和成交三方对账。
2. `ING` 状态必须根据真实订单结果转为 `OK``FAILED``CANCELED``UNKNOWN`
3. 已清仓证券应从状态中删除,并清除观察器和止盈峰值。
4. 状态文件不增加版本号,不增加新字段。
验收标准:程序在下单后崩溃并重启,能够从券商真实状态恢复,而不会重复下单。
### 4.6 日志调用格式错误且异常上下文不足
位置:`py-client/`
现状:不符合 logging 格式化规则。
解决方案:
1. 统一优化日志打印同时输出至文本文件(每天一个文件)。
验收标准:日志输出期间不出现 logging 自身的格式化异常。