Alpha-Online
技术手册

4. 截面回测引擎

回测框架应匹配问题形态:截面选股用矩阵驱动而非事件驱动;信号与风控和实盘同源。

作用:用「日期 for-loop + 截面矩阵运算」逐日模拟组合净值,输出绩效与报告。

输入与输出的契约

输入是信号层的每日合成分截面和预处理层的 returns 面板。输出是净值曲线、绩效指标(Sharpe / 最大回撤 / Calmar / 胜率 / 盈亏比)和图表报告。

这一层对上下游的承诺:T 日的每一个决策只用 T-1 及更早的信息;信号、风控、因子筛选与实盘走同一套实现。回测数字的意义完全建立在这两条之上——破坏任何一条,Sharpe 再高也只是自欺。

决策一:矩阵驱动,而不是事件驱动

Alpha101 是每日对 500 只标的进行截面评分排序的截面问题,而非针对单只标的的突破交易。Backtrader 等事件驱动框架把每根 K 线当事件逐个处理,在此场景下性能开销过高;自研矩阵驱动引擎约 300 行代码,执行效率更优,且更容易从结构上杜绝未来函数。

一般化的判据:回测框架应匹配问题形态。逐标的的时序策略适合事件驱动;逐日全截面的选股策略适合矩阵驱动。

决策二:防未来函数靠物理隔离,不靠逐处检查

信号矩阵在进入主循环之前,就对每个标的整体后移一天——T 日行上放的是 T-1 收盘后就能算出的分数。主循环里没有任何「这个数据是不是未来的」的时间判断,因为被后移过的矩阵在结构上就不可能包含未来。

这个设计的好处是审计成本低:要验证回测无未来函数,只需要检查一处(入口的 shift),而不需要逐行审查整个循环体。

决策三:成本显式建模,调仓频率是一等参数

换手成本不是事后估算,而是每个调仓日精确计入:

换手率 = Σ|新权重 − 旧权重| / 2
当日成本 = 换手率 × 单边费率 × 2(双边)

算个例子:旧组合 {A: 50%, B: 50%},新组合 {B: 50%, C: 50%}。A 清仓 50%、C 建仓 50%、B 不动,换手率 = (50% + 0 + 50%) / 2 = 50%,按单边费率 0.1% 计,当日成本 = 50% × 0.1% × 2 = 0.1%,直接从当日收益里扣。

这个算术直接解释了为什么调仓频率必须与实盘一致:同样的组合,每日调仓的摩擦成本约是 5 日调仓的 5 倍。回测按 5 日、实盘按每日,亏损的主因就不是因子失效而是摩擦 → 事故档案 · 调仓频率不一致导致摩擦放大

调仓相位(首次调仓落在第几天)同样是显式参数:同一策略把相位从 0 拨到 4,净值曲线可能差出一截。这就是 timing luck——好策略应该对相位不敏感,所以它必须可测。

一次调仓日发生了什么

以默认参数(Top 10、5 日调仓、单边成本 0.1%)为例,设今天是 T 日且距上次调仓已满 5 个交易日:

  1. 取信号:取出 T 日行的截面分数(实际是 T-1 的信息,见决策二)
  2. 截面排序:去掉缺失值后取前 10 名。有效分数不足 10 个就有多少取多少,小样本日降级持仓
  3. 等权分配:每只 1/10(评分加权作为备选口径预留,当前实际走等权)
  4. 风控过滤(顺序敏感):单票超 10% 封顶 → 持仓数超 30 按权重截断 → 当前回撤 ≥ 15% 时全体权重乘 0.5 → 总仓超 95% 整体缩放 → 丢掉 < 0.01% 的粉尘权重。注意回撤状态来自昨天日终的记账(第 6 步),不引入当日信息
  5. 算换手与成本:按决策三的公式,从当日收益里扣
  6. 记收益、滚净值:日收益 = Σ 权重 × 当日 returns − 成本,净值累乘。当日缺 returns 的持仓按 0 收益处理——这是停牌 / 缺数据的简化口径,与预处理层的 tradable_mask 缺口是同一件事
  7. 日终风控记账:更新历史峰值、算当前回撤,供下一个调仓日的第 4 步使用。回测路径的「回撤止损」就在这里——执行路径恰恰缺的就是这一步(见下方缺口)

非调仓日只有第 6、7 步:持仓不动、换手为 0,净值随市场漂。

常见错误

当前状态

历史回测在特定区间与参数下得到 Sharpe 1.3–1.8 量级的结果。口径不一、均为局部拟合,须与 SPY/QQQ 基准对比才有意义——详见指标口径与免责声明

主要缺口:回撤止损只在回测路径生效。权重类风控(单票 / 持仓数 / 总仓上限)已通过复用同一个风控模块接入执行路径,但回撤状态只有回测的日终记账在维护,实盘路径没有喂入回撤数据,回撤降仓不会触发。

实现对照


内部溯源(私有仓库):三路分叉与止损失效 issue #31 · 风控接入执行 #34 · 风控自动化测试 #36

On this page