七日量化回测入门(四)Backtrader 双均线回测告别未来函数
1. 引言在量化回测中未来函数Look-ahead Bias是导致回测结果虚高、实盘却亏损的头号杀手。它的本质是在计算当天交易信号时无意中使用了当天收盘后甚至未来才产生的数据。正确做法是所有交易信号必须基于 T-1 及之前的数据。在 Backtrader 中通过负索引访问历史数据例如self.data.close[-1]表示昨日收盘价。永远不要用[0]来决定当天的交易信号因为[0]代表当前正在形成的 K 线其收盘价在当天收盘前是未知的。本文将以工业**sh.601138为例演示如何用 Baostock 拉取数据、用 Backtrader 构建双均线策略并重点修复代码中的未来函数隐患。2. 数据获取与清洗首先使用 Baostock 拉取日线数据并完成类型转换与索引设置。importbaostockasbsimportpandasaspdimportbacktraderasbt# 登录print(1.准备登录,flushTrue)lgbs.login()print(f2.登录返回:{lg.error_code}{lg.error_msg},flushTrue)# 拉取数据print(3.开始查询,flushTrue)rsbs.query_history_k_data_plus(sh.601138,date,code,open,high,low,close,volume,amount,start_date2026-01-01,end_date2026-09-30,frequencyd,adjustflag2)print(4.查询返回,flushTrue)# 转 DataFramedata_list[]while(rs.error_code0)rs.next():data_list.append(rs.get_row_data())print(f5.拿到{len(data_list)},flushTrue)dfpd.DataFrame(data_list,columnsrs.fields)# -------------------------- 关键修复1全字段类型转换 --------------------------# 把所有空字符串替换成NaN避免字符串残留dfdf.replace(,pd.NA)# 把所有行情数值字段一次性转为float类型覆盖所有Backtrader需要用到的列numeric_cols[open,high,low,close,volume,amount]forcolinnumeric_cols:df[col]pd.to_numeric(df[col],errorscoerce)# 日期字段转为datetime类型df[date]pd.to_datetime(df[date],errorscoerce)# -------------------------- 关键修复2设置日期为索引 --------------------------# Backtrader的PandasData默认优先用索引作为datetime列这是最不容易出错的配置dfdf.sort_values(date).reset_index(dropTrue)df.set_index(date,inplaceTrue)# 清洗掉所有带缺失值的行避免后续计算异常df.dropna(subsetnumeric_cols,inplaceTrue)print(df.tail())3. 策略定义信号只用 T-1 数据这是本文的核心。在next()方法中所有决策都基于self.data.close[-1]昨日收盘价和均线指标绝不使用self.data.close[0]当前未收盘的K线。classDualMAStrategy(bt.Strategy):params((fast_period,3),# 短期均线(slow_period,20),# 长期均线)def__init__(self):self.fast_mabt.indicators.SimpleMovingAverage(self.data.close-1,periodself.params.fast_period)self.slow_mabt.indicators.SimpleMovingAverage(self.data.close-1,periodself.params.slow_period)self.crossoverbt.indicators.CrossOver(self.fast_ma,self.slow_ma)#初始化订单引用这里有个Backtrader常见坑任何打算在next()里读写的自定义属性都必须在___init___里先赋初值。self.orderNonedefstart(self):# 这里的打印只会在第一次回测启动时执行一次print(-*25)print(f短线均线{self.params.fast_period})print(f长线均线{self.params.slow_period})print(-*25)defnext(self):#有未完成订单则跳过避免重复下单ifself.order:returnifnotself.position:# 空仓ifself.crossover0:# 金叉买入#用open[0]次日开盘假估算仓位贴近实盘sizeint(self.broker.getcash()*0.95/self.data.close[-1])ifsize0:self.orderself.buy(sizesize)else:# 持仓ifself.crossover0:# 死叉卖出self.orderself.close()为什么不能用[0]self.data.close[0]是当前正在形成的K线的收盘价。在实盘中这根K线还没走完收盘价是未知的。如果用它计算买入数量或触发信号回测时相当于「偷看」了当天收盘数据实盘时根本无法复现。self.data.close[-1]是昨日收盘价是实盘中当下就能拿到的确定数据用它计算信号才真实可靠。建议尝试昨日收盘价和当日开盘价self.data.open[0],收益率能差出一些。4. 回测引擎配置与运行将清洗后的数据接入 Backtrader配置好手续费、滑点与绩效分析器。# 4.1 包装数据为 Backtrader 数据源data_feedbt.feeds.PandasData(datanamedf,openopen,highhigh,lowlow,closeclose,volumevolume,openinterestNone)# 4.2 配置回测引擎cerebrobt.Cerebro()cerebro.broker.setcash(100000)# 初始资金 10 万cerebro.broker.setcommission(commission0.0003)# 手续费万分之三cerebro.broker.set_slippage_perc(0.001)# 滑点千分之一cerebro.adddata(data_feed)cerebro.addstrategy(DualMAStrategy)# 添加分析器cerebro.addanalyzer(bt.analyzers.SharpeRatio,_namesharpe)cerebro.addanalyzer(bt.analyzers.DrawDown,_namedrawdown)cerebro.addanalyzer(bt.analyzers.Returns,_namereturns)# 4.3 运行回测print(\n 双均线回测开始 )print(f初始资金:{cerebro.broker.getvalue():.2f})resultscerebro.run()print(f最终资金:{cerebro.broker.getvalue():.2f})# 4.4 输出绩效指标stratresults[0]print(\n---------- 绩效指标 ----------)# 1. 夏普比率sharpe_resstrat.analyzers.sharpe.get_analysis()sharpe_valsharpe_res.get(sharperatio)ifsharpe_valisnotNone:print(f夏普比率:{sharpe_val:.2f})else:print(夏普比率: 无有效数据)# 2. 最大回撤drawdown_resstrat.analyzers.drawdown.get_analysis()print(f最大回撤:{drawdown_res.max.drawdown:.2f}%)# 3. 年化收益率returns_resstrat.analyzers.returns.get_analysis()print(f年化收益率:{returns_res.rnorm100:.2f}%)5.完整代码importbaostockasbsimportpandasaspdimportmatplotlib.pyplotaspltimportbacktraderasbt# 1. 登录 print(1.准备登录,flushTrue)lgbs.login()print(f2.登录返回:{lg.error_code}{lg.error_msg},flushTrue)# 2. 拉取数据 print(3.开始查询,flushTrue)rsbs.query_history_k_data_plus(sh.601138,date,code,open,high,low,close,volume,amount,start_date2026-01-01,end_date2026-09-30,frequencyd,adjustflag2)print(4.查询返回,flushTrue)# 3. 转 DataFrame data_list[]while(rs.error_code0)rs.next():data_list.append(rs.get_row_data())print(f5.拿到{len(data_list)},flushTrue)dfpd.DataFrame(data_list,columnsrs.fields)# 4. 清洗 # -------------------------- 关键修复1全字段类型转换 --------------------------# 把所有空字符串替换成NaN避免字符串残留dfdf.replace(,pd.NA)# 把所有行情数值字段一次性转为float类型覆盖所有Backtrader需要用到的列numeric_cols[open,high,low,close,volume,amount]forcolinnumeric_cols:df[col]pd.to_numeric(df[col],errorscoerce)# 日期字段转为datetime类型df[date]pd.to_datetime(df[date],errorscoerce)# -------------------------- 关键修复2设置日期为索引 --------------------------# Backtrader的PandasData默认优先用索引作为datetime列这是最不容易出错的配置dfdf.sort_values(date).reset_index(dropTrue)df.set_index(date,inplaceTrue)# 清洗掉所有带缺失值的行避免后续计算异常df.dropna(subsetnumeric_cols,inplaceTrue)print(df.tail())# 5. 双均线回测 # 5.1 定义策略classDualMAStrategy(bt.Strategy):params((fast_period,5),# 短期均线(slow_period,10),# 长期均线)def__init__(self):#用close(-1)昨日收盘价计算指票避免未来函数self.fast_mabt.indicators.SimpleMovingAverage(self.data.close(-1),periodself.params.fast_period)self.slow_mabt.indicators.SimpleMovingAverage(self.data.close(-1),periodself.params.slow_period)self.crossoverbt.indicators.CrossOver(self.fast_ma,self.slow_ma)#初始化订单引用self.orderNonedefstart(self):# 这里的打印只会在第一次回测启动时执行一次print(-*25)print(f短线均线{self.params.fast_period})print(f长线均线{self.params.slow_period})print(-*25)defnext(self):#有未完成订单则跳过避免重复下单ifself.order:returnifnotself.position:# 空仓ifself.crossover0:# 金叉买入#用open[0]次日开盘假估算仓位贴近实盘sizeint(self.broker.getcash()*0.95/self.data.close[-1])ifsize0:self.orderself.buy(sizesize)else:# 持仓ifself.crossover0:# 死叉卖出self.orderself.close()defnotify_order(self,order):#订单状态回调成交或失败后清空order引用iforder.statusin[order.Completed,order.Canceled,order.Margin,order.Rejected]:self.orderNone# 5.2 包装数据为 Backtrader 数据源data_feedbt.feeds.PandasData(datanamedf,openopen,highhigh,lowlow,closeclose,volumevolume,openinterestNone)# 5.3 配置回测引擎cerebrobt.Cerebro()cerebro.broker.setcash(100000)# 初始资金 10 万cerebro.broker.setcommission(commission0.0003)# 手续费万分之三cerebro.broker.set_slippage_perc(0.001)# 滑点千分之一cerebro.adddata(data_feed)cerebro.addstrategy(DualMAStrategy)# 添加分析器cerebro.addanalyzer(bt.analyzers.SharpeRatio,_namesharpe)cerebro.addanalyzer(bt.analyzers.DrawDown,_namedrawdown)cerebro.addanalyzer(bt.analyzers.Returns,_namereturns)# 5.4 运行回测print(\n 双均线回测开始 )print(f初始资金:{cerebro.broker.getvalue():.2f})resultscerebro.run()print(f最终资金:{cerebro.broker.getvalue():.2f})# 5.5 输出绩效指标stratresults[0]print(\n---------- 绩效指标 ----------)# 1. 夏普比率sharpe_resstrat.analyzers.sharpe.get_analysis()sharpe_valsharpe_res.get(sharperatio)ifsharpe_valisnotNone:print(f夏普比率:{sharpe_val:.2f})else:print(夏普比率: 无有效数据)# 2. 最大回撤dd_resstrat.analyzers.drawdown.get_analysis()max_dddd_res.get(max,{}).get(drawdown)ifmax_ddisnotNone:print(f最大回撤:{max_dd:.2f}%)else:print(最大回撤: 无有效数据)# 3. 收益率ret_resstrat.analyzers.returns.get_analysis()rtotret_res.get(rtot)rnormret_res.get(rnorm)ifrtotisnotNone:print(f总收益率:{rtot:.2%})# 使用 :.2% 自动转换为百分比格式ifrnormisnotNone:print(f年化收益率:{rnorm:.2%})# 登出bs.logout()6. 总结常见错误正确做法用self.data.close[0]触发信号用self.data.close[-1]或均线指标判断用self.data.close[0]计算下单数量用self.data.close[-1]计算下单数量无self.order管理可能重复下单加self.ordernotify_order回调忽略数据清洗字符串残留全字段pd.to_numericdropna未设置日期索引df.set_index(date, inplaceTrue)核心心法回测中每一个信号都必须是在实盘当下「已经发生」的数据。[0]是未来[-1]才是过去。永远站在 T-1 的视角做决策你的回测结果才值得信赖。

相关新闻

JSP在线家政网课设全解析:从架构选型到订单权限实战

JSP在线家政网课设全解析:从架构选型到订单权限实战

简介:基于JSP的在线家政网设计与实现文档是一份完整的课程设计与毕业设计参考方案,重点面向Web开发方向的在校学生,以及需要快速搭建家政服务平台的开发者。系统主要覆盖家政预约、家教预约、在线求职申请以及个人后台查询申请状态等核心功能…

2026/10/4 9:01:05 阅读更多 →
SSM电影推荐系统毕设实战:协同过滤算法落地与部署避坑指南

SSM电影推荐系统毕设实战:协同过滤算法落地与部署避坑指南

简介:这份资源面向计算机相关专业毕业生与Java初学者,提供一套可直接用于毕业设计的电影推荐系统完整方案,解决从选题到答辩缺乏可运行项目与配套文档的痛点。压缩包共846个文件,约22.45MB,以128个Java源码、49个Vue组…

2026/10/5 12:25:50 阅读更多 →
JavaWeb学生选课系统源码实战:Servlet+JDBC角色分流与选课逻辑拆解

JavaWeb学生选课系统源码实战:Servlet+JDBC角色分流与选课逻辑拆解

简介:这份JavaWeb实训资料包面向高校计算机相关专业学生与Java初学者,围绕学生选课系统这一经典课程设计场景,提供从需求分析到代码落地的完整参考。系统按角色划分功能:学生端支持注册登录、浏览课程、选课退选与查看已选结果&am…

2026/10/4 9:01:05 阅读更多 →

最新新闻

BranchIP:自适应等变计算驱动的原子间势能建模新范式

BranchIP:自适应等变计算驱动的原子间势能建模新范式

1. 项目概述:为什么“自适应等变计算”正在重构原子间势能建模的底层逻辑BranchIP这个名字乍看像某个冷门开源库的代号,但拆开来看——Branch(分支)、IP(Interatomic Potential,原子间势能)——…

2026/10/5 13:59:21 阅读更多 →
自己动手,三分之一预算:开源驾驶舱openrig铝型材DIY全攻略

自己动手,三分之一预算:开源驾驶舱openrig铝型材DIY全攻略

你有没有算过一笔账:一套像样的模拟赛车驾驶舱,成品买下来普遍要8000到20000元,好一点的直接奔着5万去了。但如果自己动手,用开源图纸和铝型材拼一台,成本可以压到三分之一以下,刚性还不输成品。这就是open…

2026/10/5 13:59:21 阅读更多 →
OpenRig开源开放式机架:从设计到组装,打造高效散热DIY工作站

OpenRig开源开放式机架:从设计到组装,打造高效散热DIY工作站

OpenRig 这名字拆开看就是 Open Rig,开放式机架。我做这个项目从画第一张草图到实机点亮,前后折腾了两个多月,期间推翻了三次结构方案,废掉一版亚克力切割件,最后才定下来现在这套既兼顾散热、又方便维护、还能随意扩…

2026/10/5 13:59:21 阅读更多 →
CARM:LLM强化学习中取消响应的精准掩码方案

CARM:LLM强化学习中取消响应的精准掩码方案

1. 项目概述:为什么在LLM强化学习中,“取消响应”会成为训练灾难的隐形推手?最近在做几个数学推理和代码生成类任务的RLHF微调时,我反复遇到一个特别诡异的现象:模型明明在监督微调(SFT)阶段表现…

2026/10/5 13:59:21 阅读更多 →
OpenShell实战:让大模型通过自然语言驱动本地Shell执行代码

OpenShell实战:让大模型通过自然语言驱动本地Shell执行代码

1. 项目概述1.1 它到底是什么OpenShell这个名字,乍一听像是个终端模拟器,或者某个开源Shell的变体。但实际上,它做的事比“Shell”这两个字所暗示的要大得多——它是一套把自然语言转换成可执行代码,并在本地环境中直接运行的开源…

2026/10/5 13:59:21 阅读更多 →
落地页文案的10个转化技巧:用ai-design-skills写好标题公式与CTA

落地页文案的10个转化技巧:用ai-design-skills写好标题公式与CTA

落地页文案的10个转化技巧:用ai-design-skills写好标题公式与CTA 【免费下载链接】ai-design-skills 项目地址: https://gitcode.com/gh_mirrors/ai/ai-design-skills ai-design-skills 是一套面向 Claude Code、Cursor 等 AI 编程工具的落地页设计技能库&a…

2026/10/5 13:58:20 阅读更多 →

日新闻

马斯克杀回智能体战场,Grok 4.5万亿参数撑腰,Cursor接手数字白领项目:用TaoToken统一Key跑通多模型Agent工作流

马斯克杀回智能体战场,Grok 4.5万亿参数撑腰,Cursor接手数字白领项目:用TaoToken统一Key跑通多模型Agent工作流

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/5 0:00:22 阅读更多 →
AI编程工具插件机制详解:plugin.json配置与加载失败排查指南

AI编程工具插件机制详解:plugin.json配置与加载失败排查指南

1. 从“plugins”这个词说起:它到底在解决什么问题如果你最近在折腾 AI 编程工具,尤其是 Cursor、Codex CLI、Claude Code 这类带 CLI 的编辑器或命令行助手,那你大概率绕不开一个词——plugins。这个词本身不新鲜,从浏览器到 IDE…

2026/10/5 0:00:23 阅读更多 →
第26课:OpenClaw|日志审计与问题诊断:把日志链路改到 TaoToken 的排查清单

第26课:OpenClaw|日志审计与问题诊断:把日志链路改到 TaoToken 的排查清单

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/5 0:00:23 阅读更多 →

周新闻

KT148A语音芯片外挂8002D功放的工程实践指南

KT148A语音芯片外挂8002D功放的工程实践指南

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/5 5:06:42 阅读更多 →
LLC谐振变换器增益公式推导:从FHA等效到完整归一化表达式

LLC谐振变换器增益公式推导:从FHA等效到完整归一化表达式

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/5 1:10:22 阅读更多 →
ARM架构深度解析:从RISC设计理念到交叉编译实战

ARM架构深度解析:从RISC设计理念到交叉编译实战

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/5 3:06:17 阅读更多 →

月新闻

我发现了一个新思路:用 Remotion + Claude Code 像写代码一样自动化生成短视频

我发现了一个新思路:用 Remotion + Claude Code 像写代码一样自动化生成短视频

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/4 11:40:45 阅读更多 →
Windows下 Codex 中 Chrome 和 Computer Use 插件不可用问题排查及解决参考方式:TaoToken 统一 Key 配置与验证

Windows下 Codex 中 Chrome 和 Computer Use 插件不可用问题排查及解决参考方式:TaoToken 统一 Key 配置与验证

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/4 9:43:54 阅读更多 →
黑夜航拍船只数据集训练YOLOV5模型全流程解析

黑夜航拍船只数据集训练YOLOV5模型全流程解析

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/4 20:14:29 阅读更多 →