This commit is contained in:
2026-09-05 11:46:31 +08:00
parent 3689a7ad86
commit 63ffc1329b
13 changed files with 229 additions and 731 deletions

109
docs/ipo-trend-audit.md Normal file
View File

@@ -0,0 +1,109 @@
# IPO / Trend 当前代码审计
日期2026-09-05。范围`py-client/strategy/{ipo,trend}`,以及直接依赖的 SDK、公共库、配置与 `api/qmt_rest_new.py` 下单/撤单处理器。以下路径除特别注明外均相对于 `py-client/`
本报告取代上一轮 Trend 审计的当前结论。仅清理测试文件、更新文档;未实施策略修复。采用静态阅读与隔离的内存断言,不连接柜台、不提交真实订单。风险等级按状态丢失、重复委托、漏单等后果划分;未验证真实 QMT 的返回值与回报时序。
## IPO高风险
### I1. 未确认受理就写入永久申购标记 (忽略)
位置:`strategy/ipo/boot.py:53``libs/lockfile.py:14`;服务端 `api/qmt_rest_new.py:246`
`client.passorder()` 返回值被忽略,随后无条件写锁、记录申购日志。服务端只要底层调用未抛异常就返回 success并直接把返回值字符串化未验证引用是否有效。因此本地标记只能证明调用返回不能证明委托有效。遇到失败业务响应或无效引用后续轮次仍因锁文件存在而跳过导致漏申购。
建议:依据明确的柜台受理契约验证结果;无法确认的结果保留待核查记录,按委托/成交查询确认后再标记完成。
### I2. 检查锁、下单、写锁不是原子操作,且没有券商对账(忽略)
位置:`strategy/ipo/boot.py:49``libs/lockfile.py:9`
两个调用可同时看到锁不存在并分别提交;下单已受理但响应超时或写锁失败,也不会留下标记,下轮再次提交。函数没有查询已有券商委托,也没有传入稳定的本地订单标识。重复请求是否由柜台拒绝不在本次静态审计可证明范围内。
建议:按账户与发行事件生成幂等标识,提交前原子占位;不确定结果先查询再决定是否重试。
### I3. 锁文件未隔离账户和发行事件(忽略)
位置:`strategy/ipo/boot.py:49`
路径仅为全局数据目录下的 `{stock}.lock`,既没有账户也没有日期/发行标识。多个账户共享目录时,先运行的账户会阻止另一账户申购;代码复用或残留标记也无法区分新的发行事件。
建议:至少加入账户与发行事件标识;不要仅依靠无期限的证券代码标记。
## IPO中风险
### I4. 证券代码和数值校验不完整(忽略)
位置:`strategy/ipo/boot.py:40`
`str(None)` 会变成非空的 `None`;价格 `NaN`/正无穷不会被 `<= 0` 拦截;`int(100.9)` 会截断为 100。证券代码还直接用于文件名路径字符可导致越界路径或非法文件名。单条异常隔离能保住后续候选但不能保证本条参数正确。
建议:证券代码采用明确格式;价格必须为有限正数,数量必须为正整数且满足实际申购规则;构造路径前排除路径分隔符。
已确认有效IPO 按 `list[dict]` 逐条读取Client 使用上下文关闭;候选级异常不会终止后续候选。函数实际返回 `None`,文档中“返回成功数量”和“券商对账”的说明与实现不一致。
## Trend严重风险
### T1. 快照缺席或撤单过滤会不可逆地丢失未决状态
位置:`strategy/trend/order.py:60``strategy/trend/state.py:144``strategy/trend/boot.py:176`
`refresh()` 覆盖 `data`;短暂空快照没有保留本地 pending。更直接的触发是超时订单调用撤单后立即 `continue`,无论撤单业务结果如何,都不再进入对账数据。`reconcile()` 找不到订单即把 ING 清为空字符串,无持仓还会删除记录;之后只处理 ING 的逻辑无法回写迟到的成交。
`busy_keys` 已保留这些活动订单,能阻止同方向再次提交,但它没有传给状态对账,不能解决此问题。建议保留待确认状态,并将完整订单快照与展示列表分开;只有明确终态才能结束对账。
## Trend高风险
### T2. 部分成交与取消拆单被过滤成全部完成
位置:`strategy/trend/order.py:69``strategy/trend/state.py:153``strategy/trend/state.py:213`
同一本地订单包含完成子单和取消子单时,取消子单被过滤;剩余全是 56状态变为 OK并增加补仓次数。建议用完整快照判定终态单独累计实际成交部分成交的记账与是否完整完成应分别处理。
### T3. 下单受理后、状态落盘前崩溃会丢失订单关联
位置:`strategy/trend/open.py:89``strategy/trend/positions.py:181`
先调用 place 成功,再保存状态。响应丢失、进程退出或写盘失败可能使底仓/补仓缺少对应记录。重启后即使券商活动订单暂时阻止重复提交,补仓次数与成交关联仍可能无法恢复。建议提交前持久化意图,保留可核查订单标识;不要求必须采用 SQLite。
### T4. 开仓没有账户余额预算,且与补仓并发 (忽略)
位置:`strategy/trend/boot.py:185``strategy/trend/open.py:49``strategy/trend/positions.py:42`
开仓循环每次使用完整 buy_value没有扣减余额补仓线程独立使用全部可用资金。多个候选或开仓与补仓同时触发时计划金额可超出账户资金。建议统一预留预算受理失败时按确定性结果释放。
### T5. Trend 会撤销账户内其他来源的超时订单 (忽略)
位置:`strategy/trend/boot.py:57``strategy/trend/order.py:67`
组合接口返回账户订单refresh 对全部满足状态和时间条件的订单调用 cancel_by_id没有检查本地订单前缀或策略归属。若该账户同时有手工、IPO 或其他策略的可撤委托,也会被处理。建议防重可以参考全账户委托,但自动撤单只作用于明确归属本策略的订单。
## Trend中风险及需要确认的行为
- **T6 异常隔离不足(已处理)**:下单入口现捕获 APIError、httpx.RequestError 和 SDK 解码抛出的 ValueError记录异常后返回 False结果不确定时保留缓存防重不自动重试。持仓循环增加逐证券 Exception 边界,记录证券代码和堆栈后继续下一只。已通过隔离断言验证连接错误、读取超时、解码错误,以及首只证券异常后第二只仍被处理;未新增 tests 文件。
- **T7 初始化资源释放(已处理)**StartTrend 在 Client 创建后使用统一 try/finally线程池单独持有引用退出时先等待已创建的线程池结束再关闭 Client即使线程池关闭抛异常内层 finally 仍关闭 Client。已通过 7 个隔离场景验证组合查询、撤单刷新、状态读取、信号加载、Runtime 构建失败,以及正常退出和线程池关闭异常。未连接柜台或新增测试文件。
- **T8 排除名单只约束持仓管理(已处理)**excluded_codes 同时禁止新开仓open_signal 在每条信号处理前检查并记录跳过原因,与持仓管理一致。已通过隔离验证:高于昨收直接开仓、反弹确认开仓两条路径均跳过排除证券,后续允许证券仍正常处理。未新增测试文件或调用真实交易接口。
- **T9 成交价缺失时均价可能被低估 (忽略)**`state.py:225` 对所有成交计入数量,却只对有金额/价格的记录计入金额;例如两笔各 100 股,只有一笔有 1000 元金额,会得到 5 元均价。应标记数据不完整并补查,不把未知金额当作零。当前交易决策主要使用券商 position.open_price本项直接影响本地记录。
- **T10 峰值止盈有门槛限制且不持久化 (忽略)**`positions.py:105` 在低于最低利润时直接返回。已达峰值后跳跌到门槛以下不会触发回撤卖出;进程重启也丢失历史峰值。若目标是“激活后持续追踪”,需保留激活状态和峰值;若门槛是最低可接受卖价,应明确记录这一取舍。
- **T11 信号仅启动读取 (忽略)**`boot.py:67` 不在循环内刷新;首次读取失败会使该信号整次运行缺席,后续更新不会生效。建议按业务需要定时刷新。
## 当前保留的风控设置
以下行为仍存在;按此前明确跳过的决定列为保留风险,不视为本次自动修复授权:
- `libs/market.py:34` 始终允许开仓/补仓,市场失败或下跌不阻断。
- `libs/calc.py:10` 不足一手预算仍返回 100 股,可超过单笔额度。
- `boot.py:147` 现金安全线只限制开仓,未扣除补仓安全储备。
- `positions.py:16` 固定亏损档位,最低盈利按股价计算,配置的 loss_trigger_pct/min_profit_pct 不参与 Trend。
另:`boot.py:90` 当前仍为 `>= 15:00:00`,与此前“大于 15:00”约定不一致。IPO 与 Trend 使用的 trading_time 仅校验工作日和时段,不包含节假日日历;本次未扩展该功能。
## 已确认修复与验证边界
- Trend 的 busy() 和 place() 均检查 busy_keys + SimpleCache。集合在撤单前收集故请求撤单仍保留防重refresh 不会续期 SimpleCache。
- Trend 调用现有 passorderSDK 参数对应一致。
- 未知信号配置通过 continue 跳过,不再解引用 None。
- 旧测试共 6 个 `.py` 文件按要求删除,可从 Git 恢复;不将旧测试失败统计当成本轮结果,也不重新建立 tests 文件。
- 本次只进行语法与隔离行为检查,未验证真实柜台受理、撤单终态和回报字段。服务端直接字符串化底层 passorder 返回值Trend 以有效 order_ref 判断成功;二者能否匹配真实 QMT 必须以实际回报核验,不能仅凭静态代码断言。
优先处理 T1/T2 对账数据问题、I1/I2 申购确认与幂等、T5 撤单归属,再评估资金预算与其他保留风险。