8.9 KiB
8.9 KiB
Trend 策略代码审计报告
审计日期: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。
建议处理顺序
- S1、S2:恢复可靠的订单防重和未决状态对账。
- H2、H3、H5:确保订单生命周期、撤单和成交状态可信。
- H1、H4、H6、M1:恢复风控并统一账户预算。
- M2、M3、M4、M5、M6:处理配置、异常隔离、资源和刷新策略。
- 修复测试夹具并补齐跨轮、重启、空快照、撤单失败和并发预算回归。