第十二章:回测框架搭建
回测框架这东西,说白了就是你的时光机。我刚开始做市商那会儿,总觉得自己策略无敌,结果实盘一跑就被市场按在地上摩擦。后来才明白——不回测就上实盘,等于闭眼开车。
今天咱们聊聊怎么搭一套靠谱的回测框架。我会从历史数据回测讲起,再到事件驱动回测,最后说说模拟撮合引擎的设计。嗯,这三块是递进关系,缺一不可。
12.1 历史数据回测:最基础的验证手段
历史数据回测,就是拿过去的数据跑一遍你的策略。听起来简单,但坑特别多。
我个人习惯把历史数据回测分成三步:
- 数据清洗——去掉异常值、填充缺失值、对齐时间戳
- 策略执行——按时间顺序逐笔或逐Tick跑信号
- 绩效统计——算夏普、最大回撤、胜率这些指标
这里有个关键点:前视偏差。我在项目中遇到过好几次,回测结果漂亮得不像话,结果发现代码里不小心用了未来的数据。比如用当天的收盘价去判断当天的开仓信号——这肯定不行。
我曾经犯过一个低级错误:在回测时用了整个数据集计算均线,而不是滚动计算。结果回测曲线完美向上,实盘直接崩了。记住:回测必须严格模拟当时能获取到的信息。
来看一个简单的历史回测代码框架:
class BacktestEngine:
def __init__(self, data, initial_capital=100000):
self.data = data
self.capital = initial_capital
self.positions = []
self.trades = []
def run(self):
for i in range(len(self.data)):
# 获取当前时刻的数据(注意:只能用到i时刻之前的数据)
current_bar = self.data.iloc[:i+1]
# 生成信号
signal = self.generate_signal(current_bar)
# 执行交易
if signal != 0:
self.execute_trade(signal, self.data.iloc[i])
# 更新持仓市值
self.update_pnl(self.data.iloc[i])
def generate_signal(self, data):
# 这里写你的策略逻辑
pass
def execute_trade(self, signal, price):
# 执行交易逻辑
pass
def update_pnl(self, current_price):
# 更新盈亏
pass
12.2 事件驱动回测:更贴近真实市场
历史数据回测有个硬伤——它假设你能在任意时刻以任意价格成交。真实市场不是这样的。你想想看,你的订单发出去,得经过交易所撮合,可能成交也可能不成交,成交价格也可能滑点。
事件驱动回测就是为了解决这个问题。它模拟了市场事件的流动:
- Tick事件——每一笔成交或报价变化
- 订单事件——你发出去的限价单、市价单的状态变化
- 定时事件——比如每秒检查一次持仓
我建议用事件队列来管理这些事件。说白了,就是一个先进先出的队列,按时间顺序处理每个事件。
事件驱动回测的核心是:把市场变化和策略决策都抽象成事件。每个事件触发相应的处理函数,这样代码结构清晰,也容易扩展。
举个例子,一个简单的事件驱动回测框架:
class EventDrivenBacktest:
def __init__(self):
self.event_queue = []
self.current_time = None
def add_event(self, event):
# 按时间排序插入事件
self.event_queue.append(event)
self.event_queue.sort(key=lambda e: e.timestamp)
def run(self):
while self.event_queue:
event = self.event_queue.pop(0)
self.current_time = event.timestamp
if event.type == 'TICK':
self.on_tick(event.data)
elif event.type == 'ORDER':
self.on_order(event.data)
elif event.type == 'TIMER':
self.on_timer()
def on_tick(self, tick_data):
# 处理Tick数据,生成信号
signal = self.strategy.on_tick(tick_data)
if signal:
self.send_order(signal)
def on_order(self, order_data):
# 处理订单状态变化
if order_data.status == 'FILLED':
self.update_position(order_data)
def on_timer(self):
# 定时任务,比如风控检查
self.risk_check()
12.3 模拟撮合引擎设计:回测的灵魂
模拟撮合引擎,这是回测框架里最难也最重要的部分。它决定了你的订单能不能成交、以什么价格成交。
我见过很多团队,回测时直接用收盘价成交,结果实盘滑点吃掉所有利润。所以,撮合引擎的精度直接决定了回测的可信度。
一个合格的模拟撮合引擎至少要考虑:
| 因素 | 说明 | 实现方式 |
|---|---|---|
| 订单簿深度 | 当前买一卖一的价格和数量 | 用Level2数据或模拟深度 |
| 滑点模型 | 大单成交时的价格偏移 | 线性滑点或基于深度的滑点 |
| 成交概率 | 限价单能否成交 | 根据价格位置和流动性估算 |
| 延迟模拟 | 订单从发出到成交的时间差 | 固定延迟或随机延迟 |
我个人习惯用订单簿快照来模拟撮合。每次Tick到来时,更新订单簿,然后检查你的挂单是否被吃掉。这样虽然计算量大一点,但结果更真实。
如果你没有Level2数据,可以用Tick数据模拟一个简化的订单簿。比如:假设买一卖一价差固定,深度按成交量比例分配。虽然粗糙,但比直接用收盘价强多了。
来看一个简化版的撮合引擎:
class MatchingEngine:
def __init__(self):
self.bids = [] # 买单队列
self.asks = [] # 卖单队列
self.order_book = {'bid': {}, 'ask': {}}
def update_order_book(self, tick):
# 更新订单簿
self.order_book['bid'] = tick['bid_prices']
self.order_book['ask'] = tick['ask_prices']
def match_order(self, order):
if order.side == 'BUY':
# 市价买单:按卖一价成交
if order.type == 'MARKET':
best_ask = min(self.order_book['ask'].keys())
fill_price = best_ask
fill_qty = min(order.qty, self.order_book['ask'][best_ask])
return fill_price, fill_qty
# 限价买单:价格高于卖一才能成交
else:
if order.price >= min(self.order_book['ask'].keys()):
fill_price = order.price
fill_qty = order.qty
return fill_price, fill_qty
else:
return None, 0 # 未成交
else:
# 卖单逻辑类似
pass
def simulate_slippage(self, order, fill_price):
# 模拟滑点:大单加价
if order.qty > 100:
slippage = order.qty * 0.0001 # 每手加0.01%
return fill_price * (1 + slippage)
return fill_price
12.4 回测框架的整体架构
把上面三块拼起来,就是一个完整的回测框架。我画了张图,帮你理清关系:
从这张图你能看到,数据从底层往上流,策略信号从上往下执行。每一层各司其职,互不干扰。这也是我推荐的分层设计思路——每一层都可以单独替换或升级。
回测框架搭建,核心就三件事:
1. 历史数据回测——验证策略在历史数据上的表现
2. 事件驱动回测——模拟真实市场的事件流
3. 模拟撮合引擎——决定订单能否成交、以什么价格成交
这三块做好了,你的回测结果才有参考价值。否则,回测再漂亮也是自欺欺人。
嗯,今天就聊到这儿。回测框架这东西,光看理论没用,你得动手搭一遍。我建议你先从最简单的历史数据回测开始,慢慢加上事件驱动和撮合引擎。每一步踩过的坑,都是你成长的阶梯。
无相订单流研究社 微信Lucian808555