3、策略回测框架:回测的核心概念与Backtrader实战

各位同学好,我是你们的讲师。今天咱们聊聊回测框架。说实话,我见过太多人把回测当成「过家家」——跑个曲线,看个收益,就觉得稳了。结果实盘一上,亏得亲妈都不认识。

为什么会这样?因为回测里藏着太多坑。滑点、手续费、资金管理,任何一个没处理好,你的「完美策略」都可能变成灾难。今天我就把这些核心概念掰开揉碎,再带大家上手Backtrader这个利器。

核心观点:回测不是验证策略「能不能赚钱」,而是验证策略「在真实市场中会不会死」。滑点、手续费、资金管理,就是决定生死的三个关卡。

3.1 回测的核心概念

3.1.1 滑点(Slippage)

滑点说白了,就是你下单的价格和实际成交价格之间的差价。我刚开始做回测时,天真地以为「想买就能买到,想卖就能卖掉」。直到有一次,我在回测里年化收益80%,实盘一跑直接腰斩。查了半天,罪魁祸首就是滑点。

滑点产生的原因主要有三个:

  • 市场流动性不足:你挂100手买单,但卖单只有50手,剩下的只能追高
  • 交易延迟:从你点击「下单」到交易所收到指令,这几十毫秒里价格可能已经变了
  • 大单冲击:你的订单本身就会影响市场价格,尤其是小市值股票

在回测中,我们通常用固定滑点或百分比滑点来模拟。我个人习惯用万分之三到万分之五,这个范围比较贴近A股实际情况。

避坑指南:我曾经见过有人把滑点设成0.0001%,回测曲线漂亮得像艺术品。结果实盘第一天就亏了2%。记住:滑点宁可高估,不要低估。

3.1.2 手续费(Commission)

手续费这块,很多人觉得「不就是万几嘛,影响不大」。你想想看,高频策略一天交易几十次,手续费累积起来可能吃掉你一半的利润。

常见的费用包括:

  • 佣金:券商收取的交易费用,A股一般是万2.5到万3
  • 印花税:卖出时收取,目前是千分之一
  • 过户费:沪深两市都有,万0.2左右
  • 平台费:有些量化平台会额外收取

在Backtrader里,我们可以这样设置手续费:

import backtrader as bt

cerebro = bt.Cerebro()
# 设置佣金为万分之三
cerebro.broker.setcommission(commission=0.0003)
# 也可以按固定金额,比如每笔5元
cerebro.broker.setcommission(commission=5.0, mult=1.0)

注意:不同品种的手续费差异很大。期货是万分之几,但还有保证金占用;ETF是万分之零点几;期权更复杂。一定要根据实际交易品种来设置。

3.1.3 资金管理(Money Management)

资金管理,说白了就是「每次该下多少注」。我见过最惨的案例:一个策略回测收益300%,实盘时全仓干进去,结果连续三次止损,本金直接腰斩。

常见的资金管理方法:

  • 固定仓位:每次固定买入100股或1手,简单粗暴
  • 固定比例:每次用总资金的固定比例(比如10%)开仓
  • 凯利公式:根据胜率和盈亏比计算最优仓位
  • 风险平价:让每个品种的风险贡献相等

我个人比较推荐固定比例法,尤其是新手。比如每次只用总资金的2%去冒险,就算连续亏10次,也只亏20%,还有翻盘的机会。

3.2 Backtrader框架入门

Backtrader是我用得最多的回测框架。为什么选它?因为它是纯Python,扩展性强,社区活跃。说白了,你想要的它基本都有,没有的也能自己写。

3.2.1 核心组件

Backtrader有五个核心组件,我画了个图帮你理解:

Backtrader核心组件架构图 Cerebro(大脑) Data Feed(数据源) Strategy(策略) Broker(经纪人) Analyzer(分析器) Observer(观察者) Cerebro协调数据、策略、经纪人,分析器计算指标,观察者监控运行状态

简单解释一下:

  • Cerebro:整个框架的大脑,负责协调所有组件
  • Data Feed:喂给策略的数据,比如日K线、分钟线
  • Strategy:你的交易逻辑,什么时候买、什么时候卖
  • Broker:模拟真实券商,处理订单、计算资金
  • Analyzer/Observer:分析回测结果,监控运行过程

3.2.2 快速上手

来看一个最简单的例子,实现一个「金叉买入,死叉卖出」的策略:

import backtrader as bt
import backtrader.indicators as btind

class GoldenCrossStrategy(bt.Strategy):
    params = (
        ('fast_period', 10),
        ('slow_period', 30),
    )
    
    def __init__(self):
        # 计算快线和慢线
        self.fast_ma = btind.SMA(self.data.close, period=self.params.fast_period)
        self.slow_ma = btind.SMA(self.data.close, period=self.params.slow_period)
        # 金叉信号
        self.crossover = btind.CrossOver(self.fast_ma, self.slow_ma)
    
    def next(self):
        # 没有持仓时,金叉买入
        if not self.position:
            if self.crossover > 0:
                self.buy()
        # 有持仓时,死叉卖出
        elif self.crossover < 0:
            self.sell()

# 创建大脑
cerebro = bt.Cerebro()
# 添加数据
data = bt.feeds.YahooFinanceData(dataname='AAPL', fromdate='2020-01-01', todate='2023-12-31')
cerebro.adddata(data)
# 添加策略
cerebro.addstrategy(GoldenCrossStrategy)
# 设置初始资金
cerebro.broker.setcash(100000.0)
# 运行回测
results = cerebro.run()
# 画图
cerebro.plot()

小技巧:刚开始用Backtrader时,建议先用内置的YahooFinance数据测试。等熟悉了再换成自己的数据源。我当年就是直接上自己的数据,结果格式不对折腾了一整天。

3.3 自定义策略编写

内置策略不够用?那就自己写。Backtrader的策略编写其实就三个核心方法:

  • __init__:初始化指标和变量
  • next:每根K线都会调用一次,在这里写交易逻辑
  • notify_order:订单状态变化时触发,用于处理成交、撤单等

来看一个更复杂的例子,加入止损和仓位管理:

class MyStrategy(bt.Strategy):
    params = (
        ('rsi_period', 14),
        ('rsi_upper', 70),
        ('rsi_lower', 30),
        ('risk_per_trade', 0.02),  # 每次冒险2%的资金
    )
    
    def __init__(self):
        self.rsi = btind.RSI(self.data.close, period=self.params.rsi_period)
        self.order = None  # 跟踪当前订单
    
    def next(self):
        # 如果有未完成的订单,跳过
        if self.order:
            return
        
        # 计算仓位大小
        size = int(self.broker.getcash() * self.params.risk_per_trade / self.data.close[0])
        
        # 超卖区域买入
        if not self.position and self.rsi < self.params.rsi_lower:
            self.order = self.buy(size=size)
        
        # 超买区域卖出
        elif self.position and self.rsi > self.params.rsi_upper:
            self.order = self.sell(size=size)
    
    def notify_order(self, order):
        if order.status in [order.Completed, order.Canceled, order.Margin]:
            self.order = None

注意:在next方法里,一定要处理订单状态。我曾经犯过一个错误:连续下单导致资金不足,回测结果完全乱套。记住:每次下单前检查是否有未完成的订单。

3.4 回测结果评估指标

回测跑完了,怎么判断策略好不好?光看收益率曲线可不行。我见过太多「漂亮曲线」背后藏着巨大的风险。

3.4.1 夏普比率(Sharpe Ratio)

夏普比率衡量的是「每承担一单位风险,能获得多少超额收益」。说白了,就是你的策略值不值得冒险。

计算公式:

夏普比率 = (策略收益率 - 无风险利率) / 策略波动率

一般来说:

  • 夏普比率 > 1:还不错
  • 夏普比率 > 2:很好
  • 夏普比率 > 3:非常优秀(但要小心过拟合)

在Backtrader里计算夏普比率:

cerebro.addanalyzer(bt.analyzers.SharpeRatio, _name='sharpe')
results = cerebro.run()
sharpe = results[0].analyzers.sharpe.get_analysis()
print(f"夏普比率: {sharpe['sharperatio']:.2f}")

3.4.2 最大回撤(Max Drawdown)

最大回撤,就是策略从最高点跌到最低点的最大幅度。这个指标比收益率更重要。为什么?因为回撤超过30%,大部分人就扛不住了,会手动止损,然后完美错过后面的反弹。

我个人的经验:

  • 回撤 < 10%:很稳健
  • 回撤 10%-20%:可以接受
  • 回撤 20%-30%:需要警惕
  • 回撤 > 30%:建议重新审视策略
cerebro.addanalyzer(bt.analyzers.DrawDown, _name='drawdown')
results = cerebro.run()
dd = results[0].analyzers.drawdown.get_analysis()
print(f"最大回撤: {dd['max']['drawdown']:.2f}%")

3.4.3 年化收益(Annualized Return)

年化收益是把总收益换算成一年的收益率。注意,不是简单的总收益除以年数,而是用复利计算。

举个例子:你回测了3年,总收益50%,那年化收益不是50%/3=16.67%,而是:

年化收益 = (1 + 0.5)^(1/3) - 1 = 14.47%

在Backtrader里:

cerebro.addanalyzer(bt.analyzers.AnnualReturn, _name='annual')
results = cerebro.run()
annual = results[0].analyzers.annual.get_analysis()
print(f"年化收益: {annual['annual_return']:.2f}%")

3.4.4 综合评估表

我把这几个指标整理成了一张表,方便你对照:

指标 含义 优秀值 警戒值
夏普比率 风险调整后收益 > 2.0 < 1.0
最大回撤 最大亏损幅度 < 10% > 20%
年化收益 复利年化收益率 > 20% < 10%
胜率 盈利交易占比 > 60% < 40%
盈亏比 平均盈利/平均亏损 > 2.0 < 1.5

重要提醒:不要只看单一指标。一个策略可能夏普比率很高,但最大回撤也很大。要综合评估,尤其是要看看资金曲线是否平滑。我见过最坑的策略:夏普比率2.5,但最大回撤40%,这种策略实盘根本拿不住。

好了,这一章的内容就到这里。回测框架是量化交易的基石,滑点、手续费、资金管理这三个概念一定要吃透。Backtrader是个好工具,但工具再好,也得看你怎么用。下一章我们会深入讲数据获取和清洗,那是另一个容易踩坑的地方。


专注资料整理