4. 第一个回测:编写简单均线策略、运行回测、查看结果

本章将带领你完成第一个完整的 Backtrader 回测流程:编写一个经典的“双均线交叉”策略,运行回测引擎,并解读输出结果。这是后续所有复杂策略的基础骨架。

4.1 策略逻辑:双均线交叉

我们选择最直观的趋势跟踪策略——金叉买入,死叉卖出:

  • 短期均线(快线):10 周期简单移动平均线(SMA10)
  • 长期均线(慢线):30 周期简单移动平均线(SMA30)
  • 买入信号:SMA10 上穿 SMA30(金叉)
  • 卖出信号:SMA10 下穿 SMA30(死叉)
💡 策略说明:双均线交叉是趋势策略的典型代表。金叉意味着短期趋势强于长期趋势,市场可能进入上升通道;死叉则相反。本策略在回测中不设止损或止盈,仅用于演示 Backtrader 的基本流程。

4.2 编写策略类

在 Backtrader 中,所有策略都继承自 bt.Strategy。我们需要重写 __init__ 和 next 两个核心方法:

import backtrader as bt

class SmaCross(bt.Strategy):
    # 定义均线参数,可在回测时动态调整
    params = (
        ('fast', 10),
        ('slow', 30),
    )

    def __init__(self):
        # 计算快慢均线
        self.sma_fast = bt.ind.SMA(period=self.params.fast)
        self.sma_slow = bt.ind.SMA(period=self.params.slow)
        # 创建交叉信号指标
        self.crossover = bt.ind.CrossOver(self.sma_fast, self.sma_slow)

    def next(self):
        # 检查是否有未完成的订单
        if self.position.size == 0:
            # 金叉:买入
            if self.crossover > 0:
                self.buy()
        elif self.position.size > 0:
            # 死叉:卖出
            if self.crossover < 0:
                self.close()
📌 关键代码解析:
  • bt.ind.SMA:Backtrader 内置的简单移动平均指标,自动处理数据对齐。
  • bt.ind.CrossOver:交叉信号指标,返回 +1(上穿)、-1(下穿)、0(无交叉)。
  • self.buy() / self.close():策略下单方法,默认以市价单执行。
  • self.position.size:当前持仓数量,0 表示空仓。

4.3 组装回测引擎

策略编写完成后,需要将其注入 Cerebro 引擎,并配置数据源、初始资金和佣金:

# 1. 创建 Cerebro 引擎
cerebro = bt.Cerebro()

# 2. 添加数据源(以本地 CSV 文件为例)
data = bt.feeds.YahooFinanceCSVData(
    dataname='./data/AAPL.csv',
    fromdate=datetime.datetime(2020, 1, 1),
    todate=datetime.datetime(2023, 12, 31)
)
cerebro.adddata(data)

# 3. 添加策略
cerebro.addstrategy(SmaCross)

# 4. 设置初始资金和佣金
cerebro.broker.setcash(100000.0)
cerebro.broker.setcommission(commission=0.001)  # 0.1% 佣金

# 5. 添加分析器(用于输出绩效指标)
cerebro.addanalyzer(bt.analyzers.SharpeRatio, _name='sharpe')
cerebro.addanalyzer(bt.analyzers.DrawDown, _name='drawdown')

# 6. 运行回测
print('初始资金: %.2f' % cerebro.broker.getvalue())
results = cerebro.run()
print('最终资金: %.2f' % cerebro.broker.getvalue())

4.4 回测流程可视化

下图展示了从策略定义到结果输出的完整数据流:

Backtrader 回测流程(双均线策略) 数据源 CSV / Yahoo / 本地 策略类 SmaCross Cerebro 引擎 组装 & 运行 回测执行 run() 交易日志 买卖记录 绩效分析 夏普 / 回撤 资金曲线 净值变化 📋 流程说明 数据源 → 策略定义 → Cerebro 组装 → run() 执行 → 输出交易日志、绩效指标和资金曲线

4.5 查看回测结果

运行上述代码后,控制台会输出初始资金和最终资金。但更详细的结果需要通过分析器获取:

# 获取第一个策略运行结果
strat = results[0]

# 输出夏普比率
sharpe = strat.analyzers.sharpe.get_analysis()
print('夏普比率:', sharpe.get('sharperatio', 'N/A'))

# 输出最大回撤
drawdown = strat.analyzers.drawdown.get_analysis()
print('最大回撤: %.2f%%' % drawdown['max']['drawdown'])
print('回撤周期: %d 天' % drawdown['max']['len'])

# 输出交易统计(需要额外添加 TradeAnalyzer)
cerebro.addanalyzer(bt.analyzers.TradeAnalyzer, _name='trades')
results = cerebro.run()
strat = results[0]
trades = strat.analyzers.trades.get_analysis()
print('总交易次数:', trades['total']['total'])
print('盈利交易:', trades['won']['total'])
print('亏损交易:', trades['lost']['total'])

4.6 结果解读与常见问题

指标 含义 参考范围
最终资金 回测结束时的账户总价值 高于初始资金为盈利
夏普比率 风险调整后的收益,衡量每单位风险获得的超额回报 > 1 良好,> 2 优秀
最大回撤 账户净值从峰值到谷底的最大跌幅 < 20% 可接受
胜率 盈利交易次数占总交易次数的比例 趋势策略通常 40%-60%
⚠️ 常见陷阱:
  • 未来函数:确保在 __init__ 中定义的指标不会用到未来数据。Backtrader 的指标默认只使用当前及历史数据,但自定义指标需谨慎。
  • 过拟合:不要根据历史数据反复调整参数(如均线周期)。应使用样本外数据验证策略有效性。
  • 佣金与滑点:实盘交易存在佣金和滑点成本。回测时务必设置合理的 commission 和 slippage,否则结果会过于乐观。

4.7 完整代码模板

将以下代码保存为 sma_cross_backtest.py,即可一键运行你的第一个回测:

import datetime
import backtrader as bt

class SmaCross(bt.Strategy):
    params = (('fast', 10), ('slow', 30))

    def __init__(self):
        self.sma_fast = bt.ind.SMA(period=self.params.fast)
        self.sma_slow = bt.ind.SMA(period=self.params.slow)
        self.crossover = bt.ind.CrossOver(self.sma_fast, self.sma_slow)

    def next(self):
        if self.position.size == 0:
            if self.crossover > 0:
                self.buy()
        elif self.position.size > 0:
            if self.crossover < 0:
                self.close()

if __name__ == '__main__':
    cerebro = bt.Cerebro()
    data = bt.feeds.YahooFinanceCSVData(
        dataname='AAPL.csv',
        fromdate=datetime.datetime(2020, 1, 1),
        todate=datetime.datetime(2023, 12, 31)
    )
    cerebro.adddata(data)
    cerebro.addstrategy(SmaCross)
    cerebro.broker.setcash(100000.0)
    cerebro.broker.setcommission(commission=0.001)
    cerebro.addanalyzer(bt.analyzers.SharpeRatio, _name='sharpe')
    cerebro.addanalyzer(bt.analyzers.DrawDown, _name='drawdown')
    cerebro.addanalyzer(bt.analyzers.TradeAnalyzer, _name='trades')

    print('初始资金: %.2f' % cerebro.broker.getvalue())
    results = cerebro.run()
    print('最终资金: %.2f' % cerebro.broker.getvalue())

    strat = results[0]
    print('夏普比率:', strat.analyzers.sharpe.get_analysis().get('sharperatio', 'N/A'))
    print('最大回撤: %.2f%%' % strat.analyzers.drawdown.get_analysis()['max']['drawdown'])