Skip to content

4.1 回测引擎原理

概念详解

回测引擎是量化策略研究的核心基础设施。它的本质是一个模拟器:在历史数据上按时间顺序"重放"市场行情,根据策略在每个时点产生的信号模拟买卖行为,最终产生一条模拟的净值曲线和一系列绩效指标。

在设计回测引擎时,我们需要回答三个根本问题:

  1. 时间推进方式:是按自然日推进、按交易日推进,还是仅在事件发生时推进?
  2. 订单执行模型:下单后何时成交?以什么价格成交?能否全部成交?
  3. 状态维护:如何管理持仓、现金、保证金等账户状态?

事件驱动 vs 向量化回测

名词解释:前视偏差

在回测中无意使用了未来才能获知的信息,导致结果过于乐观。常见于使用全样本统计量或未来数据。

名词解释:事件驱动回测

按时间顺序逐个处理市场数据点、信号事件和订单事件,模拟真实的交易流程。每个时间步依次处理行情更新、信号生成、订单撮合和持仓更新。

名词解释:向量化回测

利用矩阵/向量的批量运算一次性计算整个回测期的收益。速度快但难以处理路径依赖的逻辑,如动态止损、保证金管理等。

两种架构的对比:

维度事件驱动向量化
速度较慢极快
灵活性高(可处理复杂逻辑)低(依赖路径独立假设)
精度高(可模拟逐笔成交)中(通常用收盘价近似)
实现难度
适用场景高频、复杂风控中低频、因子回测

数学原理

回测的核心是净值曲线的计算。设 t 时刻的策略收益率为:

rtstrategy=i=1nwi,t1ri,t

其中 wi,t1 是资产 it1 时刻的权重(基于 t1 时刻已知信息决定),ri,t 是资产 it 时刻的收益率。净值曲线为:

NAVt=NAV0k=1t(1+rkstrategy)

在事件驱动框架中,我们还需要显式建模资金流转:

Casht=Casht1iPricei,tQtyi,tbuy+iPricei,tQtyi,tsellPositioni,t=Positioni,t1+Qtyi,tbuyQtyi,tsell

Python实战

📌 案例:向量化回测

PYTHON7 行 · 252 B
📄此处有展示代码7 行 · 252 B展开 ▼
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

📌 案例:事件驱动回测框架

PYTHON41 行 · 1.6 KB
📄此处有展示代码41 行 · 1.6 KB展开 ▼
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`。该片段为教学展示(未包含独立运行的输入数据),可在实战练习中结合真实数据调用。

📌 案例:回测引擎的核心循环(完整版)

PYTHON55 行 · 2.0 KB
📄此处有展示代码55 行 · 2.0 KB展开 ▼
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`(完整的回测主循环,模拟逐日交易流程)。该片段为教学展示(未包含独立运行的输入数据),可在实战练习中结合真实数据调用。

常见误区

  1. 误以为向量化回测足够精确:向量化无法处理路径依赖逻辑(如"回撤超过5%则清仓"),这类策略必须用事件驱动回测。

  2. 忘记 shift(1) 导致前视偏差:在向量化回测中,信号必须滞后一期使用。returns * df['signal'].shift(1) 是正确的,returns * df['signal'] 是错的。

  3. 忽略停牌和涨跌停:停牌日收益率为0不代表可以交易。需要检查 volume > 0tradable 标记。

  4. 用全样本数据做标准化:对因子做Z-score标准化时,必须用每期之前的数据计算均值和标准差,不能用全样本统计量。

  5. 忽略复权数据:使用未复权的价格会导致分红、拆股造成虚假的收益跳变。必须用后复权价格。

实战练习

  1. 编写一个简单的向量化回测函数,输入DataFrame(含收盘价和信号列),输出净值曲线、年化收益和最大回撤。并用随机信号验证:完全随机的信号理论上收益应接近零(扣除成本后为负)。

  2. 实现事件驱动框架中的 on_bar 方法,使其支持:

    • 固定比例的手续费(如万三)
    • 单只股票上限10%的约束
    • 日换手率不超过30%的约束
  3. 思考题:如果策略需要在盘中(9:45)根据开盘价信号交易,但回测只有日线数据,会产生什么问题?如何减轻这个问题?

延伸阅读

本章要点

  • 回测引擎是量化策略研究的"实验平台",分为事件驱动向量化两种架构。
  • 事件驱动回测按时间顺序处理每个事件,灵活性高、精度高;向量化回测速度快但无法处理路径依赖逻辑。
  • 净值曲线计算的核心公式:NAVt=NAV0(1+rkstrategy)
  • 回测中最关键的纪律:每个时点只能使用该时点之前的信息,这是避免前视偏差的唯一方法。
  • 实现完整的回测引擎需要处理:信号生成、持仓计算、订单执行、成本扣除、净值记录五个步骤。
  • 必须使用复权价格数据,检查股票可交易状态,并在每期使用递归窗口做统计计算。