ARTICLE DETAIL

建站实战干货

来自一线的建站与推广经验沉淀,每一条都经过真实交付验证。

Python从零搭建股票回测系统:数据清洗、backtrader多股回测与绩效验证

2026/9/24 4:29:49 拓冰建站 浏览量
Python从零搭建股票回测系统:数据清洗、backtrader多股回测与绩效验证 1. 为什么我要自己搭一套回测系统1.1 从一次“看起来很美”的实盘说起几年前我拿着一个在历史数据上跑出来年化收益相当漂亮的均线策略兴冲冲地投了真金白银进去结果三个月不到就回撤到怀疑人生。回头复盘才发现问题根本不在策略逻辑本身而在于我当初用的那套回测环境——数据只用了单只股票、复权处理没做、手续费和滑点几乎忽略不计、还犯了未来函数的低级错误。那次教训让我明白一件事回测系统不是跑个收益率曲线就完事了它是一条从数据采集、清洗、对齐、撮合、绩效统计到可视化输出的完整链路任何一环偷懒结果都会骗你。市面上现成的量化平台确实不少点几下就能出报告但用久了你会发现几个绕不开的痛点数据源不透明、撮合规则黑盒、想加个自定义指标或者换个仓位管理逻辑就处处受限更别提把系统部署到自己机器上做批量实验了。所以后来我干脆花了两周时间用 Python 从零搭了一套属于自己的股票策略回测系统核心框架选的是 backtrader数据层自己写绩效分析自己补。这套东西到现在还在用跑过单股、跑过多股组合、也接过实盘信号。这篇文章就把这条完整链路拆开讲清楚数据从哪来、怎么存、怎么喂给回测引擎、backtrader 的策略怎么写、多股回测怎么处理、结果怎么验证。适合已经会一点 Python、想认真做策略研究的朋友也适合被现成平台坑过、想拿回控制权的人。1.2 一套回测系统到底包含哪些模块很多人一上来就问“backtrader 怎么用”其实框架只是中间那一环。我习惯把整套系统拆成五层从下往上分别是数据层负责行情数据的获取、清洗、复权、存储和读取。这是地基地基不稳后面全塌。引擎层也就是 backtrader 这类回测框架负责按时间推进、撮合订单、计算持仓和资金曲线。策略层你自己的买卖逻辑包括信号生成、仓位管理、止损止盈。分析层绩效指标计算比如年化收益、最大回撤、夏普比率、胜率、盈亏比。展示层图表输出、交易明细、参数对比方便你判断策略到底行不行。这五层里数据层和分析层是最容易被忽视、却最影响结论可信度的部分。框架层反而相对标准化backtrader 的文档和社区案例足够多照着写就行。所以下面我会把重心放在数据链路和结果验证上框架部分讲清楚关键配置和踩坑点。提示不要一上来就追求“全自动”“多因子”“机器学习选股”先把单标的、日频、含手续费的回测跑通跑对再往上叠复杂度。我见过太多人系统还没搭稳就开始堆策略最后连自己错在哪都找不到。2. 数据层整条链路里最脏最累的活2.1 数据从哪来怎么选数据源大致分三类免费公开接口、付费数据服务、券商或交易所授权数据。做个人研究起步阶段用免费接口完全够用比如常见的财经数据接口能拿到日线级别的开高低收、成交量、成交额。但免费数据有几个通病你得心里有数复权问题除权除息当天价格会跳空如果不做前复权或后复权处理均线、收益率全是错的。我一般用前复权因为回测时看的是“如果当时买入现在值多少”前复权更贴近真实持有体验。停牌和缺失个股停牌期间没有行情直接按日期对齐会出现空洞。处理方式要么前向填充要么在回测时跳过该标的的该时段。幸存者偏差如果你只拿现在还在上市的股票做回测那些退市的、被并购的标的就消失了结果会系统性偏乐观。严谨的做法是使用包含退市股票的历史成分股数据个人研究至少要有这个意识。我自己的做法是先用免费接口把全市场日线拉下来存成本地文件再做统一的复权和清洗。这样每次回测读本地数据速度快、可复现也不受接口限流影响。2.2 数据清洗与复权的具体操作复权计算本身不复杂核心是拿到除权除息日的复权因子。前复权价格 原始价格 × 复权因子后复权则相反。很多接口直接提供复权后的价格但我建议你至少手动验证一次因为不同平台的复权算法细节可能不同。清洗环节我固定做这几件事去重同一交易日同一标的只保留一条记录。排序按日期升序排列backtrader 对数据顺序敏感。缺失值处理成交量缺失填 0价格缺失用前值填充但打标记回测时可选择跳过。异常值过滤单日涨跌幅超过合理范围的记录要核查可能是数据错误。字段统一把列名统一成 datetime、open、high、low、close、volume、openinterest方便直接喂给 backtrader。下面是我常用的清洗函数骨架你可以直接改成自己的版本import pandas as pd import numpy as np def clean_stock_data(df): df df.copy() df[datetime] pd.to_datetime(df[datetime]) df df.drop_duplicates(subset[datetime]) df df.sort_values(datetime).reset_index(dropTrue) df[volume] df[volume].fillna(0) price_cols [open, high, low, close] df[price_cols] df[price_cols].ffill() df[openinterest] 0 df df[[datetime, open, high, low, close, volume, openinterest]] return df这段代码看着简单但每一步都有讲究。比如openinterest字段股票回测用不上但 backtrader 的通用数据格式需要它填 0 即可。再比如ffill前向填充只在价格缺失时用而且要在复权之后做顺序错了结果就偏了。2.3 数据存储格式的选择存数据我试过三种方案CSV、SQLite、HDF5。各有优劣看你数据量和使用习惯。存储方案优点缺点适用场景CSV通用、可读、易调试大文件读取慢、无索引少量标的、快速验证SQLite单文件、支持查询、无需服务并发差、大数据略慢中等规模、需要按条件筛选HDF5读写快、压缩好、适合数值需要额外库、可读性差全市场多年数据、批量回测我现在的方案是原始数据存 SQLite 方便查询和增量更新回测时按需导出成内存 DataFrame 直接喂给 backtrader不落中间文件。这样既保留了数据管理的灵活性又保证了回测速度。注意不管用哪种格式一定要给数据加时间戳和版本标记。我吃过亏——某次更新数据后没记录版本回测结果和之前对不上排查了半天才发现是数据源调整了复权算法。3. backtrader 引擎层核心配置与多股回测3.1 为什么选 backtraderPython 生态里回测框架不少backtrader 的优势在于事件驱动、文档相对完整、支持多数据源和多时间框架、社区案例多、扩展性强。它的核心抽象是 Cerebro引擎、Strategy策略、Data Feed数据、Broker经纪商模拟、Analyzer分析器概念清晰组合灵活。当然它也有缺点源码更新不算活跃、部分 API 设计偏老派、多股回测时内存占用较高。但对个人研究来说这些都能接受。如果你追求极致性能可以考虑向量化回测库但事件驱动框架在模拟真实撮合上更贴近实盘我倾向于用它做最终验证。3.2 最小可运行回测的搭建步骤先装依赖pip install backtrader pandas numpy matplotlib然后写一个最小回测用双均线策略跑单只股票import backtrader as bt import pandas as pd class DualMA(bt.Strategy): params ((fast, 5), (slow, 20),) def __init__(self): self.fast_ma bt.indicators.SMA(self.data.close, periodself.p.fast) self.slow_ma bt.indicators.SMA(self.data.close, periodself.p.slow) self.crossover bt.indicators.CrossOver(self.fast_ma, self.slow_ma) def next(self): if not self.position: if self.crossover 0: self.buy() elif self.crossover 0: self.close() cerebro bt.Cerebro() data bt.feeds.PandasData(datanamedf) cerebro.adddata(data) cerebro.addstrategy(DualMA) cerebro.broker.setcash(100000) cerebro.broker.setcommission(commission0.0003) cerebro.addanalyzer(bt.analyzers.SharpeRatio, _namesharpe) cerebro.addanalyzer(bt.analyzers.DrawDown, _namedd) results cerebro.run() print(最终资金:, cerebro.broker.getvalue())这段代码能跑但离“可用”还差得远。几个关键点必须补上手续费setcommission设的是比例A 股还有印花税和过户费实际要分开算。滑点真实成交价和你看到的收盘价有差距backtrader 支持设置滑点百分比。仓位管理默认buy()是全仓实际要按资金比例或固定股数下单。数据对齐多股回测时不同标的的交易日历要对齐。3.3 多股回测的正确姿势多股回测是 backtrader 里最容易踩坑的地方。核心问题是不同股票的上市时间、停牌时间不同数据长度不一致引擎按主数据的时间轴推进时其他数据可能还没开始或已经结束。我的处理原则是统一交易日历以回测区间内的全市场交易日为准缺失的标的用 NaN 或跳过。逐标的判断可交易性在next()里检查当前数据是否有有效价格没有就跳过。资金分配多股同时出信号时按预设权重或等权分配资金避免超买。持仓跟踪用字典记录每只标的的持仓状态不要依赖单一self.position。下面是一个多股回测的骨架class MultiStockStrategy(bt.Strategy): params ((max_positions, 5),) def __init__(self): self.indicators {} for d in self.datas: self.indicators[d._name] bt.indicators.SMA(d.close, period20) def next(self): held [d for d in self.datas if self.getposition(d).size 0] for d in self.datas: if len(d) 20: continue price d.close[0] if price ! price: # NaN 检查 continue ma self.indicators[d._name][0] if d.close[0] ma and self.getposition(d).size 0: if len(held) self.p.max_positions: cash_per self.broker.getcash() / (self.p.max_positions - len(held)) size int(cash_per / price / 100) * 100 if size 0: self.buy(datad, sizesize) held.append(d) elif d.close[0] ma and self.getposition(d).size 0: self.close(datad)这里有几个细节值得说len(d) 20是为了跳过数据不足的早期阶段price ! price是判断 NaN 的常用技巧int(cash_per / price / 100) * 100是按 A 股一手 100 股取整。这些在单股回测里不会遇到多股一上来就全冒出来了。提示backtrader 多股回测时self.datas的顺序和你adddata的顺序一致策略里用d._name做键来管理指标和持仓比用索引更可靠。4. 策略层从信号到订单的完整逻辑4.1 信号生成与未来函数的防范策略的核心是信号但信号最容易犯的错就是未来函数——用了当前 bar 还没结束时的信息。比如你在next()里用self.data.close[0]判断然后以同一个收盘价成交这在实盘中是不可能的因为收盘价要等收盘才知道。backtrader 的默认撮合是“下一根 bar 开盘价成交”这已经规避了大部分未来函数。但如果你手动指定了成交价或者用了cheat_on_open就要格外小心。我的习惯是信号用当前 bar 的收盘价计算订单在下一根 bar 开盘执行这样最贴近实盘。另外指标计算也要注意。backtrader 的指标是逐 bar 计算的self.fast_ma[0]是当前值self.fast_ma[-1]是上一根的值不要搞混。如果你自己写指标确保只用到当前及之前的数据。4.2 仓位管理与风险控制仓位管理决定了策略的收益曲线形状比信号本身还重要。我常用的几种方式固定比例每次用总资金的固定百分比买入简单但不够灵活。波动率倒数按标的波动率分配仓位波动大的少买波动小的多买。凯利公式理论上最优但需要准确的胜率和赔率估计实际中容易过度自信。最大持仓数限制多股回测时控制同时持有的标的数量分散风险。止损止盈我一般用 backtrader 的sell配合价格触发或者在next()里手动判断。手动判断更灵活但要注意不要在同一个 bar 里既买又卖容易逻辑混乱。def next(self): for d in self.datas: pos self.getposition(d) if pos.size 0: cost pos.price current d.close[0] if current cost * 0.92: # 止损 8% self.close(datad) elif current cost * 1.20: # 止盈 20% self.close(datad)这段逻辑简单直接但实盘里还要考虑跳空、跌停无法卖出等情况。回测时至少要把跌停无法成交的情况模拟进去否则收益会虚高。4.3 参数优化与过拟合的边界backtrader 支持参数优化cerebro.optstrategy可以遍历参数组合。但我对参数优化一直很谨慎因为优化本身就是过拟合的温床。我的做法是样本内优化样本外验证把数据分成两段前段调参后段验证。参数平原优先选参数时看的是“一片区域都不错”而不是“某一个点特别好”。限制参数数量参数越多过拟合风险越大一般不超过 3 个。经济逻辑检验参数背后的逻辑要说得通不能纯靠数据挖出来。我见过太多人把均线周期优化到 7 和 13回测收益翻倍实盘一塌糊涂。参数优化的结果只能作为参考不能当作策略有效的证据。5. 分析层绩效指标与结果验证5.1 必须看的几个核心指标backtrader 自带 Analyzer常用的有 SharpeRatio、DrawDown、Returns、TradeAnalyzer。但自带指标有些细节需要自己补比如年化收益的计算方式、无风险利率的设定。我一般自己写一个绩效汇总函数把关键指标一次性算出来指标含义我的经验阈值年化收益策略每年的平均收益跑赢基准即可不追求极高最大回撤从峰值到谷底的最大跌幅超过 30% 要警惕夏普比率单位风险的超额收益大于 1 算合格胜率盈利交易占比不一定要高配合盈亏比看盈亏比平均盈利/平均亏损大于 1.5 比较健康交易次数总交易笔数太少不具统计意义这些指标要结合起来看。高胜率低盈亏比的策略一次大亏就能吃掉很多次小赚低胜率高盈亏比的策略心理上很难坚持。没有绝对的好坏只有适不适合你的风险偏好。5.2 回测结果的常见陷阱回测跑出漂亮曲线时先别高兴检查这几件事幸存者偏差数据里是不是只有现在还活着的股票前视偏差有没有用到未来才知道的信息比如财报公布日之前就用了财报数据复权错误除权日附近的价格跳空有没有正确处理流动性假设小盘股按收盘价全仓成交实盘可能根本买不到这个量。手续费和滑点有没有算进去算得够不够狠过拟合参数是不是调出来的样本外表现如何我自己的习惯是任何策略在实盘前先用最保守的假设跑一遍手续费翻倍、滑点加大、成交价用次日开盘、剔除流动性差的标的。如果这样还能接受才考虑进一步验证。5.3 可视化与交易明细复盘backtrader 自带cerebro.plot()能画出价格、指标、买卖点和资金曲线。但默认图表信息量有限我一般会额外导出交易明细用 pandas 做二次分析trades results[0].analyzers.trade.get_analysis() trade_df pd.DataFrame([ {date: t.date, price: t.price, size: t.size, pnl: t.pnl} for t in trades ])有了交易明细你可以按月份、按标的、按持仓时长分组统计找出策略在什么市场环境下表现好、什么环境下失效。这一步比看总收益有用得多也是我每次迭代策略的起点。6. 常见问题与排查技巧实录6.1 数据相关的高频问题问题一回测报错“数据长度不一致”多股回测时最常见。原因是不同标的的交易日数量不同。解决办法是统一交易日历或者在策略里用len(d)判断数据是否足够。问题二收益率明显偏高先查复权。如果用了未复权数据除权日的价格跳空会被当成真实涨跌收益率会虚高。再查手续费和滑点很多人忘了设。问题三某些标的完全没有交易检查数据里是否有 NaNbacktrader 遇到 NaN 可能直接跳过。另外检查len(d)是否满足指标最小周期。6.2 策略逻辑的排查思路策略不交易或者交易异常我一般按这个顺序排查指标是否有值打印self.fast_ma[0]看是不是 NaN。信号条件是否触发在next()里加 print看条件判断结果。订单是否被拒绝检查资金是否足够、股数是否取整到 0。持仓状态是否正确self.getposition(d).size是否符合预期。时间轴是否对齐多股时确认当前 bar 是否所有数据都有值。这套流程能解决 90% 的“策略不动”问题。剩下的 10% 通常是数据问题回到数据层查。6.3 性能优化的实用技巧全市场多年数据回测backtrader 跑起来可能很慢。我试过几个有效的优化减少数据量只加载回测区间内的数据不要全历史。用 PandasData 而非 CSVData内存数据比文件读取快很多。关闭不必要的 Analyzer每个 Analyzer 都有开销只留需要的。分批回测按年份或按板块分批跑最后汇总。用 PyPybacktrader 在 PyPy 下速度提升明显但部分库兼容性要注意。注意优化性能之前先确保结果正确。我见过有人为了提速把复权关了结果回测快了一倍结论全错。7. 我踩过的坑和几条实在建议搭这套系统的过程中有几个坑让我印象特别深。第一个是数据版本管理早期我直接覆盖更新数据后来发现同一策略两次回测结果不一致查了很久才定位到是数据源调整了复权因子。从那以后我给每次数据更新都打版本号回测记录里也带上数据版本。第二个是多股回测的资金分配一开始我用等权买入结果同时出信号的股票太多资金不够backtrader 直接拒绝订单策略表现和预期完全不符。后来改成动态计算可用资金和最大持仓数才稳定下来。第三个是过度依赖参数优化我曾经花了一周优化一个策略的参数样本内夏普做到 2.5样本外直接负收益。那次之后我给自己定了规矩参数优化只用来理解策略的敏感度不用来选“最优参数”。如果你也在搭自己的回测系统我的建议是先把数据链路做扎实再跑通单股回测然后是多股最后才是参数优化和复杂策略。每一步都做验证别跳步。回测系统的价值不在于跑得多快、策略多花哨而在于它给出的结论你能不能信。信不过的回测跑一万次也没用。这套系统后续还可以扩展的方向不少比如接入分钟级数据做日内回测、加入基本面因子做多因子选股、把回测信号直接对接模拟盘做纸面交易验证。但那是下一步的事眼下先把日频、多股、含成本的链路跑稳比什么都重要。