第二十章:冲击成本回测框架:回测设计、评价指标、过拟合防范

冲击成本建模,说到底是个「纸上谈兵」的活儿。你建了个模型,说「我能在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 过拟合防范:回测是历史,不是未来

过拟合,是量化交易的头号杀手。我见过太多人,回测曲线漂亮得像教科书,实盘却一塌糊涂。说白了,就是模型记住了历史噪音,而不是真实规律。

怎么防?我分享三个实战方法:

  1. 交叉验证:别只用一段数据。把数据切成5份,4份训练,1份验证。轮着来。如果每次验证结果差异很大,那模型大概率过拟合了。
  2. 参数稳定性测试:把模型参数稍微调一调,比如滑点系数从0.1改成0.12,看看结果是不是剧烈变化。如果是,说明模型对参数太敏感,实盘里肯定出问题。
  3. 样本外测试:这是最笨但最有效的方法。留出最后20%的数据,整个回测过程中绝对不看它。等模型定稿了,再跑一次。如果样本外结果远差于样本内,那就得重新思考了。

我曾经踩过的坑:有一次,我为了追求回测夏普比率,加了十几个特征。结果样本外测试直接腰斩。后来我删掉了一半特征,虽然回测夏普降了0.3,但样本外表现反而更稳定。记住:简单模型往往更鲁棒

还有一个容易被忽略的点:时间序列的「未来信息」泄露。比如你用当天的收盘价来预测当天的冲击成本,这就是典型的未来函数。我建议在回测框架里加一个「时间戳检查器」,自动检测这种问题。

20.4 核心流程图:回测框架的完整链路

下面这张图,是我在实战中总结的回测框架流程图。它涵盖了从数据输入到结果输出的完整链路。你照着这个框架搭,基本不会漏掉关键环节。

冲击成本回测框架流程图 历史订单簿数据 数据清洗 & 时间戳对齐 回测引擎(Level 1/2/3) 订单簿模拟 | 撮合逻辑 | 滑点计算 评价指标计算 MAE | RMSE | Hit Rate | PnL | 最大回撤 过拟合检测(交叉验证 / 样本外测试) 反馈优化 模型部署

这张图里,我特别想强调那个「反馈箭头」。很多人回测完就结束了,其实不对。你应该把评价结果反馈到模型里,反复迭代。但注意:迭代次数不能太多,否则又过拟合了。我一般控制在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}

我的建议:别一上来就写复杂的回测框架。先用这个骨架跑通一个最简单的模型,比如「冲击成本 = 订单大小 / 盘口深度」。然后慢慢加细节。这样你能清楚每一步的改进到底有没有用。

好了,这一章的内容就到这里。回测框架是冲击成本建模的「试金石」,设计得好,你能快速迭代出靠谱的模型;设计得差,你就是在浪费时间。记住:回测不是为了证明模型好,而是为了发现模型哪里不好


无相订单流研究社 微信Lucian808555