Python量化:用Backtrader回测策略

Backtrader是Python生态中最成熟的量化回测框架之一,架构清晰、扩展性好。本文用一个均线交叉策略演示完整的回测流程。

Backtrader架构

Backtrader的核心组件:

  • Cerebro:大脑,负责组装所有组件并驱动回测
  • Data Feed:数据源,提供K线数据(OHLCV)
  • Strategy:策略,定义交易逻辑
  • Broker:经纪人,处理订单、管理资金和持仓
  • Analyzer:分析器,计算回测指标(收益率、夏普比率等)

安装

pip install backtrader matplotlib

数据准备

Backtrader支持多种数据源,最简单的是从CSV加载:

import backtrader as bt
import datetime

# 从Yahoo Finance格式的CSV加载
data = bt.feeds.YahooFinanceCSVData(
    dataname='aapl.csv',
    fromdate=datetime.datetime(2019, 1, 1),
    todate=datetime.datetime(2021, 6, 30),
)

也可以用pandas DataFrame:

import pandas as pd
import backtrader as bt

df = pd.read_csv('stock_data.csv', parse_dates=['date'], index_col='date')
data = bt.feeds.PandasData(dataname=df)

编写均线交叉策略

策略逻辑:短期均线(SMA10)上穿长期均线(SMA30)时买入,下穿时卖出。

import backtrader as bt

class SmaCross(bt.Strategy):
    params = (
        ('fast_period', 10),
        ('slow_period', 30),
    )

    def __init__(self):
        # 计算均线指标
        self.sma_fast = bt.indicators.SMA(
            self.data.close, period=self.params.fast_period
        )
        self.sma_slow = bt.indicators.SMA(
            self.data.close, period=self.params.slow_period
        )
        # 交叉信号: 1=金叉, -1=死叉
        self.crossover = bt.indicators.CrossOver(self.sma_fast, self.sma_slow)
        
        # 记录订单状态
        self.order = None

    def log(self, txt):
        dt = self.datas[0].datetime.date(0)
        print(f'{dt}: {txt}')

    def notify_order(self, order):
        if order.status in [order.Completed]:
            if order.isbuy():
                self.log(f'BUY executed, Price: {order.executed.price:.2f}, '
                         f'Cost: {order.executed.value:.2f}, '
                         f'Comm: {order.executed.comm:.2f}')
            else:
                self.log(f'SELL executed, Price: {order.executed.price:.2f}, '
                         f'Comm: {order.executed.comm:.2f}')
        self.order = None

    def next(self):
        # 有未完成的订单则跳过
        if self.order:
            return
        
        if not self.position:
            # 未持仓:金叉买入
            if self.crossover > 0:
                size = int(self.broker.getcash() * 0.95 / self.data.close[0])
                self.order = self.buy(size=size)
                self.log(f'BUY signal, Close: {self.data.close[0]:.2f}')
        else:
            # 持仓:死叉卖出
            if self.crossover < 0:
                self.order = self.sell(size=self.position.size)
                self.log(f'SELL signal, Close: {self.data.close[0]:.2f}')

核心方法:

  • __init__:定义指标,只执行一次
  • next:每根K线执行一次,这里写交易逻辑
  • notify_order:订单状态变化时回调

运行回测

import backtrader as bt
import datetime

def run_backtest():
    cerebro = bt.Cerebro()
    
    # 加载数据
    data = bt.feeds.YahooFinanceCSVData(
        dataname='aapl.csv',
        fromdate=datetime.datetime(2019, 1, 1),
        todate=datetime.datetime(2021, 6, 30),
    )
    cerebro.adddata(data)
    
    # 添加策略
    cerebro.addstrategy(SmaCross)
    
    # 初始资金
    cerebro.broker.setcash(100000.0)
    
    # 手续费
    cerebro.broker.setcommission(commission=0.001)  # 0.1%
    
    # 添加分析器
    cerebro.addanalyzer(bt.analyzers.SharpeRatio, _name='sharpe')
    cerebro.addanalyzer(bt.analyzers.Returns, _name='returns')
    cerebro.addanalyzer(bt.analyzers.DrawDown, _name='drawdown')
    cerebro.addanalyzer(bt.analyzers.TradeAnalyzer, _name='trades')
    
    # 运行
    print(f'Starting Portfolio Value: {cerebro.broker.getvalue():.2f}')
    results = cerebro.run()
    strat = results[0]
    print(f'Final Portfolio Value: {cerebro.broker.getvalue():.2f}')
    
    # 输出分析结果
    print_analysis(strat)
    
    # 绘制图表
    cerebro.plot(style='candlestick', volume=False)

def print_analysis(strat):
    sharpe = strat.analyzers.sharpe.get_analysis()
    returns = strat.analyzers.returns.get_analysis()
    drawdown = strat.analyzers.drawdown.get_analysis()
    trades = strat.analyzers.trades.get_analysis()
    
    print('
===== Backtest Results =====')
    print(f'Sharpe Ratio: {sharpe.get("sharperatio", "N/A")}')
    print(f'Total Return: {returns.get("rtot", 0) * 100:.2f}%')
    print(f'Max Drawdown: {drawdown.get("max", {}).get("drawdown", 0):.2f}%')
    
    total = trades.get('total', {})
    won = trades.get('won', {})
    lost = trades.get('lost', {})
    print(f'Total Trades: {total.get("total", 0)}')
    print(f'Won: {won.get("total", 0)}, Lost: {lost.get("total", 0)}')

if __name__ == '__main__':
    run_backtest()

参数优化

Backtrader内置参数优化功能,通过optstrategy替代addstrategy

cerebro.optstrategy(
    SmaCross,
    fast_period=range(5, 15),
    slow_period=range(20, 40, 5),
)

这会遍历所有参数组合,输出每组的回测结果。注意:参数优化容易过拟合,结果仅供参考。

实践建议

  1. 先在样本外数据验证:用2019-2020数据优化参数,然后在2021数据上验证
  2. 考虑滑点和手续费cerebro.broker.set_slippage_perc(0.001)
  3. 用百分比仓位管理:不要满仓操作
  4. 关注最大回撤:收益率高但回撤50%的策略实盘很难执行
# 添加固定百分比仓位管理
cerebro.addsizer(bt.sizers.PercentSizer, percents=95)

Backtrader的文档不算友好,但源码写得很清晰,遇到问题直接读源码往往比翻文档更快。