Skip to content

第25章 因子回测框架搭建:回测引擎设计、滑点与手续费模拟、基准选择、绩效评估指标

因子挖掘出来之后,最怕什么?

怕它是个「纸上英雄」。实盘一跑,全完蛋。

所以回测框架就是你的照妖镜。我做了这么多年量化,见过太多人在回测上栽跟头——要么过度拟合,要么忽略交易成本,要么基准选错。今天咱们就把这个框架彻底讲透。

25.1 回测引擎设计:核心架构

回测引擎说白了就是一个时间机器。你把历史数据喂进去,它模拟你在过去某个时间点买卖,最后告诉你赚了还是亏了。

我个人习惯把引擎拆成三个模块:

  • 数据模块:负责喂价格、成交量、因子值
  • 策略模块:根据因子信号生成买卖指令
  • 执行模块:模拟订单成交、记录持仓

嗯,这里要注意一个坑——未来函数。我在项目中遇到过有人用当天的收盘价去判断当天的买入信号,这等于作弊。正确的做法是:用T-1日的因子值,去预测T日的收益。

核心原则:回测中所有信息,必须是你在那个时间点真实能拿到的。

下面是一个极简的回测引擎骨架,我用Python写给你看:

class BacktestEngine:
    def __init__(self, data, factor_col, price_col='close'):
        self.data = data
        self.factor_col = factor_col
        self.price_col = price_col
        self.positions = []
        self.returns = []

    def run(self):
        # 按时间逐日推进
        for i in range(1, len(self.data)):
            # 用前一天的因子值判断
            signal = self.data.iloc[i-1][self.factor_col]
            # 生成持仓信号(这里简化:因子大于0就做多)
            if signal > 0:
                self.positions.append(1)
            else:
                self.positions.append(-1)
            # 计算当日收益
            ret = self.data.iloc[i][self.price_col] / self.data.iloc[i-1][self.price_col] - 1
            self.returns.append(ret * self.positions[-1])
        return self.returns

你看,逻辑其实不复杂。但真实场景下,你得处理停牌、涨跌停、分红除权这些细节。我曾经因为没处理除权,回测收益虚高了30%,后来排查了整整两天。

25.2 滑点与手续费模拟:别让利润被吃掉

很多新手回测时收益曲线漂亮得不行,一上实盘就亏。为什么?

说白了,就是没考虑交易成本。

滑点:你下单的价格和实际成交价格之间的差异。流动性差的股票,滑点可能高达0.5%。

手续费:佣金、印花税、过户费。A股目前佣金普遍万2.5,印花税千1(卖出时收)。

我建议你这样模拟:

  • 滑点:固定比例,比如0.1%~0.3%
  • 手续费:按实际费率计算,双边或单边

我的经验:回测时把滑点设得比实际高一点,比如0.2%。这样跑出来的结果更保守,也更接近实盘。宁可少赚,不能假赚。

代码实现也很直接:

def apply_trading_cost(returns, slippage=0.001, commission=0.00025):
    """
    对每日收益率扣除滑点和手续费
    slippage: 滑点比例,默认0.1%
    commission: 佣金比例,默认万2.5
    """
    # 假设每天换仓一次
    cost = slippage + commission * 2  # 买入卖出各一次佣金
    net_returns = [r - cost for r in returns]
    return net_returns

你想想看,如果每天换仓,一年250个交易日,光手续费就能吃掉多少?所以高频因子对成本极其敏感。我见过一个因子,扣除成本前年化20%,扣除后直接变负——这就是典型的「伪因子」。

25.3 基准选择:你的对手是谁

回测不能只看绝对收益。你得问自己:我跑赢大盘了吗?

基准选择有几个原则:

  • 相关性:你的股票池是沪深300,基准就用沪深300
  • 可投资性:基准本身要能复制,比如用指数ETF
  • 一致性:回测期间不要换基准

我记得有一次帮朋友看策略,他选的是中证500做基准,但持仓全是银行股。这明显不对——银行股跟大盘蓝筹更相关,应该用沪深300才对。

注意:不要为了美化业绩而选一个弱势基准。比如2018年熊市,你选个债券指数做基准,那股票策略肯定跑赢——但这没有意义。

基准数据怎么拿?一般用万得或聚宽的数据接口。如果你自己搭框架,可以直接下载指数日线数据:

import pandas as pd
# 假设你已经有了基准的每日收益率
benchmark_returns = pd.Series([0.01, -0.005, 0.02, ...])
strategy_returns = pd.Series([0.015, -0.003, 0.025, ...])

25.4 绩效评估指标:用数字说话

回测跑完了,一堆收益率数据摆在那。怎么判断这个因子好不好?

我一般看四个核心指标:

指标 含义 理想值
夏普比率 每承担一单位风险,获得多少超额收益 > 1.0(优秀),> 2.0(极好)
最大回撤 从最高点到最低点的最大亏损幅度 < 20%(股票策略)
胜率 盈利交易次数 / 总交易次数 > 50%
年化收益率 折算成一年的平均收益率 看市场环境,一般 > 10%

夏普比率怎么算?公式是:

sharpe = (策略年化收益率 - 无风险利率) / 策略年化波动率

无风险利率一般用国债收益率,比如3%。

代码实现:

def calculate_sharpe(returns, risk_free_rate=0.03):
    excess_returns = returns - risk_free_rate / 252  # 日度化
    sharpe = np.sqrt(252) * excess_returns.mean() / excess_returns.std()
    return sharpe

def calculate_max_drawdown(cumulative_returns):
    peak = cumulative_returns.expanding().max()
    drawdown = (cumulative_returns - peak) / peak
    return drawdown.min()

def calculate_win_rate(returns):
    wins = sum(1 for r in returns if r > 0)
    return wins / len(returns)

这里有个细节:夏普比率对异常值很敏感。我遇到过因子在大部分时间表现平平,但某一个月突然暴涨,夏普被拉得很高。这种因子你敢用吗?反正我不敢。

我的建议:不要只看夏普。把最大回撤和胜率结合起来看。如果一个因子夏普2.0,但最大回撤40%,那它的风险其实很大。

25.5 知识体系总览

下面这张图,我把整个回测框架的核心逻辑画出来了。你一看就明白:

flowchart TD
    A[数据模块<br/>价格、成交量、因子值] --> B[策略模块<br/>因子信号 → 买卖指令]
    B --> C[执行模块<br/>模拟成交、持仓]
    C --> D[成本扣除<br/>滑点 + 手续费]
    D --> E[绩效评估<br/>夏普、回撤、胜率]
    E --> F[基准对比<br/>跑赢大盘了吗?]

从数据到策略,再到执行,最后扣除成本、评估绩效、对比基准——每一步都不能少。我见过有人跳过成本扣除,直接拿策略收益跟基准比,结果看起来跑赢10个点,实际扣完成本只跑赢2个点。

避坑指南:我曾经在回测中忘记考虑停牌股票。因子信号出来了,但股票停牌买不了,结果回测收益虚高。后来我加了一个「可交易」过滤条件,才把这个问题解决。

好了,这一章的内容就到这里。回测框架是因子挖掘的「质检员」,你花再多时间搭建都不为过。记住:回测不是目的,找到能稳定赚钱的因子才是。