2026-09-05 11:32:10 +08:00
|
|
|
|
# Trend 策略代码审计报告
|
|
|
|
|
|
|
2026-09-05 11:46:31 +08:00
|
|
|
|
> 历史报告:当前 IPO / Trend 复核结果见 [ipo-trend-audit.md](ipo-trend-audit.md)。下述问题状态与测试统计不代表当前版本;尤其 busy_keys 防重已实现,旧测试文件已按要求清理。
|
|
|
|
|
|
|
2026-09-05 11:32:10 +08:00
|
|
|
|
审计日期:2026-09-05
|
|
|
|
|
|
审计对象:`py-client/strategy/trend` 当前工作树版本。
|
|
|
|
|
|
关联范围:仅核对直接影响 Trend 行为的 `sdk`、`libs`、`config` 和 Trend 测试。
|
|
|
|
|
|
审计方法:重新读取当前代码,不沿用历史审计结论;本次只更新审计文档,不修改策略代码。
|
|
|
|
|
|
|
|
|
|
|
|
## 结论摘要
|
|
|
|
|
|
|
|
|
|
|
|
当前版本不建议直接用于无人值守实盘。发现 2 个严重问题、6 个高风险问题、6 个中风险问题和 2 个低风险/测试问题。最高优先级是本地防重与券商实际活动委托脱节,以及空订单快照会清除未决状态。另有 5 项 Trend 测试在目标断言前报错,当前测试结果不能为下单路径提供有效回归保障。
|
|
|
|
|
|
|
|
|
|
|
|
## 严重问题
|
|
|
|
|
|
|
|
|
|
|
|
### S1. 防重只依赖进程内 TTL,完全忽略券商活动委托
|
|
|
|
|
|
|
|
|
|
|
|
- 位置:`strategy/trend/order.py:45-48,80-92,97-104`
|
|
|
|
|
|
- 证据:`refresh()` 虽统计 `busy_keys`,但没有写入 `SimpleCache`;`busy()` 和 `place()` 都只查询 `busy_cache`,没有检查 `self.data`。缓存也不会跨进程恢复。
|
|
|
|
|
|
- 触发场景:程序重启后券商仍有活动订单;或本地 180 秒 TTL 到期但订单仍未终结。
|
|
|
|
|
|
- 影响:同一证券、同一方向可能重复开仓、补仓或止盈。卖出路径可能再次按全部可用持仓提交委托。
|
|
|
|
|
|
- 建议:`busy()` 和 `place()` 在同一互斥区内同时检查 TTL 缓存及 `self.data` 中的 `BUSY_STATUSES`。
|
|
|
|
|
|
|
|
|
|
|
|
### S2. 一次空订单快照会不可逆地清除待成交状态
|
|
|
|
|
|
|
|
|
|
|
|
- 位置:`strategy/trend/order.py:57-92`、`state.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-92`、`state.py:112-139,211-220`
|
|
|
|
|
|
- 证据:`refresh()` 丢弃取消、拒绝和异常订单,只把处理态及完成态写入 `data`。同一本地订单若一笔完成、一笔取消,传给 `State` 的只剩完成记录,`_order_status()` 会返回 `OK`。
|
|
|
|
|
|
- 影响:部分成交可能被视为完整成交;状态数量、成本和补仓次数与真实结果不一致。
|
|
|
|
|
|
- 建议:展示/防重列表可以过滤,但状态对账必须使用完整原始订单快照。
|
|
|
|
|
|
|
|
|
|
|
|
### H3. 柜台受理与状态落盘之间存在崩溃窗口
|
|
|
|
|
|
|
|
|
|
|
|
- 位置:`strategy/trend/open.py:78-98`、`positions.py:174-193`
|
|
|
|
|
|
- 证据:开仓和补仓都先执行 `orders.place()`,成功返回后才写入并保存 `StateItem`。
|
|
|
|
|
|
- 影响:委托受理后若进程退出或状态写入失败,本地没有对应状态;重启后容易重复下单。
|
|
|
|
|
|
- 建议:提交前持久化订单意图,提交后记录柜台结果;不确定结果保留为可对账状态。
|
|
|
|
|
|
|
|
|
|
|
|
### H4. 开仓与补仓并发,资金预算彼此隔离
|
|
|
|
|
|
|
|
|
|
|
|
- 位置:`strategy/trend/boot.py:180-190`、`open.py:15-74`、`positions.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:49`、`positions.py:168`
|
|
|
|
|
|
- 证据:`calc_buy_volume()` 使用 `max(1, floor(...)) * 100`。
|
|
|
|
|
|
- 影响:`buy_value < price * 100` 时委托金额必然超过预算,并放大 H4。
|
|
|
|
|
|
- 建议:不足一手时返回 `0`,由调用方记录并跳过。
|
|
|
|
|
|
|
|
|
|
|
|
## 中风险问题
|
|
|
|
|
|
|
|
|
|
|
|
### M1. 现金安全线只限制新开仓,不限制亏损补仓
|
|
|
|
|
|
|
|
|
|
|
|
- 位置:`strategy/trend/boot.py:145-148,183-188`、`positions.py:68-80`
|
|
|
|
|
|
- 证据:`allow_open_by_cash` 只控制 `open_signal()`;`manage_positions()` 始终获得全部 `assets.available`。
|
|
|
|
|
|
- 影响:账户已低于最小现金比例时仍可能增加亏损仓位。
|
|
|
|
|
|
- 建议:若安全线也约束补仓,应只传递扣除安全储备后的预算。
|
|
|
|
|
|
|
|
|
|
|
|
### M2. 两个风控配置未参与 Trend 决策
|
|
|
|
|
|
|
|
|
|
|
|
- 位置:`config/__init__.py:48-50`、`strategy/trend/positions.py:16,52-59,157-160`
|
|
|
|
|
|
- 证据:配置提供 `loss_trigger_pct` 和 `min_profit_pct`,但补仓使用固定 `LOSS_TIERS`,止盈门槛按股价区间硬编码。
|
|
|
|
|
|
- 影响:修改配置不会改变实盘行为,运维人员可能误判实际参数。
|
|
|
|
|
|
- 建议:让策略明确使用配置,或删除无效配置并输出最终生效参数。
|
|
|
|
|
|
|
|
|
|
|
|
### M3. 非 `APIError` 下单异常会终止整批持仓管理
|
|
|
|
|
|
|
|
|
|
|
|
- 位置:`strategy/trend/order.py:105-116`、`positions.py:38-91`
|
|
|
|
|
|
- 证据:`OrderBook.place()` 只捕获 `APIError`;`manage_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. 修复测试夹具并补齐跨轮、重启、空快照、撤单失败和并发预算回归。
|