Files
big-qmt/docs/trend-audit.md
2026-09-05 11:46:31 +08:00

9.1 KiB
Raw Permalink Blame History

Trend 策略代码审计报告

历史报告:当前 IPO / Trend 复核结果见 ipo-trend-audit.md。下述问题状态与测试统计不代表当前版本;尤其 busy_keys 防重已实现,旧测试文件已按要求清理。

审计日期2026-09-05
审计对象:py-client/strategy/trend 当前工作树版本。
关联范围:仅核对直接影响 Trend 行为的 sdklibsconfig 和 Trend 测试。
审计方法:重新读取当前代码,不沿用历史审计结论;本次只更新审计文档,不修改策略代码。

结论摘要

当前版本不建议直接用于无人值守实盘。发现 2 个严重问题、6 个高风险问题、6 个中风险问题和 2 个低风险/测试问题。最高优先级是本地防重与券商实际活动委托脱节,以及空订单快照会清除未决状态。另有 5 项 Trend 测试在目标断言前报错,当前测试结果不能为下单路径提供有效回归保障。

严重问题

S1. 防重只依赖进程内 TTL完全忽略券商活动委托

  • 位置:strategy/trend/order.py:45-48,80-92,97-104
  • 证据:refresh() 虽统计 busy_keys,但没有写入 SimpleCachebusy()place() 都只查询 busy_cache,没有检查 self.data。缓存也不会跨进程恢复。
  • 触发场景:程序重启后券商仍有活动订单;或本地 180 秒 TTL 到期但订单仍未终结。
  • 影响:同一证券、同一方向可能重复开仓、补仓或止盈。卖出路径可能再次按全部可用持仓提交委托。
  • 建议:busy()place() 在同一互斥区内同时检查 TTL 缓存及 self.data 中的 BUSY_STATUSES

S2. 一次空订单快照会不可逆地清除待成交状态

  • 位置:strategy/trend/order.py:57-92state.py:101-160
  • 证据:refresh() 每轮用当前券商结果覆盖 OrderBook.data。订单刚提交但暂未出现在快照时,reconcile() 会把对应 ING 改为空字符串;若尚无持仓,随后还会删除整个 StateItem
  • 影响:底仓状态可能丢失,补仓次数可能不增加;后续轮次可能重新开仓或重复使用同一亏损档位。
  • 建议:本地 pending 应保留至明确终态或确认超时;短暂缺席不能立即视为失败。

高风险问题

H1. 大盘风控被固定为允许开仓

  • 位置:libs/market.py:34-38;调用位置:strategy/trend/boot.py:153,185-188
  • 证据:market_allow_open() 无条件返回 True,不读取 _market_status
  • 影响:市场状态为下跌、未知或刷新失败时,策略仍可开仓和亏损补仓。
  • 测试证据:test_market.MarketCacheTests.test_refresh_failure_blocks_open 失败。
  • 建议:仅在缓存状态明确为 UP 时允许开仓;未知状态采用 fail-closed。

H2. 过滤异常订单后再对账,会把拆单结果判错

  • 位置:strategy/trend/order.py:62-65,80-92state.py:112-139,211-220
  • 证据:refresh() 丢弃取消、拒绝和异常订单,只把处理态及完成态写入 data。同一本地订单若一笔完成、一笔取消,传给 State 的只剩完成记录,_order_status() 会返回 OK
  • 影响:部分成交可能被视为完整成交;状态数量、成本和补仓次数与真实结果不一致。
  • 建议:展示/防重列表可以过滤,但状态对账必须使用完整原始订单快照。

H3. 柜台受理与状态落盘之间存在崩溃窗口

  • 位置:strategy/trend/open.py:78-98positions.py:174-193
  • 证据:开仓和补仓都先执行 orders.place(),成功返回后才写入并保存 StateItem
  • 影响:委托受理后若进程退出或状态写入失败,本地没有对应状态;重启后容易重复下单。
  • 建议:提交前持久化订单意图,提交后记录柜台结果;不确定结果保留为可对账状态。

H4. 开仓与补仓并发,资金预算彼此隔离

  • 位置:strategy/trend/boot.py:180-190open.py:15-74positions.py:28-91
  • 证据:持仓管理与开仓并发执行。补仓只在自身循环扣减余额;每个开仓信号均独立使用完整 buy_value,两条路径没有账户级资金预留。
  • 影响:同轮总委托金额可能超过真实可用资金,成交组合取决于柜台顺序。
  • 建议:统一生成订单意图,用账户级单一预算预留资金后再提交。

H5. 撤单结果未校验,活动订单却立即从跟踪列表删除

  • 位置:strategy/trend/order.py:67-78
  • 证据:超过 10 秒即调用 cancel_by_id(),不检查响应是否成功,随后无条件 continue
  • 影响:撤单失败或结果未知时,本地已不再跟踪仍有效的订单;结合 S1 可能重复提交。
  • 建议:仅在券商明确返回取消终态后移除;失败或不确定时继续保留活动状态。

H6. 单笔预算不足一手时仍强制买入 100 股

  • 位置:libs/calc.py:10-12;调用位置:strategy/trend/open.py:49positions.py:168
  • 证据:calc_buy_volume() 使用 max(1, floor(...)) * 100
  • 影响:buy_value < price * 100 时委托金额必然超过预算,并放大 H4。
  • 建议:不足一手时返回 0,由调用方记录并跳过。

中风险问题

M1. 现金安全线只限制新开仓,不限制亏损补仓

  • 位置:strategy/trend/boot.py:145-148,183-188positions.py:68-80
  • 证据:allow_open_by_cash 只控制 open_signal()manage_positions() 始终获得全部 assets.available
  • 影响:账户已低于最小现金比例时仍可能增加亏损仓位。
  • 建议:若安全线也约束补仓,应只传递扣除安全储备后的预算。

M2. 两个风控配置未参与 Trend 决策

  • 位置:config/__init__.py:48-50strategy/trend/positions.py:16,52-59,157-160
  • 证据:配置提供 loss_trigger_pctmin_profit_pct,但补仓使用固定 LOSS_TIERS,止盈门槛按股价区间硬编码。
  • 影响:修改配置不会改变实盘行为,运维人员可能误判实际参数。
  • 建议:让策略明确使用配置,或删除无效配置并输出最终生效参数。

M3. 非 APIError 下单异常会终止整批持仓管理

  • 位置:strategy/trend/order.py:105-116positions.py:38-91
  • 证据:OrderBook.place() 只捕获 APIErrormanage_positions() 没有逐持仓异常边界。
  • 影响:一只证券发生连接、超时等异常后,其余持仓当轮不再处理。
  • 建议:订单层捕获明确的传输异常,持仓循环增加逐证券隔离。

M4. 启动阶段异常不会可靠释放 HTTP Client

  • 位置:strategy/trend/boot.py:47-84
  • 证据Client 创建后的组合查询、撤单、状态读取和信号加载不在统一 try/finally 中。
  • 影响:初始化失败时连接池不能确定及时释放。
  • 建议:建立统一资源生命周期,在 finally 中关闭已创建资源。

M5. 信号只在启动时加载一次

  • 位置:strategy/trend/boot.py:67-71,88-110
  • 证据:init_signals() 位于永久循环外。
  • 影响:运行期间新增、撤销或修正的远程信号不会生效。
  • 建议:按业务时效定期刷新,或明确“启动快照整日有效”的约束。

M6. 明确拒单后仍保留 180 秒缓存锁

  • 位置:strategy/trend/order.py:97-126
  • 证据:柜台调用前设置缓存,但 API 明确失败、响应格式无效或抛出 APIError 时均不删除键。
  • 影响:可立即纠正或重试的订单被无条件抑制 180 秒,可能错过窗口。
  • 建议:明确未受理时释放缓存;结果不确定时保留保护并等待对账。

低风险与测试问题

L1. 结束时间条件与“大于 15:00”不一致

  • 位置:strategy/trend/boot.py:90-91
  • 证据:当前使用 >= (15, 0, 0),在恰好 15:00:00 时退出,而约定是大于 15:00 后退出。
  • 影响:边界秒行为与需求不一致。
  • 建议:使用严格大于比较,并让日志描述与条件一致。

L2. Trend 测试夹具已与生产接口漂移

  • 位置:tests/test_trend.py:19-42,291-352
  • 证据:生产代码调用 client.passorder(...),但测试替身只实现已删除的 passorder_latest_tagged();两个 RunOnce 夹具缺少采集任务需要的 account_id
  • 影响:核心下单和调度测试在目标断言前报错,无法验证真实调用契约。
  • 建议:测试替身实现当前 passorder 签名并补齐 account_id增加券商活动订单、TTL、空快照、撤单失败和并发预算测试。

回归结果

执行:cd py-client && python -B -m unittest tests.test_trend tests.test_market -v

结果17 项测试中 11 项通过、1 项失败、5 项错误。

  • 失败:市场状态刷新失败后仍允许开仓。
  • 错误3 项下单测试使用旧接口替身2 项 RunOnce 测试缺少 account_id

建议处理顺序

  1. S1、S2恢复可靠的订单防重和未决状态对账。
  2. H2、H3、H5确保订单生命周期、撤单和成交状态可信。
  3. H1、H4、H6、M1恢复风控并统一账户预算。
  4. M2、M3、M4、M5、M6处理配置、异常隔离、资源和刷新策略。
  5. 修复测试夹具并补齐跨轮、重启、空快照、撤单失败和并发预算回归。