update api,libs
This commit is contained in:
204
docs/CODE_AUDIT_2026-08-29.md
Normal file
204
docs/CODE_AUDIT_2026-08-29.md
Normal file
@@ -0,0 +1,204 @@
|
||||
# big-qmt 代码复审报告(2026-08-29)
|
||||
|
||||
## 1. 审计范围与方法
|
||||
|
||||
- 审计范围:当前工作区中的 `api/`、`py-client/`、启动脚本及测试。
|
||||
- 明确排除:`docs/todo.md`,本报告没有读取或引用该文件作为判断依据。
|
||||
- 关注项:致命错误、交易业务逻辑错误、代码冗余、过度验证。
|
||||
- 动态验证:`python -B -m unittest discover -s tests -v`;共 11 项,8 项通过、3 项错误。
|
||||
- 编译验证:`python -B -m compileall -q .` 通过。
|
||||
- 限制:未连接真实 QMT,未执行真实委托、撤单或成交回报验证。
|
||||
|
||||
## 2. 总结
|
||||
|
||||
当前版本不适合直接实盘运行。至少存在 4 条会使计划业务完全不执行或造成重复/超额下单的高风险路径:
|
||||
|
||||
1. IPO 功能在启用时必然先抛出 `TypeError`,而且其后仍有路径拼接、锁判断和调度生命周期错误。
|
||||
2. 趋势策略的 `UNKNOWN` 状态虽然不会被删除,但并未参与开仓过滤,无法兑现“禁止自动重复下单”。
|
||||
3. 多个开仓信号之间没有共享可用资金预算,且首笔开仓数量函数可能主动超过配置金额。
|
||||
4. `OrderBook` 会覆盖同证券同方向的多笔订单,并可能反复撤销已终结的历史订单。
|
||||
|
||||
测试失败并非测试环境问题,而是直接命中了当前生产函数的确定性参数错误。
|
||||
|
||||
## 3. P0:致命或可能造成错误交易
|
||||
|
||||
### 3.1 IPO 入口启用后必然报错,全部核心测试无法执行
|
||||
|
||||
位置:
|
||||
|
||||
- `py-client/strategy/ipo/boot.py:22-29`
|
||||
- `py-client/libs/calc.py:5-7`
|
||||
|
||||
`AutoBuyIpo(now)` 调用 `trading_time()` 时没有传入必需的 `now` 参数。只要 `enable_auto_ipo=True`,函数便在创建客户端、读取候选和检查重复委托之前抛出 `TypeError`。
|
||||
|
||||
复现结果:
|
||||
|
||||
- `test_local_record_prevents_duplicate_after_restart`:ERROR
|
||||
- `test_broker_order_prevents_duplicate`:ERROR
|
||||
- `test_one_rejection_does_not_stop_other_candidates`:ERROR
|
||||
|
||||
影响:自动打新完全不可用;如果由主程序同步调用,异常还可能中止启动流程。
|
||||
|
||||
建议:传入 `now or datetime.now()`,并将交易日与交易时段判断统一成一个可测试入口。修复后必须重新运行现有 4 项 IPO 测试。
|
||||
|
||||
### 3.2 IPO 路径还有两层确定性错误:字符串路径相除、锁语义反向
|
||||
|
||||
位置:
|
||||
|
||||
- `py-client/strategy/ipo/boot.py:37-49`
|
||||
- `py-client/config/__init__.py:28,109`
|
||||
- `py-client/libs/lockfile.py:9-18`
|
||||
|
||||
即使修复 3.1,代码仍执行:
|
||||
|
||||
```python
|
||||
Path(config.global_config.qmt_data_dir / f"{stock}.lock")
|
||||
```
|
||||
|
||||
`qmt_data_dir` 在 `GlobalConfig` 中是 `str`,因此先计算 `str / str`,会再次抛出 `TypeError`。此外,`is_lock()` 的语义是“锁文件已存在”,当前代码却仅在其返回 `True` 时提交申购;首次运行没有锁文件,所有候选都会被跳过。正确业务语义应是“没有本地成功记录时才继续”,并且还要和券商订单/成交记录交叉核对。
|
||||
|
||||
影响:修复第一个异常后,IPO 仍无法正常首申购;若人工预建锁文件绕过判断,则反而允许对已锁定证券再次申购。
|
||||
|
||||
建议:使用 `Path(qmt_data_dir) / f"{stock}.lock"`;反转本地锁判断;提交成功后才写锁;提交失败不得写锁;恢复测试中已经表达但当前实现缺失的券商订单和成交去重。
|
||||
|
||||
### 3.3 `UNKNOWN` 只被保存在状态文件中,没有真正阻止再次开仓
|
||||
|
||||
位置:
|
||||
|
||||
- `py-client/strategy/trend/state.py:76-82,175-228`
|
||||
- `py-client/strategy/trend/boot.py:136-164`
|
||||
- `py-client/strategy/trend/open.py:17-70`
|
||||
|
||||
`State.delete()` 会静默保留 `ING/UNKNOWN`,但 `RunOnce()` 构造 `allow_open` 时只排除真实持仓代码,不排除 `State` 中的未决代码。`open_signal()` 也只检查 `OrderBook.busy()`。当重启对账无法找到真实活动委托并把订单转成 `UNKNOWN` 后,如果券商订单查询暂时缺失或委托已终结但结果不明,`OrderBook` 没有活动锁,下一次同证券信号仍可提交新买单。
|
||||
|
||||
另外,`RunOnce()` 无论 `State.delete()` 是否因保护而拒绝删除,都会执行 `open_watch.forget()` 和 `add_watch.forget()`,形成“状态保留但观察锁被清除”的不一致状态。
|
||||
|
||||
影响:前一笔订单结果无法确认时仍可能重复开仓;这与状态修复目标和验收标准直接冲突。
|
||||
|
||||
建议:开仓候选必须同时排除持仓代码、`ING/UNKNOWN` 状态代码以及活动买单代码。`delete()` 应返回是否实际删除,调用方只在删除成功后清理观察器。运行期每轮应将订单簿刷新结果用于状态对账,而不是仅在启动时对账一次。
|
||||
|
||||
|
||||
## 4. P1:重要业务逻辑错误
|
||||
|
||||
### 4.1 IPO 定时任务实际上不会在 10:00 被调度
|
||||
|
||||
位置:`py-client/main.py:111-115`
|
||||
|
||||
代码注册每日 10:00 任务后只调用一次 `schedule.run_pending()`,随后进入 `StartTrend()` 的永久循环,再也没有机会驱动调度器。除非程序恰好在任务已到期的极窄窗口启动,否则自动打新不会执行。
|
||||
|
||||
建议:把调度轮询合并进主循环,或使用独立受控线程/独立进程;IPO 异常必须隔离,不能终止趋势策略。
|
||||
|
||||
### 4.2 样例账户没有启用任何趋势信号
|
||||
|
||||
位置:
|
||||
|
||||
- `py-client/config/__init__.py:48`
|
||||
- `py-client/etc/dev.yaml`
|
||||
- `py-client/libs/signal.py:23-28`
|
||||
|
||||
`signal_allow` 默认空列表,而当前 `dev.yaml` 没有该字段。`init_signals()` 只加载显式出现在允许列表中的信号,因此样例配置启动后趋势策略永远没有开仓候选,日志也不会提示“允许列表为空”。
|
||||
|
||||
建议:明确选择一种语义:空列表表示全部禁用,并在启动时显著告警;或空列表表示允许全部。样例配置应与预期业务一致。
|
||||
|
||||
### 4.3 同证券同方向的多笔委托会互相覆盖
|
||||
|
||||
位置:`py-client/strategy/trend/order.py:68-77,118-122`
|
||||
|
||||
订单缓存以 `BUY-code` / `SELL-code` 为唯一键。券商返回同一证券同方向的多笔委托时,字典推导只保留最后一笔。过期撤单、状态展示和锁判断无法看到被覆盖的订单。
|
||||
|
||||
建议:以系统委托号作为主键,另建 `(side, code) -> set[system_order_id]` 活动索引。
|
||||
|
||||
### 4.4 过期撤单没有过滤活动状态
|
||||
|
||||
位置:`py-client/strategy/trend/order.py:79-90`
|
||||
|
||||
`cancel_expired()` 遍历 `data.values()` 时只检查时间和系统委托号,没有要求 `order.status in BUSY_STATUSES`。如果接口返回历史委托,程序每轮都可能对已成交、已撤销或已失败订单调用撤单。
|
||||
|
||||
建议:只对活动状态且超过时限的订单撤单,并维护撤单请求中的冷却状态,避免每 30 秒重复请求。
|
||||
|
||||
### 4.5 状态落盘失败后的故障策略不一致
|
||||
|
||||
位置:
|
||||
|
||||
- `py-client/strategy/trend/open.py:59-70`
|
||||
- `py-client/strategy/trend/positions.py:175-183`
|
||||
|
||||
开仓提交成功但保存失败时只记录日志,仍清除观察器并继续运行;补仓保存失败则异常向外传播,但真实订单已经提交。两条路径都可能出现“真实订单存在、本地恢复信息缺失”,处理策略却不一致。
|
||||
|
||||
建议:真实下单成功后若持久化失败,应将证券置于内存级熔断集合,保留订单簿锁并持续重试落盘;禁止该证券继续自动交易,直至对账确认。
|
||||
|
||||
### 4.6 启动脚本会杀死机器上的所有 Python 进程
|
||||
|
||||
位置:`run.bat:1-7`
|
||||
|
||||
`taskkill /IM python.exe /F` 不限定当前项目、PID 或命令行,会强制终止机器上所有 Python 工作负载。脚本也没有先切换到自身目录,`python main.py` 是否能找到文件取决于调用时当前目录;仓库根目录本身并不存在 `main.py`。
|
||||
|
||||
影响:可能中断无关服务、研究任务或其他交易程序,同时自身仍可能因工作目录错误无法启动。
|
||||
|
||||
建议:保存本项目 PID 并只终止该 PID;脚本开头使用 `cd /d "%~dp0py-client"` 或调用绝对脚本路径;不要在生产启动流程中无条件 `git pull`。
|
||||
|
||||
## 5. P2:代码冗余、失效代码和过度验证
|
||||
|
||||
### 5.1 API 服务文件完全重复且正式入口不明确
|
||||
|
||||
位置:
|
||||
|
||||
- `api/qmt_api_new.py`
|
||||
- `api/qmt_api_rele.py`
|
||||
- 工作区状态中的已删除 `api/QMT_API.py`
|
||||
|
||||
两个现存文件 SHA-256 完全一致,属于逐字节重复;原正式命名文件当前又处于删除状态。部署人员无法从启动脚本或文档可靠判断应加载哪个版本,后续修复也容易只改到其中一份。
|
||||
|
||||
建议:保留唯一正式入口,开发版本通过 Git 分支管理,不以 `_new`、`_rele` 复制整文件。
|
||||
|
||||
### 5.2 IPO 模块呈现“旧实现覆盖新设计”的死代码特征
|
||||
|
||||
位置:`py-client/strategy/ipo/boot.py:5-19`
|
||||
|
||||
`json`、`Any`、`IPO_STRATEGY_NAME`、`IPO_REMARKS`、`IPO_SESSIONS`、`TRADING_CALENDAR_SYMBOL` 均未使用;测试则期待交易日查询、券商对账、异常隔离、上下文管理器和返回成功数量,但当前函数都未实现。这不是单纯格式问题,而是实现与测试/设计发生大段脱节的信号。
|
||||
|
||||
建议:不要逐个删除常量掩盖问题;先恢复完整 IPO 流程,再移除确认无用的符号。
|
||||
|
||||
### 5.3 主程序存在不可达代码、拼写错误和未使用符号
|
||||
|
||||
位置:
|
||||
|
||||
- `py-client/main.py:5,15,100-101,115-116`
|
||||
|
||||
`StartTrend()` 正常情况下永久循环,因此其后的“策略启动成功”日志不可达;即便未来返回,使用的 `config.account_config.strateg` 也不存在。非 Windows 分支调用未定义的 `log.error`。`TimedRotatingFileHandler` 和 `GLOBAL_CONFIG_PATH` 没有使用。
|
||||
|
||||
建议:启动成功日志应放在进入循环前;统一使用 `logging`;清除未使用导入和常量。
|
||||
|
||||
### 5.4 `State.delete()` 的全局保护属于过度且不透明的验证
|
||||
|
||||
位置:`py-client/strategy/trend/state.py:76-82`
|
||||
|
||||
任何调用者请求删除 `ING/UNKNOWN` 状态都会被静默拒绝,没有返回值、日志、强制删除入口或订单证据参数。它确实避免了一类误删,但也会阻止人工确认后的清理,并使调用者误以为删除成功。当前 `RunOnce()` 正因此错误清除了观察器。
|
||||
|
||||
建议:将“是否可清理”的业务判断放在显式对账流程中;`delete()` 返回布尔值或抛出明确异常;如需保护,提供带审计原因的显式强制路径。验证应基于订单证据,不应仅基于状态字符串。
|
||||
|
||||
### 5.5 对账存在重复落盘
|
||||
|
||||
位置:`py-client/strategy/trend/state.py:84-108,110-137`
|
||||
|
||||
`reconcile()` 先调用会自行 `save()` 的 `sync_positions()`,完成订单对账和清理后又 `save()`。每次启动对账至少写两次同一状态文件,第一份还是未完成订单对账的中间状态。
|
||||
|
||||
建议:为 `sync_positions()` 增加不立即保存的内部版本,整个对账事务只在最终一致状态落盘一次。
|
||||
|
||||
## 6. 建议整改顺序
|
||||
|
||||
1. 先恢复 IPO 的可执行性,并让现有 4 项 IPO 测试全部通过。
|
||||
2. 完成 `UNKNOWN/ING` 与开仓候选、订单簿之间的统一锁定,增加重启未成交端到端测试。
|
||||
3. 修复开仓数量和同轮资金预留,增加高价股、多信号资金边界测试。
|
||||
4. 重构 `OrderBook` 主键与活动索引,并限制撤单状态。
|
||||
5. 修复调度生命周期和启动脚本的进程范围。
|
||||
6. 最后清理重复 API 文件、死代码、重复落盘和无效日志。
|
||||
|
||||
## 7. 验收门槛
|
||||
|
||||
- 全部 11 项现有测试通过,且不通过跳过/删除失败测试达成。
|
||||
- 新增:IPO 首次申购、重复启动、券商已有委托、单只拒单不影响其他候选。
|
||||
- 新增:开仓未成交重启、订单查询暂时缺失、部分成交活动、部分成交撤单、完全成交。
|
||||
- 新增:`buy_value` 不足一手、多信号总金额超过现金、价格滑点场景。
|
||||
- 新增:同证券同方向两笔活动委托都能被发现和撤销。
|
||||
- 在模拟账户完成至少一次完整的下单、部分成交、撤单、重启对账闭环后,再考虑实盘。
|
||||
176
docs/CODE_REAUDIT_2026-08-29_V2.md
Normal file
176
docs/CODE_REAUDIT_2026-08-29_V2.md
Normal file
@@ -0,0 +1,176 @@
|
||||
# big-qmt 手动修改后代码复审(2026-08-29 V2)
|
||||
|
||||
## 1. 范围与验证
|
||||
|
||||
- 基于当前未提交工作区重新审计,不直接继承上一版报告结论。
|
||||
- 审计范围:`api/`、`py-client/`、启动脚本、配置和测试。
|
||||
- 明确排除:`docs/todo.md`;未读取、未引用其内容。
|
||||
- `python -B -m unittest discover -s tests -v`:11 项中 7 项通过、4 项错误。
|
||||
- 全部 Python 文件 AST 解析通过。
|
||||
- 未连接真实 QMT,未执行真实申购、买卖、撤单和成交回报测试。
|
||||
|
||||
## 2. 总体结论
|
||||
|
||||
当前版本仍不建议直接实盘运行。
|
||||
|
||||
手动修改已经修复了 IPO 路径中的三个表面问题:`trading_time()` 现在传入时间、数据目录能够正确转成 `Path`、首次申购的本地锁判断方向已经改正;APScheduler 也能在趋势策略永久循环之外每日 10:00 触发任务。
|
||||
|
||||
但核心安全闭环仍未完成:IPO 会在没有确认券商受理的情况下写入“已申购”锁,也没有使用券商委托/成交记录防重;趋势策略的 `UNKNOWN` 状态仍不能阻止再次开仓;开仓资金仍可能超预算。测试失败数量还从上一轮的 3 项变成了 4 项。
|
||||
|
||||
## 3. P0:致命或可能导致错误交易
|
||||
|
||||
### 3.1 IPO 不校验下单结果就写入成功锁,可能永久漏申购
|
||||
|
||||
位置:`py-client/strategy/ipo/boot.py:37-51`
|
||||
|
||||
`client.passorder()` 的返回值被完全忽略,随后无条件执行 `write_lockfile(lp)`。如果服务端以正常 HTTP 响应返回 `{"status": "failed"}`、空订单号或其他业务拒绝结果,本地仍会记录为已申购,后续每天都会跳过该证券。
|
||||
|
||||
影响:券商实际没有接受订单,但本地永久认为已经提交,造成漏申购。
|
||||
|
||||
建议:统一校验 `status == "success"` 且 `order_ref` 有效;只有明确受理后才能写锁。未知响应应告警并保持可对账状态,不能直接标记成功。
|
||||
|
||||
|
||||
|
||||
### 3.3 单只 IPO 异常会中止当天全部后续候选
|
||||
|
||||
位置:`py-client/strategy/ipo/boot.py:37-51`
|
||||
|
||||
候选循环内部没有单只证券级异常隔离。第一只证券的字段缺失、下单拒绝抛异常或锁文件写入失败,都会直接退出 `AutoBuyIpo()`;后面的候选不会再尝试。APScheduler 会记录任务异常,但当天 10:00 不会自动重新执行整个任务。
|
||||
|
||||
影响:一只异常证券导致当天其他所有新股漏申购。
|
||||
|
||||
建议:每只候选独立 `try/except` 并记录证券代码;失败继续处理下一只。任务结束后汇总成功、跳过、失败数量。
|
||||
|
||||
### 3.4 趋势策略 `UNKNOWN` 状态仍不能阻止自动重复开仓
|
||||
|
||||
位置:
|
||||
|
||||
- `py-client/strategy/trend/state.py:83-89,184-237`
|
||||
- `py-client/strategy/trend/boot.py:145-164`
|
||||
- `py-client/strategy/trend/open.py:17-70`
|
||||
|
||||
`State.delete()` 会保留 `ING/UNKNOWN`,但 `RunOnce()` 生成开仓候选时只排除真实持仓;`open_signal()` 只检查 `OrderBook.busy()`。如果启动对账将订单标记成 `UNKNOWN`,同时券商活动委托列表暂时没有该订单,状态文件虽然存在,下一轮信号仍可再次下单。
|
||||
|
||||
此外,运行期调用 `delete()` 后不检查是否真的删除,就清除两个观察器,造成未决状态与观察状态不一致。
|
||||
|
||||
影响:订单结果无法确认时可能重复买入,未达到“UNKNOWN 禁止自动重复下单”的目标。
|
||||
|
||||
建议:开仓候选同时排除 `ING/UNKNOWN` 状态代码和活动买单代码;`delete()` 返回实际删除结果;每轮用最新订单簿重新对账状态。
|
||||
|
||||
|
||||
|
||||
## 4. P1:重要业务逻辑问题
|
||||
|
||||
### 4.1 IPO 函数契约退化,现有 4 项测试全部报错
|
||||
|
||||
位置:
|
||||
|
||||
- `py-client/strategy/ipo/boot.py:22`
|
||||
- `py-client/tests/test_ipo.py:52-136`
|
||||
|
||||
`AutoBuyIpo(now: datetime | None = None)` 被改成无参数函数,测试无法注入确定时间,4 项测试全部以 `TypeError: AutoBuyIpo() takes 0 positional arguments but 1 was given` 结束。APScheduler 并不要求删除可选参数;保留可选 `now` 同样可以无参调度。
|
||||
|
||||
测试当前也明确要求但实现未满足:返回成功数量、关闭客户端、本地重启防重、券商记录防重、单只拒绝不影响其他候选。
|
||||
|
||||
建议:恢复可选 `now` 参数,内部使用 `current = now or datetime.now()`;不要修改测试来掩盖业务契约缺失。
|
||||
|
||||
|
||||
### 4.3 IPO 客户端从不关闭
|
||||
|
||||
位置:`py-client/strategy/ipo/boot.py:31-51`
|
||||
|
||||
每日任务创建新的 `httpx.Client`,成功、跳过和异常路径都没有调用 `close()`。长期运行会累积未及时释放的连接池资源。
|
||||
|
||||
建议:使用 `with Client(...) as client:`,现有 `Client` 已实现上下文管理器。
|
||||
|
||||
### 4.4 IPO 返回类型与真实返回值不一致
|
||||
|
||||
位置:`py-client/strategy/ipo/boot.py:22-51`
|
||||
|
||||
函数标注返回 `int`,只有禁用和非交易时间返回 0;正常处理完候选后隐式返回 `None`,也没有统计提交成功数量。
|
||||
|
||||
影响:日志、监控和测试无法知道任务到底提交了多少只证券。
|
||||
|
||||
建议:维护 `success_count` 并在所有出口返回整数。
|
||||
|
||||
### 4.5 同证券同方向的多笔趋势委托仍会互相覆盖
|
||||
|
||||
位置:`py-client/strategy/trend/order.py:70-79,121-125`
|
||||
|
||||
订单缓存仍以 `BUY-code` / `SELL-code` 为唯一键。同证券同方向多笔委托只保留最后一笔,其他活动订单无法撤销或对账。
|
||||
|
||||
建议:系统委托号作为主键,方向与证券组合作为一对多活动索引。
|
||||
|
||||
### 4.6 过期撤单仍会处理已终结历史订单
|
||||
|
||||
位置:`py-client/strategy/trend/order.py:81-93`
|
||||
|
||||
撤单条件没有检查 `order.status in BUSY_STATUSES`。接口若返回历史订单,程序会对已成交、已撤、已失败订单反复发送撤单请求。
|
||||
|
||||
建议:仅处理活动状态,并为已发送撤单请求增加冷却或本地状态。
|
||||
|
||||
### 4.7 下单成功但状态落盘失败的安全策略不统一
|
||||
|
||||
位置:
|
||||
|
||||
- `py-client/strategy/trend/open.py:59-70`
|
||||
- `py-client/strategy/trend/positions.py:175-183`
|
||||
|
||||
开仓保存失败只记日志并继续;补仓保存失败则异常退出本轮。两者都可能已经存在真实订单,却缺少可恢复的本地状态。
|
||||
|
||||
建议:落盘失败后将证券加入内存熔断集合、保留订单锁并持续重试;在完成真实订单对账前禁止该证券继续自动交易。
|
||||
|
||||
|
||||
### 5.3 主程序仍有不可达代码、拼写错误和无效符号
|
||||
|
||||
位置:
|
||||
|
||||
- `py-client/main.py:5,15,100-101,127-128`
|
||||
|
||||
`StartTrend()` 正常情况下永久循环,其后的日志不可达;即便返回,`config.account_config.strateg` 也不存在。非 Windows 分支使用未定义的 `log.error`。`TimedRotatingFileHandler`、`GLOBAL_CONFIG_PATH` 未使用。
|
||||
|
||||
建议:启动成功日志放在进入永久循环前;统一使用 `logging`;删除无效导入和常量。
|
||||
|
||||
### 5.4 `State.delete()` 的保护过宽且静默
|
||||
|
||||
位置:`py-client/strategy/trend/state.py:83-89`
|
||||
|
||||
任何 `ING/UNKNOWN` 都会让删除静默失效,没有返回值、日志、订单证据或人工强制清理入口。这是基于状态字符串的全局拦截,不是真实订单对账,并已造成调用方误清观察器。
|
||||
|
||||
建议:把清理许可放入显式对账决策;`delete()` 返回布尔值或明确拒绝原因;人工确认终结后应有可审计的清理路径。
|
||||
|
||||
### 5.5 状态对账重复写盘并暴露中间状态
|
||||
|
||||
位置:`py-client/strategy/trend/state.py:91-117,119-146`
|
||||
|
||||
`reconcile()` 调用会自行保存的 `sync_positions()`,完成订单对账后再次保存。一次启动对账至少写盘两次,第一次还是未完成订单状态恢复的中间结果。
|
||||
|
||||
建议:内部同步只改内存,完整对账结束后一次性原子落盘。
|
||||
|
||||
### 5.6 调度器配置包含当前进程内不必要的重复任务替换
|
||||
|
||||
位置:`py-client/main.py:116-123`
|
||||
|
||||
调度器每次启动都是新实例,只添加一次固定 ID 任务,因此 `replace_existing=True` 在当前结构下没有实际作用。它无害,但属于多余防御参数,容易让人误以为使用了持久化任务仓库或存在重复注册路径。
|
||||
|
||||
建议:若没有持久化 job store 或重复注册,删除该参数;若未来启用持久化,再保留并补充任务版本策略。
|
||||
|
||||
## 6. 本轮已确认改善
|
||||
|
||||
- APScheduler 后台调度不会被 `StartTrend()` 的永久循环阻塞。
|
||||
- `timezone="Asia/Shanghai"` 明确了每日 10:00 的业务时区。
|
||||
- `coalesce=True` 和 `max_instances=1` 能防止任务积压补跑和并发重叠。
|
||||
- `dev.yaml` 已显式配置 `signal_allow`,趋势信号不再因默认空列表而全部禁用。
|
||||
- IPO 本地路径拼接和首次锁判断方向已经修正。
|
||||
- 趋势部分成交判定的四类状态逻辑仍保留。
|
||||
|
||||
## 7. 建议整改顺序与验收门槛
|
||||
|
||||
1. 恢复 IPO 可选时间参数、成功计数和上下文管理器,让现有 4 项 IPO 测试先全部通过。
|
||||
2. 增加真实交易日、券商委托/成交防重、下单结果校验和单只异常隔离。
|
||||
3. 让 `ING/UNKNOWN` 真正参与趋势开仓过滤,并补充重启未成交端到端测试。
|
||||
4. 修复单笔与同轮开仓资金预算。
|
||||
5. 重构订单簿一对多索引并限制撤单状态。
|
||||
6. 清理启动脚本、重复 API 文件和死代码。
|
||||
|
||||
最低验收要求:现有 11 项测试全部通过;新增 IPO 返回失败但不写锁、锁丢失但券商已有订单、第一只拒绝而第二只成功、节假日不申购;新增趋势 `UNKNOWN` 无持仓且无活动委托时仍不重复开仓;最后在模拟账户完成下单、部分成交、撤单、重启对账闭环。
|
||||
249
docs/CODE_REAUDIT_2026-08-29_V3.md
Normal file
249
docs/CODE_REAUDIT_2026-08-29_V3.md
Normal file
@@ -0,0 +1,249 @@
|
||||
# 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 委托不重复申购”;最后在模拟账户完成买入、部分成交、撤单、卖出、重启对账闭环。
|
||||
82
docs/todo.md
82
docs/todo.md
@@ -1,5 +1,87 @@
|
||||
|
||||
|
||||
### 4.2 IPO 没有校验真实交易日,只判断周一至周五和盘中时间
|
||||
|
||||
位置:
|
||||
|
||||
- `py-client/strategy/ipo/boot.py:27`
|
||||
- `py-client/libs/calc.py:5-7`
|
||||
|
||||
`trading_time()` 只排除周末,法定节假日、临时休市仍会进入申购逻辑。模块中已有 `TRADING_CALENDAR_SYMBOL` 常量,但没有实际查询交易日接口。
|
||||
|
||||
建议:调用 SDK 的 `trading_dates()` 验证当天交易日;接口失败时按安全策略跳过,不应猜测为交易日。
|
||||
|
||||
### 4.8 启动脚本仍会强制终止机器上的所有 Python 进程
|
||||
|
||||
位置:`run.bat:1-7`
|
||||
|
||||
`taskkill /IM python.exe /F` 不区分项目和 PID;`python main.py` 又依赖调用时工作目录。它可能杀死无关任务后仍因找不到根目录下的 `main.py` 而启动失败。
|
||||
|
||||
建议:只管理本项目 PID;脚本先切换至 `%~dp0py-client`;生产启动不要无条件 `git pull`。
|
||||
|
||||
## 5. P2:代码冗余与过度验证
|
||||
|
||||
### 5.1 两份 API 文件逐字节重复,正式入口被删除
|
||||
|
||||
位置:
|
||||
|
||||
- `api/qmt_api_new.py`
|
||||
- `api/qmt_api_rele.py`
|
||||
- 当前处于删除状态的 `api/QMT_API.py`
|
||||
|
||||
两个新文件 SHA-256 相同,属于完整重复;原正式文件被删除,启动和部署入口不清晰。继续维护会造成修复只落在其中一份的风险。
|
||||
|
||||
建议:保留唯一正式文件,版本差异交给 Git 管理。
|
||||
|
||||
### 5.2 IPO 模块保留大量未使用的设计残留
|
||||
|
||||
位置:`py-client/strategy/ipo/boot.py:5-19`
|
||||
|
||||
`json`、`Any`、`IPO_STRATEGY_NAME`、`IPO_REMARKS`、`IPO_SESSIONS`、`TRADING_CALENDAR_SYMBOL` 当前均未使用。它们与测试要求一起表明完整对账实现被缩减,但残留符号没有同步整理。
|
||||
|
||||
建议:先恢复交易日、券商对账和统计逻辑,再清除确认无用的符号,不要只做表面删减。
|
||||
|
||||
### 3.5 开仓金额可能超过配置值,同轮多信号会重复使用现金
|
||||
|
||||
位置:
|
||||
|
||||
- `py-client/libs/calc.py:10-12`
|
||||
- `py-client/strategy/trend/boot.py:119-164`
|
||||
- `py-client/strategy/trend/open.py:41-70`
|
||||
|
||||
`calc_buy_volume()` 在预算不足一手时仍强制返回 100 股。趋势开仓仅检查一次现金比例,没有校验单笔预计金额,也没有为同一轮后续信号扣减已提交订单占用的资金。
|
||||
|
||||
影响:高价股单笔超出 `buy_value`;多信号集中出现时可能发送总额超过可用资金的委托。
|
||||
|
||||
建议:不足一手返回 0;开仓入口维护本轮共享 `remaining_cash`,成功提交后立即预留资金,并考虑价格滑点。
|
||||
|
||||
### 3.2 IPO 仅依赖本地空文件防重,锁丢失或落盘失败会重复申购
|
||||
|
||||
位置:
|
||||
|
||||
- `py-client/strategy/ipo/boot.py:37-51`
|
||||
- `py-client/libs/lockfile.py:9-18`
|
||||
|
||||
当前实现没有查询 `trade_detail_data("order")` 或 `deals()`,也没有按账户、交易日和申购备注对账。本地锁文件被清理、数据目录切换、写入失败或下单成功后进程崩溃时,下一次任务会再次提交同一新股。
|
||||
|
||||
影响:产生重复申购请求;实际结果取决于券商拦截,不能把安全性寄托在券商拒单上。
|
||||
|
||||
建议:本地记录只能作为快速幂等缓存,最终防重必须以“账户 + 交易日 + 证券代码”的券商委托/成交记录为准;下单前后二次核对。
|
||||
|
||||
### 3.4 开仓可能超过单笔配置金额,多信号还会重复使用同一份现金
|
||||
|
||||
位置:
|
||||
|
||||
- `py-client/libs/calc.py:10-12`
|
||||
- `py-client/strategy/trend/boot.py:119-164`
|
||||
- `py-client/strategy/trend/open.py:41-70`
|
||||
|
||||
`calc_buy_volume()` 使用 `max(1, floor(...)) * 100`。当 `buy_value < price * 100` 时,它不是返回 0,而是强制购买 100 股,实际金额必然超过 `buy_value`。趋势开仓只检查一次总账户现金比例,没有检查单笔预计金额是否小于可用现金,也没有在同一轮多个信号之间扣减已预留资金。
|
||||
|
||||
影响:高价股或同轮多信号可能导致单笔超预算、连续发送超过可用资金的委托,产生券商拒单或非预期仓位。
|
||||
|
||||
建议:预算不足一手时返回 0;像补仓路径一样维护本轮 `remaining_cash`;每次成功提交后立即预留 `price * volume`,并给价格滑点留安全余量。
|
||||
|
||||
### 2.1 每次启动都会无条件执行新股申购
|
||||
|
||||
整改状态:已于 2026-08-29 按本节方案完成,新增 4 项专项测试。
|
||||
|
||||
Reference in New Issue
Block a user