第七章:回测系统搭建
做量化交易,最怕什么?
怕策略在实盘里跑着跑着就崩了。怕历史回测漂亮得像朵花,一上实盘就变成豆腐渣。
我做了这么多年做市商系统,见过太多人栽在回测这个环节。说白了,回测系统就是你的策略试金石。今天咱们就聊聊怎么搭一个靠谱的回测系统。
7.1 历史数据回放:让市场重演
回测的第一步,得有数据。而且得是干净的数据。
我个人习惯把历史数据分成三个层级:
- Tick级数据:每笔成交的细节,适合高频策略
- 快照级数据:每秒或每百毫秒的市场状态
- K线数据:1分钟、5分钟等聚合数据
做做市商策略,我建议至少用快照级数据。为什么?因为做市商吃的是微观结构的饭,K线太粗了,很多细节都丢了。
数据清洗的坑
我曾经遇到过一个问题:某交易所的数据里,偶尔会出现时间戳倒流的情况。如果你不做处理,回测时就会算出穿越时空的收益——听起来很酷,但实盘里根本不存在。
数据回放的逻辑其实不复杂:
class DataReplayer:
def __init__(self, data_path):
self.data = self.load_data(data_path)
self.current_idx = 0
def next_tick(self):
"""获取下一个数据点"""
if self.current_idx < len(self.data):
tick = self.data[self.current_idx]
self.current_idx += 1
return tick
return None
def reset(self):
self.current_idx = 0
嗯,代码看着简单,但实际用起来要注意性能。我见过有人用Python列表存几千万条tick数据,回测一次跑半小时。后来改成内存映射文件,速度提升了十几倍。
7.2 事件驱动回测框架:核心引擎
回测框架的核心是什么?是事件驱动。
你想想看,实盘交易时,市场在不停地产生事件:新的报价来了,订单成交了,撤单成功了...回测系统就得模拟这个过程。
我的经验
事件驱动框架里,最容易被忽略的是事件的时间戳对齐。不同数据源的时间可能差几毫秒,但就是这几毫秒,可能导致你的策略在回测里赚钱,实盘里亏钱。
一个典型的事件驱动框架包含这几个组件:
- 事件队列:按时间排序的事件列表
- 事件处理器:处理每种事件的逻辑
- 策略模块:你的交易策略
- 风控模块:检查订单是否合规
- 成交引擎:模拟订单如何成交
下面是我常用的框架结构:
class EventEngine:
def __init__(self):
self.events = []
self.strategies = []
self.risk_manager = RiskManager()
self.execution_engine = ExecutionEngine()
def run(self):
while self.events:
event = heapq.heappop(self.events)
self.process_event(event)
def process_event(self, event):
if event.type == 'TICK':
for strategy in self.strategies:
strategy.on_tick(event.data)
elif event.type == 'ORDER':
self.execution_engine.handle_order(event.data)
这里有个细节:事件队列要用优先队列(heapq),保证事件按时间顺序处理。我见过有人用普通列表,然后每次排序——那性能,啧啧,不忍直视。
7.3 成交模拟:最考验功力的地方
回测里最难的是什么?是模拟成交。
实盘里,你挂一个买单,可能瞬间就被吃掉了,也可能挂一天都没人理。回测里怎么模拟这个过程?
注意
千万不要假设「挂单就能成交」。我见过太多回测系统这么干,结果实盘时发现滑点大得吓人。
我常用的成交模型分三种:
| 模型类型 | 适用场景 | 精度 |
|---|---|---|
| 立即成交模型 | 流动性极好的品种 | 低 |
| 队列模型 | 做市商策略 | 中 |
| 订单簿重建模型 | 高频策略 | 高 |
做做市商策略,我建议用队列模型。它假设订单簿上每个价位都有一个队列,你的订单排在队尾。只有前面的订单都成交了,才轮到你。
class QueueExecutionModel:
def __init__(self, queue_depth=10):
self.queue_depth = queue_depth
def can_fill(self, order, market_data):
"""判断订单是否能成交"""
# 计算订单在队列中的位置
position = self.get_queue_position(order, market_data)
# 如果位置小于队列深度,认为能成交
return position < self.queue_depth
这个模型虽然简单,但比「立即成交」靠谱多了。我曾在实盘里验证过,误差在5%以内。
7.4 绩效指标计算:用数字说话
回测跑完了,怎么评价策略好不好?
别只看总收益。我见过有人回测收益翻倍,但最大回撤80%,这种策略你敢用吗?
夏普比率(Sharpe Ratio)
夏普比率衡量的是「每承担一单位风险,能获得多少超额收益」。
def calculate_sharpe(returns, risk_free_rate=0.03):
excess_returns = returns - risk_free_rate / 252
sharpe = np.mean(excess_returns) / np.std(excess_returns)
return sharpe * np.sqrt(252) # 年化
夏普比率大于1算不错,大于2就很好了。但要注意:它假设收益是正态分布的,而实际市场里可不是这样。
索提诺比率(Sortino Ratio)
索提诺比率和夏普类似,但它只考虑下行风险。说白了,上涨的波动是好事,下跌的波动才是风险。
def calculate_sortino(returns, risk_free_rate=0.03):
excess_returns = returns - risk_free_rate / 252
downside_returns = excess_returns[excess_returns < 0]
downside_std = np.std(downside_returns)
sortino = np.mean(excess_returns) / downside_std
return sortino * np.sqrt(252)
我个人更看重索提诺比率。为什么?因为做市商策略的收益曲线通常比较平滑,夏普比率可能很高,但索提诺更能反映真实的风险。
最大回撤(Max Drawdown)
最大回撤就是「从最高点跌到最低点的最大幅度」。
def calculate_max_drawdown(equity_curve):
peak = np.maximum.accumulate(equity_curve)
drawdown = (equity_curve - peak) / peak
max_dd = np.min(drawdown)
return max_dd
做市商策略的最大回撤一般控制在5%以内。如果超过10%,你得好好想想是不是策略有问题。
避坑指南
我曾经犯过一个错误:用全部历史数据计算绩效指标,结果策略在某个极端行情里表现极差,但被其他时间段的数据「平均」掉了。后来我改用滚动窗口计算,每个时间段的绩效都单独看,这才发现问题。
7.5 回测系统的整体架构
说了这么多,咱们看看回测系统的整体架构:
这个架构图看着简单,但每一层都有很多细节。我建议你从数据层开始搭,一步步往上。别想着一次搞定,回测系统是迭代出来的。
最后说一句
回测系统不是一次性的工作。随着你对市场的理解加深,你会发现需要加更多功能。比如我后来加了「市场冲击模型」和「手续费动态计算」,回测结果才更接近实盘。
好了,回测系统的基本框架就这些。记住:回测是工具,不是目的。别在回测里追求完美,那反而会让你在实盘里摔跟头。