4、回测系统搭建:回测引擎设计原则、事件驱动回测框架、数据管理与清洗

做市策略这东西,光有理论可不行。你得让它跑起来,看看它在历史数据里到底表现如何。这就是回测系统的价值所在。

我个人习惯把回测系统看作一个「时光机」。你开着它回到过去,模拟当时的市场环境,看看你的策略能不能赚钱。但这里有个坑——如果时光机本身就有bug,那你看到的结果全是假的。所以,回测引擎的设计,必须严谨。

回测引擎设计原则:别让系统骗了你

我见过太多人,回测结果漂亮得不行,一上实盘就亏成狗。为什么?因为回测引擎的设计有问题。说白了,就是系统在「作弊」。

设计回测引擎,你得守住几条底线:

  • 确定性:同样的输入,必须得到同样的输出。随机数种子要固定,否则你没法复现结果。
  • 无前瞻性:这是最要命的。你绝对不能用到未来的数据来做今天的决策。比如,你用当天的收盘价来模拟开盘时的交易,这就是典型的「未来函数」。
  • 低延迟模拟:做市策略对延迟极其敏感。你的回测引擎必须能模拟出订单的排队、撮合、成交延迟。否则,你算出来的利润全是幻觉。
  • 资金管理真实:手续费、滑点、保证金占用,一个都不能少。我曾经有个项目,回测时没算滑点,结果实盘第一天就亏了3%。
核心原则:回测不是让你看「赚了多少」,而是让你看「在真实环境下,策略会不会死」。

事件驱动回测框架:这才是做市策略的标配

为什么用事件驱动?你想想看,做市策略的核心是什么?是对市场事件的实时响应。订单簿变了、成交了、撤单了,这些事件驱动着你的每一次决策。

传统的「K线驱动」回测框架,说白了就是按时间切片,每个切片里算一笔账。但做市策略不行,它需要在毫秒级别处理事件。所以,事件驱动是唯一的选择。

一个典型的事件驱动框架,包含这几个核心组件:

  • 事件队列:所有市场数据、订单状态、定时任务,都变成事件,扔进队列里。
  • 事件循环:一个永不停止的循环,从队列里取事件,分发给对应的处理器。
  • 策略模块:你写的做市逻辑,就挂在这里。收到事件后,决定要不要报价、要不要撤单。
  • 模拟撮合引擎:这是最复杂的部分。它要模拟交易所的撮合逻辑,包括价格优先、时间优先、部分成交等。

我给你们看一个简化版的框架结构图:

事件驱动回测框架结构 历史数据源 / 模拟数据 事件队列(Queue) 事件循环(Event Loop) 策略模块(Strategy) 模拟撮合引擎 风控 / 资金管理

这个框架的好处是什么?模块化。你可以单独替换策略模块、撮合引擎,甚至数据源。我在做高频做市策略时,经常把历史数据源换成模拟数据源,用来测试策略在极端行情下的表现。

小技巧:事件队列不要用Python自带的queue.Queue,它太慢了。用collections.deque或者第三方库如disruptor,性能能提升一个数量级。

数据管理与清洗:垃圾进,垃圾出

这句话我重复一万遍都不嫌多。你的回测结果再漂亮,如果数据是脏的,那一切都是白费。

做市策略需要的数据,比普通策略精细得多。你需要的是「逐笔成交」和「订单簿快照」级别的数据,而不是简单的1分钟K线。

数据清洗,我一般分三步走:

  1. 去重:交易所的数据偶尔会重复推送。你得按时间戳+交易ID去重。
  2. 补全:有些交易所的订单簿快照不是全量推送,而是增量推送。你得自己维护一个完整的订单簿。
  3. 对齐:不同交易所的时间戳可能不同步。你得把它们对齐到同一个时间轴上。

我给你们看一个数据清洗的代码片段:

def clean_tick_data(raw_ticks):
    """
    清洗逐笔成交数据
    """
    # 1. 去重
    cleaned = raw_ticks.drop_duplicates(subset=['trade_id', 'timestamp'])
    
    # 2. 过滤异常值
    # 价格不能为负,成交量不能为0
    cleaned = cleaned[(cleaned['price'] > 0) & (cleaned['volume'] > 0)]
    
    # 3. 排序
    cleaned = cleaned.sort_values('timestamp')
    
    # 4. 检查时间戳连续性
    # 如果两个tick之间间隔超过5秒,标记为数据缺失
    cleaned['time_diff'] = cleaned['timestamp'].diff()
    missing_intervals = cleaned[cleaned['time_diff'] > 5]
    
    if len(missing_intervals) > 0:
        print(f"警告:发现 {len(missing_intervals)} 处数据缺失")
    
    return cleaned
注意:千万不要用「向前填充」或「向后填充」来处理缺失的订单簿数据。做市策略对价格敏感,填充的数据会引入严重的偏差。我建议直接跳过缺失时段,或者用插值法谨慎处理。

数据管理方面,我推荐用HDF5或Parquet格式存储。为什么?因为做市策略的数据量太大了,一天可能就有几千万条tick数据。CSV文件根本扛不住,加载一次要几分钟。HDF5的压缩率高,读取速度快,还能按时间分片查询。

嗯,这里要注意一点:数据存储的目录结构要设计好。我一般按「交易所/交易对/日期」来组织。比如:

data/
  binance/
    BTCUSDT/
      2024-01-01.h5
      2024-01-02.h5
  okx/
    BTCUSDT/
      2024-01-01.h5

这样设计的好处是,你回测时只需要加载指定日期范围的数据,不用把整个数据集都塞进内存。

最后说一句:数据清洗这件事,看起来枯燥,但它是整个回测系统的基石。我见过太多人,花几个月写策略,却只花一天准备数据。结果呢?回测结果全是噪音。别走这条路。

专注资料整理