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 回测流程可视化
下图展示了从策略定义到结果输出的完整数据流:
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'])