公司动态
【AI量化交易实战】第05讲:小雅初试锋芒——Backtrader回测引擎入门实战
开篇导语上一讲小晴建好了数据管道本地数据库里已经躺着清洗干净的历史行情。现在最激动人心的环节来了——让小雅分析师把这些数据喂进回测引擎检验策略到底能不能赚钱。很多量化新手有一个共同的经历在炒股软件上看到一个金叉信号觉得这还不简单吗然后兴冲冲地写了几行代码假装自己在做量化。但专业的策略验证远不止金叉买入死叉卖出——你需要知道这个策略在什么市场环境下表现好、什么环境下会亏钱、最大回撤你扛得住吗、胜率和盈亏比分别是多少。这一讲小雅正式入驻分析师工位。我们将使用Backtrader——Python生态中最成熟的事件驱动型回测框架——跑通第一个完整的数据→信号→绩效闭环。学完本节后你将能够理解事件驱动回测的原理和Backtrader的核心架构独立编写并回测一个双均线交叉策略读懂年化收益、最大回撤、夏普比率风险调整后收益的衡量指标等核心指标对比多个经典策略在同一标的上的表现差异1 为什么需要回测——历史上赚不到钱实盘大概率也赚不到概念讲解回测Backtesting的本质是用历史数据模拟真实交易——假设你穿越回到2021年1月1日手里有100万全程按策略规则买卖到2024年12月31日账户还剩多少钱。这听起来简单但回测的坑远比想象的多常见陷阱表现后果未来函数策略在t日使用了t1日才公布的数据回测净值完美实盘一塌糊涂幸存者偏差回测股票池只包含现在还活着的股票忽略了已经退市的倒霉蛋过度拟合参数调得刚好贴合历史数据换一个时间段就失效摩擦成本忽略没考虑佣金、印花税、滑点回测盈利的策略实盘可能亏损一个专业策略的验证流程应该是策略想法 → 历史回测 → 样本外测试 → 模拟盘交易 → 小资金实盘 → 正式上线在本课程的体系中每一步都有对应的Agent在把关——小雅负责前两步小瑾在后面负责风险审核小彤负责策略上线后的执行监控。代码演示先安装并验证Backtrader环境 Backtrader环境验证与第一个Hello World Backtrader官网https://www.backtrader.com/ import backtrader as bt # 创建一个最小化的回测引擎 cerebro bt.Cerebro() # 打印Backtrader的基本信息 print(fBacktrader版本{bt.__version__}) print(核心概念速览) print( Cerebro → 回测引擎的大脑统筹整个回测流程) print( Strategy → 策略逻辑定义何时买卖) print( DataFeed → 数据输入为策略提供价格信息) print( Broker → 虚拟券商模拟账户和订单) print( Analyzer → 绩效分析输出收益、回撤等指标) # 验证数据加载能力 import pandas as pd print(f\nPandas版本{pd.__version__}) print(环境验证完成Backtrader就绪)关键要点Backtrader是事件驱动Event-Driven架构——每来一根新K线就触发一次策略逻辑模拟真实交易中的逐根K线判断回测结果好 ≠ 实盘能赚钱但回测结果很差一定说明策略有问题——先用回测做一票否决手续费必须建模——A股佣金万1.5 印花税0.05%卖出时单边征收两者合计对高频策略影响巨大样本外测试是把数据分成两段前段用来开发策略和调参数后段只能跑一次杜绝过度拟合2 双均线策略——小雅的第一个交易系统概念讲解双均线交叉策略是量化交易的Hello World——它足够简单但包含了完整策略的所有要素。策略逻辑只有两句话金叉买入短期均线如5日均线从下方上穿长期均线如20日均线视为上涨趋势确立买入死叉卖出短期均线从上方下穿长期均线视为上涨趋势结束卖出均线Moving Average的本质是对价格序列做平滑处理——计算过去N个交易日的收盘价平均值。短期均线MA5反应快但噪声多长期均线MA20反应慢但趋势稳定。两者的交叉点就是市场短期力量和长期力量的交汇处。双均线策略属于趋势跟踪类——它不会买在最低点也不会卖在最高点。它赚的是趋势形成后中间段的那部分利润并承受趋势反转时从高点回吐的一部分作为代价。代码演示下面用Backtrader完整实现一个双均线策略并加载真实A股数据回测import backtrader as bt import pandas as pd import akshare as ak from datetime import datetime class DualMAStrategy(bt.Strategy): 双均线交叉策略 —— 小雅的第一个交易系统 参数 short_period: 短期均线周期默认5日 long_period: 长期均线周期默认20日 params ( (short_period, 5), (long_period, 20), ) def __init__(self): 初始化计算两条均线 # 计算短期和长期简单移动平均线 self.short_ma bt.indicators.SimpleMovingAverage( self.data.close, periodself.params.short_period ) self.long_ma bt.indicators.SimpleMovingAverage( self.data.close, periodself.params.long_period ) # 记录交叉信号短期均线 - 长期均线正负号切换即为交叉 self.crossover bt.indicators.CrossOver(self.short_ma, self.long_ma) def next(self): 每根K线到来时触发——策略的核心逻辑 # 获取当前持仓状态 if not self.position: # 空仓状态 if self.crossover 0: # 金叉短期均线上穿长期均线 # 用全部资金买入 self.buy() self.log(f金叉信号触发买入价格: {self.data.close[0]:.2f}) else: # 已持仓 if self.crossover 0: # 死叉短期均线下穿长期均线 # 卖出全部持仓 self.sell() self.log(f死叉信号触发卖出价格: {self.data.close[0]:.2f}) def log(self, txt): 日志输出包含当前日期 dt self.datas[0].datetime.date(0) print(f{dt.isoformat()} - {txt}) # 回测主流程 def run_backtest(stock_code600519, start_cash100000.0): 运行双均线策略回测 参数 stock_code: 股票代码 start_cash: 初始资金默认10万元 # 第一步获取数据 df ak.stock_zh_a_hist(symbolstock_code, perioddaily, start_date20230101, end_date20250720, adjustqfq) df[日期] pd.to_datetime(df[日期]) df.set_index(日期, inplaceTrue) # 第二步将DataFrame转换为Backtrader的DataFeed data bt.feeds.PandasData( datanamedf, datetimeNone, # index已经是日期 open开盘, high最高, low最低, close收盘, volume成交量, openinterest-1 # A股无持仓量概念 ) # 第三步创建回测引擎 cerebro bt.Cerebro() cerebro.adddata(data) # 加载数据 cerebro.addstrategy(DualMAStrategy) # 加载策略 cerebro.broker.setcash(start_cash) # 设置初始资金 cerebro.broker.setcommission(commission0.00025) # 佣金万2.5 # 第四步添加分析器绩效指标 cerebro.addanalyzer(bt.analyzers.Returns, _namereturns) # 收益率 cerebro.addanalyzer(bt.analyzers.DrawDown, _namedrawdown) # 回撤 cerebro.addanalyzer(bt.analyzers.SharpeRatio, _namesharpe, riskfreerate0.03) # 夏普比率无风险利率3% cerebro.addanalyzer(bt.analyzers.TradeAnalyzer, _nametrades) # 交易统计 # 第五步执行回测 print(f\n{*50}) print(f回测标的{stock_code}) print(f策略双均线交叉MA{strategy_params[short]}/MA{strategy_params[long]}) print(f初始资金{start_cash:,.0f}元) print(f{*50}\n) start_value cerebro.broker.getvalue() results cerebro.run() end_value cerebro.broker.getvalue() # 第六步输出绩效报告 strat results[0] print(f\n{*50}) print(回测绩效报告) print(f{*50}) print(f初始资金: {start_value:,.2f} 元) print(f最终资金: {end_value:,.2f} 元) print(f总收益率: {(end_value/start_value - 1)*100:.2f}%) # 分析器结果 returns strat.analyzers.returns.get_analysis() drawdown strat.analyzers.drawdown.get_analysis() sharpe strat.analyzers.sharpe.get_analysis() trades strat.analyzers.trades.get_analysis() print(f年化收益率: {returns.get(rnorm100, 0):.2f}%) print(f最大回撤: {drawdown.get(max, {}).get(drawdown, 0):.2f}%) print(f夏普比率: {sharpe.get(sharperatio, 0):.2f}) total_trades trades.get(total, {}).get(total, 0) won_trades trades.get(won, {}).get(total, 0) print(f总交易次数: {total_trades}) print(f胜率: {(won_trades/total_trades*100) if total_trades 0 else 0:.2f}%) return cerebro # 执行回测 strategy_params {short: 5, long: 20} cerebro run_backtest(600519, start_cash100000.0) # 绘制回测结果图表 # cerebro.plot(stylecandlestick, volumeFalse) print(\n提示取消上面的一行注释可以显示K线图。)关键要点Crossover指标自动检测正负号切换——从负变正就是金叉返回值1从正变负就是死叉返回值-1Backtrader的next()方法是策略的核心——每根K线调用一次你在这里定义买卖逻辑佣金设置不能遗漏——万2.5的佣金看似不高但高频策略下20次交易就吃掉0.5%的利润参数5和20不是拍脑袋定的——应该用参数扫描Grid Search在不同参数组合上回测选样本外表现最好的3 解读绩效报告——什么才算好策略概念讲解回测跑完了屏幕上出现一串数字。但哪些数字是关键我们按重要性排序指标含义优秀标准警戒线夏普比率每承担一单位风险获得的超额回报1.0年化0.5还不如存银行最大回撤从最高点到最低点的最大亏损幅度20%35%大多数人在此之前会放弃年化收益率折算为每年平均的收益百分比10%5%跑不赢理财胜率盈利交易次数占总次数的比例40-60%正常范围30%策略逻辑可能有问题盈亏比平均单笔盈利 / 平均单笔亏损1.51.0一次亏损抵消多次盈利这里特别解释一下夏普比率Sharpe Ratio它把收益和风险放在同一个公式里比较。夏普比率 (策略年化收益率 - 无风险利率) / 年化波动率如果策略年化收益15%无风险利率3%约等于余额宝收益年化波动率12%夏普比率 (15%-3%)/12% 1.0。这意味着每承担1%的波动风险策略能带来1%的超额回报。一般认为夏普1是不错2是优秀3可能不可持续。代码演示下面写一个独立的绩效分析函数可以从任意回测结果中提取并格式化展示这些指标def analyze_performance(strat_result): 解析Backtrader回测结果输出格式化绩效报告 参数 strat_result: cerebro.run()返回的Strategy实例取第一个元素 # 提取各项分析器结果 returns strat_result.analyzers.returns.get_analysis() drawdown strat_result.analyzers.drawdown.get_analysis() sharpe strat_result.analyzers.sharpe.get_analysis() trades strat_result.analyzers.trades.get_analysis() # 构建报告 report { 年化收益率(%): round(returns.get(rnorm100, 0), 2), 最大回撤(%): round(drawdown.get(max, {}).get(drawdown, 0), 2), 夏普比率: round(sharpe.get(sharperatio, 0), 2), 总交易次数: trades.get(total, {}).get(total, 0), 盈利次数: trades.get(won, {}).get(total, 0), 亏损次数: trades.get(lost, {}).get(total, 0), } # 计算胜率 total report[总交易次数] won report[盈利次数] report[胜率(%)] round(won / total * 100, 2) if total 0 else 0 # 计算盈亏比 avg_win trades.get(won, {}).get(pnl, {}).get(average, 0) avg_loss abs(trades.get(lost, {}).get(pnl, {}).get(average, 0)) report[盈亏比] round(avg_win / avg_loss, 2) if avg_loss 0 else 0 # 格式化输出 print(\n * 50) print(策略绩效评估报告) print( * 50) for key, value in report.items(): print(f {key}: {value}) # 综合评级 if report[夏普比率] 1.5 and report[最大回撤(%)] 20: rating 优秀 - 收益风险比和回撤控制俱佳 elif report[夏普比率] 1.0: rating 良好 - 具备实盘潜力建议模拟盘验证 elif report[夏普比率] 0.5: rating 一般 - 收益风险比偏低可尝试优化参数 else: rating 较差 - 策略需要重新设计思路 print(f\n 综合评级: {rating}) return report # 快速对比同一策略在不同标的上的表现 print(\n提示可在不同股票上运行 run_backtest() 并调用 analyze_performance() 进行横向对比。) print(例如对比茅台(600519)、五粮液(000858)、招商银行(600036) 上的双均线策略表现差异。)关键要点夏普比率是最诚实的指标——它同时惩罚低收益和高波动不像单纯看收益率那么容易被美颜最大回撤决定了你能否坚持执行策略——回撤40%意味着100万变60万很少有人能不为所动胜率低的策略不一定差——趋势跟踪策略通常胜率只有35-40%但盈亏比3:1以上总体仍然盈利策略对比必须在同一时间段上进行——牛市中的任何策略都好看熊市才是真考验4 策略对比实验——为什么不同策略在不同市场表现各异概念讲解现在小雅已经掌握了回测技能接下来做一个重要的思维实验将双均线、买入持有、单均线三种策略放在同一时间段对比。这个对比实验想说明一个道理没有一种策略在任何市场环境下都表现最好。2023年茅台的走势可以分为三段1-3月单边上涨趋势策略和买入持有表现好4-7月横盘震荡双均线频繁假突破反复亏损8-12月单边下跌买入持有大幅亏损而趋势策略至少能在死叉时离场这就是第01讲说的策略与市场状态的匹配——理解这一点是后面学习多策略组合的理论基础。代码演示在同一张图表上对比三种策略的净值曲线import matplotlib.pyplot as plt import matplotlib matplotlib.rcParams[font.sans-serif] [SimHei] matplotlib.rcParams[axes.unicode_minus] False class BuyAndHold(bt.Strategy): 买入持有策略——第一天全仓买入最后一天卖出 def next(self): if len(self) 1: # 第一根K线 self.buy() class SingleMA(bt.Strategy): 单均线策略——价格上穿均线买入下穿卖出 params ((period, 20),) def __init__(self): self.ma bt.indicators.SimpleMovingAverage( self.data.close, periodself.params.period) self.crossover bt.indicators.CrossOver(self.data.close, self.ma) def next(self): if not self.position and self.crossover 0: self.buy() elif self.position and self.crossover 0: self.sell() def compare_strategies(stock_code, strategies_dict, start_cash100000.0): 对比多个策略在同一标的上的表现 参数 stock_code: 股票代码 strategies_dict: {策略名称: StrategyClass} 字典 start_cash: 初始资金 # 获取数据 df ak.stock_zh_a_hist(symbolstock_code, perioddaily, start_date20230101, end_date20250720, adjustqfq) results {} for name, strategy_cls in strategies_dict.items(): cerebro bt.Cerebro() df_copy df.copy() df_copy[日期] pd.to_datetime(df_copy[日期]) df_copy.set_index(日期, inplaceTrue) data bt.feeds.PandasData(datanamedf_copy, datetimeNone, open开盘, high最高, low最低, close收盘, volume成交量, openinterest-1) cerebro.adddata(data) cerebro.addstrategy(strategy_cls) cerebro.broker.setcash(start_cash) cerebro.broker.setcommission(commission0.00025) start_val cerebro.broker.getvalue() strat_results cerebro.run() end_val cerebro.broker.getvalue() total_return (end_val / start_val - 1) * 100 results[name] { 总收益率(%): round(total_return, 2), 最终资金: round(end_val, 2) } # 输出对比表格 print(f\n{*60}) print(f策略对比实验 - {stock_code}) print(f{*60}) print(f{策略名称:15} {总收益率(%):12} {最终资金(元):15}) print(- * 45) for name, result in results.items(): print(f{name:15} {result[总收益率(%)]:12.2f} {result[最终资金]:15,.2f}) return results # 三策略对比 strategies { 双均线(5/20): DualMAStrategy, 买入持有: BuyAndHold, 单均线(20): SingleMA, } compare_strategies(600519, strategies)关键要点策略之间的对比只能说明在这个历史时间段上谁表现好不能预测未来买入持有策略的回撤最大——因为没有止损机制跌多久就扛多久双均线策略在震荡市中表现最差——频繁的假交叉会反复割肉这是趋势策略的先天短板多策略组合趋势 震荡 套利能平滑收益曲线——这是第二阶段小雅进阶要学的核心内容实战总结本节小雅分析师完成了她的首次亮相核心成果如下回测引擎搭建Backtrader回测环境理解事件驱动的核心思想双均线策略实现完整的买卖逻辑体验数据→信号→绩效的闭环绩效解读掌握夏普比率、最大回撤、胜率、盈亏比四个核心指标策略对比用对比实验直观感受不同策略在不同市场状态下的表现差异常见误区提醒误区一把回测收益率直接当成预期收益。回测有各种隐性偏差幸存者偏差、未来函数实盘收益通常会打折扣误区二只优化收益率不理睬回撤。最大回撤50%的策略意味着你账户从100万跌到50万这个过程可能持续半年——你能拿住吗误区三回测一次就下结论。应该做鲁棒性检验换时间段、换股票、换参数组合分别回测看在多大范围内策略有效下一讲小雅将装备Talib技术指标库从手动写均线升级为调取数十种经典指标并学会识别K线形态、用成交量确认信号——让策略分析能力上一个台阶。下一篇预告第06讲小雅再升级——Talib指标库与K线形态量化均线只是冰山一角。TA-Lib提供了150种技术指标——MACD、RSI、布林带、ATR……小雅将把这些指标从看图经验变成可回测的策略组件。每周更新。关注专栏不错过后续内容。