110 lines
9.6 KiB
Markdown
110 lines
9.6 KiB
Markdown
# 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 调用现有 passorder,SDK 参数对应一致。
|
||
- 未知信号配置通过 continue 跳过,不再解引用 None。
|
||
- 旧测试共 6 个 `.py` 文件按要求删除,可从 Git 恢复;不将旧测试失败统计当成本轮结果,也不重新建立 tests 文件。
|
||
- 本次只进行语法与隔离行为检查,未验证真实柜台受理、撤单终态和回报字段。服务端直接字符串化底层 passorder 返回值,Trend 以有效 order_ref 判断成功;二者能否匹配真实 QMT 必须以实际回报核验,不能仅凭静态代码断言。
|
||
|
||
优先处理 T1/T2 对账数据问题、I1/I2 申购确认与幂等、T5 撤单归属,再评估资金预算与其他保留风险。
|