第二十章:冲击成本回测框架:回测设计、评价指标、过拟合防范
冲击成本建模,说到底是个「纸上谈兵」的活儿。你建了个模型,说「我能在1分钟内吃掉500手而不滑点」,但实盘一跑,直接被打脸。这种事我见过太多次了。
所以,回测框架就是你的「验兵场」。没有它,你的模型就是空中楼阁。今天我就把我在实战中打磨出来的回测框架,掰开揉碎了讲给你听。
20.1 回测设计:别让数据骗了你
回测设计,核心就一句话:模拟真实交易环境。你想想看,如果回测里你总能以最优价成交,那实盘肯定亏到姥姥家。
我个人习惯把回测分成三个层次:
- Level 1:静态回测 —— 用历史订单簿快照,假设你的订单按时间戳插入。简单,但容易过拟合。
- Level 2:动态回测 —— 模拟订单簿的逐笔变化,你的订单会跟其他订单竞争。更真实,但计算量大。
- Level 3:仿真回测 —— 接入模拟撮合引擎,甚至用历史行情回放。我最推荐这个,但实现成本高。
核心原则:回测环境越接近实盘,你的模型越靠谱。别偷懒,至少做到Level 2。
我在项目中遇到过最坑的事:用Level 1回测跑出来的模型,年化收益30%,实盘直接亏了两个月。后来一查,原来是回测里每次都能吃到盘口的「厚单」,实盘里那些单子根本就是假的。
嗯,这里要注意:回测的时间戳精度。如果你的数据是秒级,而你的交易是毫秒级,那回测结果基本是废纸。我建议至少用毫秒级数据,最好用纳秒级。
20.2 评价指标:别只看夏普比率
很多人回测完,就盯着夏普比率看。夏普高就开心,夏普低就改参数。这其实是个大坑。
冲击成本模型的核心评价指标,我总结为「三看」:
| 维度 | 指标 | 说明 |
|---|---|---|
| 准确性 | MAE(平均绝对误差) | 预测冲击 vs 实际冲击的绝对偏差 |
| 稳定性 | RMSE(均方根误差) | 对大偏差更敏感,能暴露模型的「极端失误」 |
| 方向性 | Hit Rate(命中率) | 预测方向与实际方向一致的比例 |
| 经济意义 | PnL(损益) | 用模型指导交易,实际赚了多少 |
| 鲁棒性 | 最大回撤 | 模型在极端行情下的表现 |
我的经验:MAE和RMSE要一起看。如果MAE小但RMSE大,说明模型偶尔会犯大错。这种模型在实盘里很危险,因为一次大错可能吃掉你半年的利润。
另外,我强烈建议加一个指标:滑点成本占比。就是你的冲击成本占交易总成本的比例。如果这个比例超过30%,说明你的模型还有很大优化空间。
20.3 过拟合防范:回测是历史,不是未来
过拟合,是量化交易的头号杀手。我见过太多人,回测曲线漂亮得像教科书,实盘却一塌糊涂。说白了,就是模型记住了历史噪音,而不是真实规律。
怎么防?我分享三个实战方法:
- 交叉验证:别只用一段数据。把数据切成5份,4份训练,1份验证。轮着来。如果每次验证结果差异很大,那模型大概率过拟合了。
- 参数稳定性测试:把模型参数稍微调一调,比如滑点系数从0.1改成0.12,看看结果是不是剧烈变化。如果是,说明模型对参数太敏感,实盘里肯定出问题。
- 样本外测试:这是最笨但最有效的方法。留出最后20%的数据,整个回测过程中绝对不看它。等模型定稿了,再跑一次。如果样本外结果远差于样本内,那就得重新思考了。
我曾经踩过的坑:有一次,我为了追求回测夏普比率,加了十几个特征。结果样本外测试直接腰斩。后来我删掉了一半特征,虽然回测夏普降了0.3,但样本外表现反而更稳定。记住:简单模型往往更鲁棒。
还有一个容易被忽略的点:时间序列的「未来信息」泄露。比如你用当天的收盘价来预测当天的冲击成本,这就是典型的未来函数。我建议在回测框架里加一个「时间戳检查器」,自动检测这种问题。
20.4 核心流程图:回测框架的完整链路
下面这张图,是我在实战中总结的回测框架流程图。它涵盖了从数据输入到结果输出的完整链路。你照着这个框架搭,基本不会漏掉关键环节。
这张图里,我特别想强调那个「反馈箭头」。很多人回测完就结束了,其实不对。你应该把评价结果反馈到模型里,反复迭代。但注意:迭代次数不能太多,否则又过拟合了。我一般控制在3-5轮。
20.5 代码示例:一个简单的回测骨架
下面是我常用的回测框架骨架。它不复杂,但够用。你可以直接拿过去改。
import pandas as pd
import numpy as np
class ImpactBacktest:
def __init__(self, orderbook_data, level='2'):
self.data = orderbook_data
self.level = level
self.results = []
def run(self, model, params):
"""执行回测"""
for idx, row in self.data.iterrows():
# 模拟订单簿状态
ob = self._build_orderbook(row)
# 用模型预测冲击成本
predicted_impact = model.predict(ob, params)
# 模拟实际成交
actual_impact = self._simulate_execution(ob, params['order_size'])
# 记录结果
self.results.append({
'timestamp': row['timestamp'],
'predicted': predicted_impact,
'actual': actual_impact,
'error': predicted_impact - actual_impact
})
return self._compute_metrics()
def _build_orderbook(self, row):
"""构建订单簿快照"""
# 这里根据level不同,实现不同复杂度
pass
def _simulate_execution(self, ob, size):
"""模拟成交,计算实际冲击"""
# Level 1: 简单按时间戳插入
# Level 2: 考虑其他订单竞争
pass
def _compute_metrics(self):
"""计算评价指标"""
df = pd.DataFrame(self.results)
mae = np.mean(np.abs(df['error']))
rmse = np.sqrt(np.mean(df['error']**2))
hit_rate = np.mean((df['predicted'] * df['actual']) > 0)
return {'MAE': mae, 'RMSE': rmse, 'Hit Rate': hit_rate}
我的建议:别一上来就写复杂的回测框架。先用这个骨架跑通一个最简单的模型,比如「冲击成本 = 订单大小 / 盘口深度」。然后慢慢加细节。这样你能清楚每一步的改进到底有没有用。
好了,这一章的内容就到这里。回测框架是冲击成本建模的「试金石」,设计得好,你能快速迭代出靠谱的模型;设计得差,你就是在浪费时间。记住:回测不是为了证明模型好,而是为了发现模型哪里不好。