5. 交易执行层 ADS
执行层的设计前提是「程序随时会挂」:模拟盘与实盘同构于同一抽象接口,状态可恢复、结果可对账,实盘与回测的漂移必须被监控。
作用:把目标持仓落地为真实交易 / 记账;模拟盘与实盘共用抽象接口;全程可恢复、可对账。
输入与输出的契约
输入是信号层的目标持仓(每个调仓日的 Top N 名单 + 权重)。输出是真实的订单与成交记录、逐日净值记账,以及实盘 vs 回测的漂移对账结果。
这一层的设计前提是程序随时会挂:进程重启、API 超时、订单被拒都是常态而非异常。因此对系统的承诺是:任何时刻宕机重启后,都能恢复到正确状态继续运行;每一笔交易都可事后对账。
决策一:模拟盘与实盘同构于同一抽象接口
模拟盘和实盘实现同一个执行引擎抽象:同样的输入(目标持仓)、同样的方法(执行调仓)、同样的落库口径。差别只在订单落到 SQLite 记账还是落到 Alpaca 真实账户。
为什么:模拟盘的意义是预演实盘。如果两者逻辑各写一套,模拟盘验证的就不是将来实盘要跑的代码,连跑四周零报错也说明不了什么。这是 One Codebase 在执行层的形态。
决策二:Delta 对齐,而不是指令重放
每次执行的动作不是「按信号买入这 10 只」,而是:
待执行交易 = 目标持仓 − 实际持仓先从券商 / 数据库读回当前真实持仓,与目标比对,只交易差额。这个设计天然幂等:重复执行同一天的调仓不会重复下单;宕机后重启,无论中断在哪一步,下一次执行都会自动补平差额。
反面教材是曾经的真实事故:服务重启后只恢复了净值、未恢复持仓明细,程序误判为空仓,触发全仓买入,换手率异常 → 事故档案 · 重启后状态丢失。教训的一般形式:执行系统的状态必须以「真实世界」(券商账户)为准,而不是以程序内存或不完整的本地记录为准。
决策三:调仓频率与回测严格一致
默认 5 日调仓。回测按 5 日调仓验证、实盘却每日调仓,同样的信号会把摩擦成本放大约 5 倍(算术见回测引擎 · 决策三)。模拟盘曾经的亏损主因正是这个不一致,而非因子失效 → 事故档案 · 调仓频率不一致导致摩擦放大。
因此调仓节流是执行入口的一等逻辑:不满 N 个交易日不执行调仓,参数与回测同源(同一份配置读出来)。
决策四:实盘与回测的漂移必须被监控
回测是执行的基准线。每日流水线最后一步做漂移对账:拿实盘(模拟盘)的真实表现,与同参数回测的理论表现比对四个维度——净值、单日收益、换手率、持仓重合度——任一超阈值即推送告警。
为什么:实盘偏离回测只有三类原因——数据不同步、代码路径分叉、真实摩擦超预期。三类都是要么能修、要么要重新评估策略的问题,但前提是偏离发生当天就被看见,而不是月底看净值时才发现。这是首页架构图里「监控层 · 偏差检测」的落地。
常见错误
- 重启后只恢复净值未恢复持仓,误判空仓全仓买入 → 事故档案 · 重启后状态丢失
- 整股离散化误差与权重 < 0.1% 的粉尘持仓持续推高交易成本 → 事故档案 · 碎股与粉尘持仓
- 回测 5 日调仓、实盘每日调仓,摩擦放大数倍 → 事故档案 · 调仓频率不一致
- 总敞口计算错误导致总仓 > 100% → 权重计算始终基于真实账户权益
当前状态
模拟盘上线;实盘整改后补齐「真实落库 + 真实滑点 + 5 日调仓节流」。漂移监控已作为流水线 Step 6 每日运行,超阈值走 Bark 告警。
主要缺口:权重类风控已接入执行路径,但回撤止损仍只在回测生效(执行路径没有喂入回撤状态);切实盘前模拟盘须「无人值守、零报错」连跑 ≥ 4 周。
实现对照
内部溯源(私有仓库):状态恢复 issue #30 · 粉尘持仓 #28 · 总仓超限 #27 · 实盘整改 #40 + commit
05eab94 · 风控接入执行 #34