# 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 自身的格式化异常。