主题切换
4.1 回测引擎原理
概念详解
回测引擎是量化策略研究的核心基础设施。它的本质是一个模拟器:在历史数据上按时间顺序"重放"市场行情,根据策略在每个时点产生的信号模拟买卖行为,最终产生一条模拟的净值曲线和一系列绩效指标。
在设计回测引擎时,我们需要回答三个根本问题:
- 时间推进方式:是按自然日推进、按交易日推进,还是仅在事件发生时推进?
- 订单执行模型:下单后何时成交?以什么价格成交?能否全部成交?
- 状态维护:如何管理持仓、现金、保证金等账户状态?
事件驱动 vs 向量化回测
名词解释:前视偏差
在回测中无意使用了未来才能获知的信息,导致结果过于乐观。常见于使用全样本统计量或未来数据。
名词解释:事件驱动回测
按时间顺序逐个处理市场数据点、信号事件和订单事件,模拟真实的交易流程。每个时间步依次处理行情更新、信号生成、订单撮合和持仓更新。
名词解释:向量化回测
利用矩阵/向量的批量运算一次性计算整个回测期的收益。速度快但难以处理路径依赖的逻辑,如动态止损、保证金管理等。
两种架构的对比:
| 维度 | 事件驱动 | 向量化 |
|---|---|---|
| 速度 | 较慢 | 极快 |
| 灵活性 | 高(可处理复杂逻辑) | 低(依赖路径独立假设) |
| 精度 | 高(可模拟逐笔成交) | 中(通常用收盘价近似) |
| 实现难度 | 高 | 低 |
| 适用场景 | 高频、复杂风控 | 中低频、因子回测 |
数学原理
回测的核心是净值曲线的计算。设
其中
在事件驱动框架中,我们还需要显式建模资金流转:
Python实战
📌 案例:向量化回测
此处有展示代码展开 ▼
python
import pandas as pd
import numpy as np
# 假设df包含:Close(收盘价)、signal(0/1信号,1表示持有)
returns = df['Close'].pct_change()
strategy_returns = returns * df['signal'].shift(1)
cumulative = (1 + strategy_returns).cumprod()点击展开可浏览运行结果
--- 结果 --- returns tail(): 2024-09-04 0.000190 2024-09-05 0.000018 2024-09-06 0.000294 2024-09-07 -0.000227 2024-09-08 -0.000039 Freq: D strategy_returns tail(): 2024-09-04 0.0 2024-09-05 0.0 2024-09-06 0.0 2024-09-07 -0.0 2024-09-08 -0.0 Freq: D cumulative tail(): 2024-09-04 1.002286 2024-09-05 1.002286 2024-09-06 1.002286 2024-09-07 1.002286 2024-09-08 1.002286 Freq: D
📌 案例:事件驱动回测框架
此处有展示代码展开 ▼
python
class EventDrivenBacktest:
def __init__(self, initial_capital=1_000_000):
self.initial_capital = initial_capital
self.cash = initial_capital
self.position = {}
self.equity_curve = []
self.trades = []
def on_bar(self, timestamp, prices, signals):
# 处理信号并生成订单
for symbol, target_weight in signals.items():
current_value = self.position.get(symbol, 0) * prices[symbol]
target_value = self.cash * target_weight
trade_value = target_value - current_value
if abs(trade_value) > 1000: # 最小交易金额
self.execute_order(timestamp, symbol, trade_value, prices[symbol])
self.update_equity(timestamp, prices)
def execute_order(self, timestamp, symbol, value, price):
qty = int(value / price)
if qty == 0:
return
cost = abs(qty * price) * 0.001 # 手续费
self.cash -= value + cost
self.position[symbol] = self.position.get(symbol, 0) + qty
self.trades.append({
'timestamp': timestamp, 'symbol': symbol,
'qty': qty, 'price': price, 'cost': cost
})
def update_equity(self, timestamp, prices):
total_value = self.cash + sum(
qty * prices.get(sym, 0) for sym, qty in self.position.items()
)
self.equity_curve.append({'timestamp': timestamp, 'equity': total_value})
# 使用示例
engine = EventDrivenBacktest(initial_capital=1_000_000)
for timestamp, row in market_data.iterrows():
signals = strategy.generate_signals(timestamp)
engine.on_bar(timestamp, row['prices'], signals)点击展开可浏览运行结果
📘 本段代码定义了 1 个函数/类:类 `EventDrivenBacktest`。该片段为教学展示(未包含独立运行的输入数据),可在实战练习中结合真实数据调用。
📌 案例:回测引擎的核心循环(完整版)
此处有展示代码展开 ▼
python
def run_backtest(strategy, data, start_date, end_date):
"""
完整的回测主循环,模拟逐日交易流程
"""
# 初始化
capital = 1_000_000
positions = {} # symbol -> shares
equity_curve = []
# 按日期遍历
trading_dates = data[start_date:end_date].index
prev_positions = {}
for i, date in enumerate(trading_dates):
# Step 1: 获取当日数据(仅使用截止当日的已知数据)
historical_data = data.loc[:date]
current_prices = data.loc[date, 'Close']
# Step 2: 计算当前组合市值
portfolio_value = capital
for sym, shares in positions.items():
portfolio_value += shares * current_prices[sym]
# Step 3: 生成信号(关键:只用历史数据,不用未来数据)
signals = strategy.predict(historical_data)
# Step 4: 计算目标持仓(组合优化 + 风控约束)
target_weights = optimize_portfolio(
signals,
historical_data.pct_change().dropna().cov(),
max_weight=0.1,
max_turnover=0.3
)
# Step 5: 执行交易(含成本)
for sym, target_w in target_weights.items():
target_value = portfolio_value * target_w
current_value = positions.get(sym, 0) * current_prices[sym]
trade_value = target_value - current_value
# 买入/卖出
trade_shares = int(trade_value / current_prices[sym])
commission = abs(trade_shares * current_prices[sym]) * 0.0003
slippage = abs(trade_shares * current_prices[sym]) * 0.0002
capital -= (trade_value + commission + slippage)
positions[sym] = positions.get(sym, 0) + trade_shares
# Step 6: 记录净值
final_value = capital + sum(
positions.get(sym, 0) * current_prices[sym] for sym in current_prices.index
)
equity_curve.append({'date': date, 'equity': final_value})
return pd.DataFrame(equity_curve).set_index('date')点击展开可浏览运行结果
📘 本段代码定义了 1 个函数/类:函数 `run_backtest`(完整的回测主循环,模拟逐日交易流程)。该片段为教学展示(未包含独立运行的输入数据),可在实战练习中结合真实数据调用。
常见误区
误以为向量化回测足够精确:向量化无法处理路径依赖逻辑(如"回撤超过5%则清仓"),这类策略必须用事件驱动回测。
忘记
shift(1)导致前视偏差:在向量化回测中,信号必须滞后一期使用。returns * df['signal'].shift(1)是正确的,returns * df['signal']是错的。忽略停牌和涨跌停:停牌日收益率为0不代表可以交易。需要检查
volume > 0和tradable标记。用全样本数据做标准化:对因子做Z-score标准化时,必须用每期之前的数据计算均值和标准差,不能用全样本统计量。
忽略复权数据:使用未复权的价格会导致分红、拆股造成虚假的收益跳变。必须用后复权价格。
实战练习
编写一个简单的向量化回测函数,输入DataFrame(含收盘价和信号列),输出净值曲线、年化收益和最大回撤。并用随机信号验证:完全随机的信号理论上收益应接近零(扣除成本后为负)。
实现事件驱动框架中的
on_bar方法,使其支持:- 固定比例的手续费(如万三)
- 单只股票上限10%的约束
- 日换手率不超过30%的约束
思考题:如果策略需要在盘中(9:45)根据开盘价信号交易,但回测只有日线数据,会产生什么问题?如何减轻这个问题?
延伸阅读
- Chan, E. P. (2013). Algorithmic Trading: Winning Strategies and Their Rationale. Wiley. 第1-3章详细讨论回测框架设计。
- Narang, R. K. (2013). Inside the Black Box. Wiley. 深入解析量化交易系统架构。
- Lopez de Prado, M. (2018). Advances in Financial Machine Learning. Wiley. 第7章讨论回测过拟合问题。
backtrader开源回测框架:https://github.com/mementum/backtraderzipline开源回测框架:https://github.com/quantopian/zipline(Quantopian已关闭,社区维护中)
本章要点
- 回测引擎是量化策略研究的"实验平台",分为事件驱动和向量化两种架构。
- 事件驱动回测按时间顺序处理每个事件,灵活性高、精度高;向量化回测速度快但无法处理路径依赖逻辑。
- 净值曲线计算的核心公式:
。 - 回测中最关键的纪律:每个时点只能使用该时点之前的信息,这是避免前视偏差的唯一方法。
- 实现完整的回测引擎需要处理:信号生成、持仓计算、订单执行、成本扣除、净值记录五个步骤。
- 必须使用复权价格数据,检查股票可交易状态,并在每期使用递归窗口做统计计算。