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