回测系统搭建(上):回测框架设计原则、事件驱动回测引擎、滑点与交易成本模型
做期权波动率曲面套利,最怕什么?
怕策略在纸上跑得漂亮,一上实盘就崩。我见过太多人,回测年化50%,实盘直接亏到怀疑人生。说白了,问题出在回测系统本身——它骗了你。
今天我们就来聊聊,怎么搭一个靠谱的回测系统。这一章先讲框架设计原则、事件驱动引擎,还有滑点和交易成本模型。嗯,都是硬骨头,但啃下来,你的策略才算真正有了「实战基因」。
一、回测框架设计原则:别让系统坑了你
我个人习惯,搭任何系统前先定原则。回测框架尤其如此。为什么?因为期权策略太敏感了——波动率、时间衰减、跳跃风险,哪个没处理好,结果都是灾难。
核心原则就三条:
- 模块化:数据、策略、风控、执行,各管各的。别写成一坨屎山。
- 可复现:同样的参数,跑一百次结果必须一样。随机种子、时间戳都得固定。
- 逼真性:回测环境越接近实盘越好。滑点、手续费、冲击成本,一个都不能少。
我在项目中遇到过一件事:有个同事的回测引擎,滑点模型写死了固定1个tick。结果实盘时,深度虚值期权流动性极差,滑点直接飙到5个tick。策略瞬间崩盘。你说这锅该谁背?
所以,设计原则不是摆设。它们是防火墙。
二、事件驱动回测引擎:让系统「活」起来
回测引擎有两种主流架构:向量化(vectorized)和事件驱动(event-driven)。
向量化快,但不够灵活。事件驱动慢一点,但能处理复杂逻辑——比如期权组合的保证金计算、行权指派、波动率曲面更新。我个人强烈推荐事件驱动,尤其是做期权套利。
事件驱动的核心是什么?说白了,就是「发生什么事,就触发什么逻辑」。
举个例子:
- 市场数据来了 → 更新波动率曲面 → 检查是否有套利机会
- 信号触发了 → 生成订单 → 检查保证金 → 发送到模拟交易所
- 成交回报来了 → 更新持仓 → 计算盈亏 → 记录日志
你看,每个步骤都是独立的。出了问题,定位也快。
下面是一个简化的事件驱动引擎核心代码。别嫌简单,骨架对了,往上加肉就行。
class Event:
"""事件基类"""
def __init__(self, event_type, timestamp, data):
self.type = event_type
self.timestamp = timestamp
self.data = data
class EventEngine:
"""事件驱动引擎"""
def __init__(self):
self.handlers = {} # 事件类型 → 处理函数列表
self.queue = [] # 事件队列
def register_handler(self, event_type, handler):
if event_type not in self.handlers:
self.handlers[event_type] = []
self.handlers[event_type].append(handler)
def put_event(self, event):
self.queue.append(event)
def run(self):
while self.queue:
event = self.queue.pop(0)
if event.type in self.handlers:
for handler in self.handlers[event.type]:
handler(event)
# 使用示例
engine = EventEngine()
engine.register_handler('MARKET_DATA', handle_market_data)
engine.register_handler('SIGNAL', handle_signal)
engine.put_event(Event('MARKET_DATA', '2024-01-01 09:30:00', {...}))
engine.run()
我的经验:事件队列别用Python list。数据量大了,pop(0)是O(n)的。用collections.deque,或者直接用asyncio.Queue。我曾经因为这个问题,回测跑了三天没跑完……后来换了deque,两小时搞定。
三、滑点与交易成本模型:回测的「照妖镜」
回测里最容易被忽略的,就是滑点和交易成本。你想想看,期权交易成本比股票高多了——买卖价差、手续费、保证金占用利息,还有冲击成本。
我曾经见过一个策略,回测时没考虑滑点,年化收益30%。加上1个tick的滑点,直接变成5%。加上手续费,变成负的。你说这策略还能用吗?
所以,滑点和成本模型必须做,而且要做细。
3.1 滑点模型
滑点分两种:固定滑点和比例滑点。固定滑点就是每笔交易加一个固定tick数。比例滑点按成交金额的百分比算。
我个人建议用混合模型:
- 流动性好的合约(比如平值期权):固定1-2个tick
- 流动性差的合约(深度实/虚值):比例滑点,按买卖价差的一定比例
下面是一个简单的滑点模型实现:
class SlippageModel:
def __init__(self, base_tick=0.5, spread_ratio=0.5):
self.base_tick = base_tick # 基础滑点(tick数)
self.spread_ratio = spread_ratio # 买卖价差比例
def calculate_slippage(self, option, trade_direction):
"""
option: 期权合约对象(包含bid, ask, last_price等)
trade_direction: 'buy' 或 'sell'
"""
spread = option.ask - option.bid
if spread <= 0:
# 异常情况,用基础滑点
return self.base_tick * option.tick_size
# 流动性判断:买卖价差越小,流动性越好
if spread <= 2 * option.tick_size:
# 流动性好,用固定滑点
slippage = self.base_tick * option.tick_size
else:
# 流动性差,用比例滑点
slippage = spread * self.spread_ratio
# 买入时滑点向上,卖出时滑点向下
if trade_direction == 'buy':
return slippage
else:
return -slippage
避坑指南:我曾经在回测中忽略了「期权行权指派」带来的滑点。实盘时,被行权的期权在最后一天流动性极差,滑点直接吃掉了我一个月的利润。所以,记得在回测中加入「到期日流动性衰减」模型。
3.2 交易成本模型
交易成本不只是手续费。期权交易的成本包括:
| 成本类型 | 说明 | 典型值(A股期权) |
|---|---|---|
| 交易所手续费 | 按张收取 | 1.6元/张(双边) |
| 结算费 | 按张收取 | 0.3元/张 |
| 行权手续费 | 行权时收取 | 1.6元/张 |
| 保证金占用利息 | 卖出期权需缴纳保证金 | 按资金成本计算 |
| 冲击成本 | 大单交易对价格的影响 | 视流动性而定 |
我个人习惯,把这些成本统一封装成一个CostModel类。这样策略代码里只需要调用一个接口,清爽得很。
class CostModel:
def __init__(self, commission_per_lot=1.6, settlement_per_lot=0.3,
exercise_fee=1.6, margin_rate=0.05, funding_rate=0.03):
self.commission_per_lot = commission_per_lot
self.settlement_per_lot = settlement_per_lot
self.exercise_fee = exercise_fee
self.margin_rate = margin_rate
self.funding_rate = funding_rate
def total_cost(self, trade):
"""
trade: 交易对象(包含合约、数量、方向、持仓时间等)
返回总成本(元)
"""
lots = abs(trade.quantity)
# 手续费
commission = self.commission_per_lot * lots * 2 # 双边
# 结算费
settlement = self.settlement_per_lot * lots * 2
# 行权费(如果是到期行权)
exercise = 0
if trade.is_exercise:
exercise = self.exercise_fee * lots
# 保证金占用利息(卖出开仓)
margin_interest = 0
if trade.direction == 'sell':
margin = trade.option.margin * lots * self.margin_rate
margin_interest = margin * self.funding_rate * (trade.hold_days / 365)
return commission + settlement + exercise + margin_interest
四、本章知识体系图
下面这张图,把回测系统的核心逻辑串起来了。你仔细看看,每个模块之间怎么交互的。
我的建议:别一上来就写全功能。先搭一个最小可用版本——能跑通一条数据、一个策略、一笔交易就行。然后逐步加滑点、加成本、加风控。迭代开发,比一次性搞个大而全的框架靠谱得多。
好了,这一章的内容就到这里。回测框架的骨架我们已经搭好了,下一章我们会在骨架上填肉——具体讲讲怎么实现波动率曲面更新、组合保证金计算,还有绩效归因。嗯,都是实战中必须啃的硬骨头。
公众号:蓝海资料掘金营,微信deep3321