Files
big-qmt/docs/CODE_REAUDIT_2026-08-29_V3.md
2026-08-30 00:34:27 +08:00

250 lines
13 KiB
Markdown
Raw Permalink 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-29 V3
## 1. 审计范围与验证
- 基于当前未提交工作区重新审计,不直接复制上一版报告。
- 范围:`api/``py-client/`、配置、启动脚本和测试。
- 明确排除:`docs/todo.md`;本轮未读取、未引用其内容。
- 全套测试:`python -B -m unittest discover -s tests -v`
- 测试结果12 项中 8 项通过、4 项错误;错误全部位于 IPO 测试。
- 全部 Python 文件 AST 解析通过。
- 未连接真实 QMT未执行真实委托、成交和撤单。
## 2. 总体结论
当前版本仍不建议直接实盘运行。
本轮有两项明确改善:趋势策略已在每轮重新对账,`ING/UNKNOWN` 同时参与候选过滤和下单前检查;订单方向锁也已经改成带秒级时间戳的字典并自动清理。新增的 `UNKNOWN` 无持仓防重复测试通过。
但订单锁的新过期规则会在真实委托仍活动时强制解锁券商刷新还会整体覆盖本地下单锁卖出订单没有持久化状态兜底因此存在重复卖出风险。IPO 仍然存在“业务失败响应也写成功锁”、券商侧防重缺失和函数契约错误。资金预算、订单覆盖及启动脚本风险也尚未解决。
## 3. P0致命或可能造成错误交易
### 3.1 活动委托达到 180 秒会被强制解锁,可能重复下单
位置:`py-client/strategy/trend/order.py:47-52,59-89,138-146`
业务锁是否有效只由创建时间和 `lock_timeout_sec` 决定,不再参考订单是否仍属于 `BUSY_STATUSES``refresh()` 明明从券商读到活动订单,仍会把创建时间超过 180 秒的锁立即删除。
定向复现:状态为 `50` 的活动卖单,创建 181 秒后,`busy("A", "SELL")` 返回 `False`
开仓和补仓还有 `State``ING/UNKNOWN` 兜底,但止盈卖单没有写入持久化状态。活动卖单超时或撤单失败后,下一轮止盈判断可能再次提交同一证券卖单。
建议:本地“提交防抖锁”可以超时,但券商明确返回活动状态时必须继续视为 busy将两者拆成 `local_locks``active_order_index``busy()` 对二者取并集。卖出订单也应进入可恢复状态或订单索引。
### 3.2 刷新券商订单会整体覆盖刚提交的本地锁
位置:`py-client/strategy/trend/order.py:72-89,132-135`
`refresh()` 使用新字典直接替换 `self.lock`。如果下单接口成功后,券商订单明细存在短暂可见性延迟,下一轮刷新会删除刚写入的本地锁。对于没有状态兜底的卖单,这同样可能导致重复委托。
建议:刷新时合并尚未超过防抖时限的本地锁;只有券商明确返回终结状态,或本地锁超时且券商持续不可见,才能移除,并记录告警。
### 3.3 IPO 不检查业务返回结果就写成功锁
位置:`py-client/strategy/ipo/boot.py:37-55`
当前 `try` 只能捕获 Python 异常。`client.passorder()` 返回 `{"status": "failed"}`、空订单号或其他业务拒绝响应时不会抛异常,代码仍进入 `else` 并写锁文件。
影响:券商没有接受申购,本地却永久标记为已申购,造成漏申购。
建议:捕获异常之外,还必须校验返回值为字典、`status == "success"``order_ref` 有效;满足全部条件后才能写锁。
### 3.4 IPO 没有券商委托/成交防重
位置:`py-client/strategy/ipo/boot.py:31-57`
当前只检查本地空文件,不查询当日券商委托和成交。下单成功后、写锁前崩溃,或锁目录被清理/切换时10:00 与 14:00 两次任务可能对同一证券再次提交申购。
建议:以账户、交易日、证券代码和 IPO 备注核对 `trade_detail_data("order")``deals()`;本地锁只能作为快速缓存,不能作为唯一事实来源。
### 3.5 趋势开仓仍可能超过单笔预算和账户可用现金
位置:
- `py-client/libs/calc.py:10-12`
- `py-client/strategy/trend/boot.py:127-185`
- `py-client/strategy/trend/open.py:41-74`
`calc_buy_volume()` 在预算不足一手时仍强制返回 100 股。趋势开仓只检查一次现金比例,没有校验单笔预计金额,也没有在同一轮多个信号之间预留资金。
影响:高价股会超过 `buy_value`;多个信号可能同时使用同一份可用现金,发送总额超限的委托。
建议:预算不足一手时返回 0开仓循环维护共享 `remaining_cash`,成功提交后立即扣减预留金额,并保留滑点余量。
## 4. P1重要业务逻辑错误
### 4.1 IPO 的 4 项现有测试全部无法进入业务逻辑
位置:
- `py-client/strategy/ipo/boot.py:22`
- `py-client/tests/test_ipo.py:52-136`
`AutoBuyIpo()` 删除了原有可选时间参数,测试传入固定时间时全部报:`TypeError: AutoBuyIpo() takes 0 positional arguments but 1 was given`。APScheduler 无参调用与保留可选 `now` 并不冲突。
测试还要求但当前实现没有满足:成功数量返回、上下文关闭、重启防重、券商记录防重、单只拒绝不影响其他候选。
建议:恢复 `now: datetime | None = None`,内部使用 `now or datetime.now()`;修复生产逻辑后让测试通过,不要删除失败测试。
### 4.2 IPO 正常完成时返回 `None`,与 `-> int` 不一致
位置:`py-client/strategy/ipo/boot.py:22-57`
只有禁用和非交易时间返回 0完成候选循环后没有返回值也没有统计成功数。
建议:维护 `success_count`,所有出口都返回整数。
### 4.3 IPO 资源关闭仍不具备异常安全性
位置:`py-client/strategy/ipo/boot.py:31-57`
`client.close()` 只在整个流程正常走到末尾时执行。`ipo_data()`、候选字段读取、锁写入等任何异常都会跳过关闭。单只下单异常虽然被捕获,但仅记录普通 info丢失堆栈和具体原因。
建议:使用 `with Client(...) as client:`;单只异常用 `logging.exception()` 并继续其他候选。
### 4.4 IPO 只判断工作日,不判断真实交易日
位置:
- `py-client/strategy/ipo/boot.py:27`
- `py-client/libs/calc.py:5-7`
法定节假日或临时休市仍会进入申购。模块中的 `TRADING_CALENDAR_SYMBOL` 未使用。
建议:用 SDK `trading_dates()` 确认当天交易日;查询失败时安全跳过。
### 4.5 同证券同方向多笔委托仍会互相覆盖
位置:`py-client/strategy/trend/order.py:74-89,132-135`
`data``lock` 都以 `SIDE-code` 为唯一键。两笔相同证券、相同方向的订单只保留最后一笔。
定向复现:传入两笔 `SELL-A`,刷新后 `len(data) == 1`
影响:被覆盖订单无法被撤销、展示或独立对账。
建议:订单数据以系统委托号为主键,另建 `(side, code) -> set[order_id]` 活动索引。
### 4.6 撤单状态集合与活动状态集合不一致
位置:`py-client/strategy/trend/order.py:15,97-104`
`BUSY_STATUSES` 包含 `48``55`,撤单逻辑只处理 `49``52`。如果 `48/55` 也是可继续成交且可撤的状态,它们永远不会被超时撤销;同时其锁仍可能在 180 秒后被删除。
建议:建立单一、有文档依据的状态映射,分别定义“活动”“可撤”“终结”,不要在不同函数中散落不一致的魔法集合。
### 4.7 撤单请求会每轮重复发送
位置:`py-client/strategy/trend/order.py:91-104`
默认 10 秒即触发撤单,主循环每 30 秒刷新一次;只要券商仍返回相同活动状态,每轮都会再次调用 `cancel_by_id()`,没有撤单中、本地冷却或最大重试次数。
建议:记录最近撤单请求时间和结果;处于撤单中的订单采用退避重试,并设置最大次数和告警。
### 4.8 状态保存失败后的安全策略仍不统一
位置:
- `py-client/strategy/trend/open.py:59-74`
- `py-client/strategy/trend/positions.py:175-183`
开仓保存失败只记录日志并继续;补仓保存失败抛出异常。两种情况都可能已经有真实订单,但恢复数据未落盘。
建议:落盘失败后保留内存订单锁并熔断该证券,持续重试保存;完成券商对账前禁止后续自动交易。
### 4.9 启动脚本会杀死机器上的全部 Python 进程
位置:`run.bat:1-7`
`taskkill /IM python.exe /F` 不区分 PID 或项目;脚本又没有先切换到 `py-client``python main.py` 依赖调用目录。
建议:仅管理本项目 PID使用 `%~dp0` 构造绝对路径;生产启动不要无条件 `git pull`
## 5. P2冗余、过度验证与维护风险
### 5.1 每轮重复查询两次委托明细
位置:
- `py-client/strategy/trend/boot.py:121-125,147-153`
- `py-client/strategy/trend/order.py:72-93`
`cancel_expired()` 内部先调用一次 `trade_detail_data("order")`,随后 `RunOnce()` 为状态对账再次调用同一接口。30 秒一轮时属于稳定的重复网络请求,两次快照还可能不一致。
建议:每轮只获取一次原始订单快照,同时传给订单簿刷新、撤单判断和状态对账。
### 5.2 状态对账重复落盘
位置:`py-client/strategy/trend/state.py:107-133,135-162`
`reconcile()` 先调用会保存的 `sync_positions()`,结束时再次保存。每轮至少写两次状态文件,第一份还是未完成订单对账的中间状态。
建议:内部同步只更新内存,完整对账成功后一次原子保存。
### 5.3 `State.delete()` 保护仍然过宽且静默
位置:`py-client/strategy/trend/state.py:93-105`
虽然已经返回布尔值,但任何 `ING/UNKNOWN` 都只按状态字符串拒绝删除,没有订单证据、日志或人工确认后的强制清理入口。当前内部清理也没有使用返回值记录拒绝原因。
建议:把删除许可作为明确的对账结果;为人工确认提供带原因的审计接口。
### 5.4 两份 API 文件逐字节重复,正式入口处于删除状态
位置:
- `api/qmt_api_new.py`
- `api/qmt_api_rele.py`
- 当前删除的 `api/QMT_API.py`
两个新文件 SHA-256 完全相同,正式加载哪个文件不明确。
建议:只保留一个正式入口,版本差异交由 Git 管理。
### 5.5 IPO 模块存在未使用的旧设计残留
位置:`py-client/strategy/ipo/boot.py:5-19`
`json``Any``IPO_STRATEGY_NAME``IPO_REMARKS``IPO_SESSIONS``TRADING_CALENDAR_SYMBOL` 当前均未使用。
建议:先完成券商对账和交易日逻辑,再删除确认无用的符号。
### 5.6 主程序仍有不可达代码和拼写错误
位置:`py-client/main.py:13-14,126-127`
`StartTrend()` 正常情况下永久循环,后面的日志不可达;即使返回,`config.account_config.strateg` 也不存在。`GLOBAL_CONFIG_PATH` 未使用。
建议:启动成功日志放在进入循环前;删除不可达代码和无效常量。
### 5.7 `replace_existing=True` 在当前内存调度器中是多余防御
位置:`py-client/main.py:115-122`
每次进程启动都创建全新内存调度器,并只注册一次固定任务,不存在同一调度器重复注册路径。该参数无害,但容易暗示存在持久化 job store。
建议:没有持久化任务仓库时可删除;未来启用持久化后再明确任务替换策略。
## 6. 本轮已确认改善
- APScheduler 在北京时间每日 10:00、14:00 触发,不受趋势永久循环阻塞。
- IPO 单只 `passorder()` 抛异常时不再写锁,并会继续处理后续候选。
- `dev.yaml` 已显式启用信号列表。
- 趋势每轮重新读取委托和成交进行状态对账;对账失败时本轮停止自动交易。
- `ING/UNKNOWN` 已同时进入候选过滤和提交前检查。
- `State.delete()` 已返回真实删除结果。
- 趋势新增的 `UNKNOWN` 防重复测试通过;趋势测试共 8 项全部通过。
- 订单方向锁已经改为 `dict[str, float]`,过期清理本身可工作。
## 7. 建议整改顺序与验收门槛
1. 先拆分“本地防抖锁”和“券商活动订单索引”,确保活动委托绝不因时间到期而变成不忙。
2. 修复 IPO 返回值校验、券商防重、可选时间参数、资源关闭和成功计数,使现有 4 项 IPO 测试通过。
3. 修复开仓单笔和同轮资金预算。
4. 将订单簿改为系统委托号主键,统一活动/可撤/终结状态表和撤单冷却。
5. 消除重复网络查询和重复状态写盘。
6. 最后清理启动脚本、重复 API 文件和死代码。
最低验收要求:现有 12 项测试全部通过;新增“活动卖单超过锁超时仍 busy”“券商刷新延迟不清除本地锁”“两笔同方向订单均可撤销”“IPO 业务失败响应不写锁”“锁丢失但券商已有 IPO 委托不重复申购”;最后在模拟账户完成买入、部分成交、撤单、卖出、重启对账闭环。