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),
)
这会遍历所有参数组合,输出每组的回测结果。注意:参数优化容易过拟合,结果仅供参考。
实践建议
- 先在样本外数据验证:用2019-2020数据优化参数,然后在2021数据上验证
- 考虑滑点和手续费:
cerebro.broker.set_slippage_perc(0.001) - 用百分比仓位管理:不要满仓操作
- 关注最大回撤:收益率高但回撤50%的策略实盘很难执行
# 添加固定百分比仓位管理
cerebro.addsizer(bt.sizers.PercentSizer, percents=95)
Backtrader的文档不算友好,但源码写得很清晰,遇到问题直接读源码往往比翻文档更快。