12、回测系统搭建:回测框架设计、历史数据回放、策略参数优化、绩效评估指标

做市交易这行,有个残酷的现实:你的策略在实盘里能活多久,取决于它在历史数据里死过多少次

我见过太多人,策略写出来就急着上实盘。结果呢?三天亏掉一个月的利润。说白了,就是没做回测,或者回测做得太糙。

这一章,我们就来聊聊怎么搭一个靠谱的回测系统。不是那种花里胡哨的,而是能真正帮你发现问题的。

核心观点:回测不是为了证明策略赚钱,而是为了证明策略在哪些情况下会亏钱。搞清楚这个,你才算入门了。

回测框架设计:别想着一步到位

我个人习惯,回测框架分三层:

  • 数据层:负责加载、清洗、对齐历史数据
  • 引擎层:负责模拟撮合、订单管理、资金计算
  • 策略层:你的做市逻辑,比如报价更新、库存管理

为什么要分层?因为你在调试策略时,最怕的是数据出问题,你却以为是策略写错了。分层之后,每一层可以独立测试。

举个例子,我早期做的一个回测框架,数据层和引擎层耦合在一起。结果有一次,数据里有个时间戳跳变,引擎直接崩溃了。我花了三天才定位到问题——原来是数据源的一个bug。从那以后,我坚持数据层必须独立,并且要有数据校验模块。

小技巧:回测框架的引擎层,不要追求完全模拟交易所。你只需要模拟「订单簿变化」和「成交逻辑」就够了。太复杂的引擎,反而容易引入bug。

历史数据回放:快进还是慢放?

历史数据回放,说白了就是让策略「穿越」回过去,重新跑一遍。

但这里有个坑:数据的时间粒度

做市策略对时间敏感,通常需要 tick 级数据。但 tick 数据量巨大,一天可能几百万条。全量回放,速度慢得让人崩溃。

我的做法是:

  • 快速回放:用于参数优化,只保留关键事件(如大单成交、价格跳变)
  • 精细回放:用于策略验证,逐 tick 模拟,但只跑最近一周的数据

你想想看,如果你每次调参数都要跑三个月的数据,那优化效率得多低?

注意:快速回放会丢失一些细节,比如滑点、订单簿深度变化。所以,参数优化用快速回放,最终验证必须用精细回放。这个顺序不能乱。

策略参数优化:别掉进过拟合的坑

参数优化,是回测里最容易让人兴奋,也最容易让人翻车的地方。

我记得有一次,我优化一个做市策略的参数,在历史数据上跑出了年化 80% 的收益。当时那个激动啊,觉得要发财了。结果实盘一周,亏了 15%。

为什么?因为参数过拟合了。策略完美适配了历史数据里的噪声,但一到实盘,噪声变了,策略就失效了。

怎么避免?我总结了三条经验:

  1. 参数数量要少:一个策略超过 5 个可调参数,就要警惕了。参数越多,过拟合风险越大。
  2. 使用交叉验证:把历史数据分成三段,用前两段优化,第三段验证。如果三段结果差异很大,说明参数不稳定。
  3. 设置参数边界:比如报价频率,不能低于 100ms,否则交易所会拒绝。这些边界条件,必须在优化前就定好。

一句话总结:参数优化不是找「最优解」,而是找「稳健解」。一个在多种市场环境下都能赚钱的参数,才是好参数。

绩效评估指标:别只看收益率

做市策略的评估,和普通交易策略不一样。你不能只看收益率,还要看:

指标 说明 我的经验值
夏普比率 衡量风险调整后收益 做市策略通常 > 2 才算合格
最大回撤 账户从峰值跌落的幅度 做市策略应 < 5%,否则风险太高
胜率 盈利交易占比 做市策略胜率通常 > 60%
盈亏比 平均盈利 / 平均亏损 做市策略盈亏比 > 1.5 比较健康
库存周转率 库存被清空的频率 越高越好,说明流动性好

嗯,这里要注意:夏普比率高不代表策略好。我见过一个策略,夏普比率 3.5,但最大回撤 20%。这种策略,你根本拿不住。

我个人习惯,先看最大回撤,再看夏普比率。回撤控制不住,收益再高也没用。

回测系统的核心逻辑

说了这么多,我画了一张图,帮你理清回测系统的整体结构:

回测系统核心逻辑 数据层 历史 tick 数据 数据清洗 & 对齐 时间戳校验 引擎层 订单簿模拟 撮合逻辑 资金 & 持仓管理 策略层 报价生成 库存管理 风险控制 回放模式切换 快速回放(参数优化) ←→ 精细回放(策略验证) 绩效评估指标 夏普比率 | 最大回撤 | 胜率 | 盈亏比 | 库存周转率 先看最大回撤,再看夏普比率 参数优化:找稳健解,不是最优解

这张图展示了回测系统的完整流程。数据层提供原料,引擎层负责烹饪,策略层决定口味。而回放模式切换,决定了你是「快餐」还是「米其林」。

一个简单的回测代码示例

说了这么多理论,来点实际的。下面是一个极简的回测引擎核心代码,只展示最关键的逻辑:

class BacktestEngine:
    def __init__(self, data, initial_capital=100000):
        self.data = data          # 历史数据
        self.capital = initial_capital
        self.position = 0         # 当前持仓
        self.trades = []          # 成交记录
    
    def run(self, strategy):
        """运行回测"""
        for tick in self.data:
            # 1. 策略生成报价
            bid, ask = strategy.generate_quotes(tick)
            
            # 2. 模拟撮合
            if tick.price <= bid:   # 买单成交
                self._execute_buy(bid, tick.volume)
            elif tick.price >= ask:  # 卖单成交
                self._execute_sell(ask, tick.volume)
            
            # 3. 更新策略状态
            strategy.update(tick, self.position)
        
        # 4. 计算绩效
        return self._calculate_performance()
    
    def _execute_buy(self, price, volume):
        cost = price * volume
        if cost <= self.capital:
            self.capital -= cost
            self.position += volume
            self.trades.append(('buy', price, volume))
    
    def _execute_sell(self, price, volume):
        if self.position >= volume:
            self.capital += price * volume
            self.position -= volume
            self.trades.append(('sell', price, volume))
    
    def _calculate_performance(self):
        # 这里计算夏普比率、最大回撤等指标
        pass

这段代码虽然简单,但包含了回测引擎的核心:数据循环、撮合逻辑、状态更新。你可以在此基础上,加入滑点、手续费、订单簿深度等细节。

我的建议:刚开始做回测,不要追求完美。先跑通一个最简单的版本,然后逐步加功能。我见过太多人,一开始就想做「通用回测框架」,结果半年过去了,连一个策略都没跑通。

避坑指南

最后,分享几个我踩过的坑:

  • 前视偏差:回测时不小心用了未来的数据。比如,用当天的收盘价来生成当天的交易信号。这绝对是回测里最常见的错误。我曾经因为这个,白高兴了一个月。
  • 忽略交易成本:做市策略对交易成本极其敏感。手续费、滑点、甚至网络延迟,都会吃掉你的利润。回测时一定要把这些算进去。
  • 过度优化:前面说过了,参数优化要适可而止。记住,历史数据不会重演,但人性会。

最后一句忠告:回测通过,只是开始。实盘验证,才是真正的考验。永远给你的回测结果打个折扣——比如 50%。这样,当实盘表现不如预期时,你至少不会太失望。


交易系统化学习资料 微信Strategy888888