# 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 委托不重复申购”;最后在模拟账户完成买入、部分成交、撤单、卖出、重启对账闭环。