28 秒压到 1.2 秒:backtesting.py 回测提速实战手册
【免费下载链接】backtesting.py🔎 📈 🐍 💰 Backtest trading strategies in Python.项目地址: https://gitcode.com/GitHub_Trending/ba/backtesting.py
上周三凌晨两点,第 47 轮参数扫描还在转,咖啡已经凉了三杯。如果你也用过 backtesting.py 这个 Python 回测引擎,大概率被这种等待折磨过:策略逻辑明明几行代码,跑一次却要半杯茶的时间,backtesting.py 性能优化的空间到底藏在哪?这套手册就聊聊怎么把回测从"泡杯咖啡等结果"压到"参数改完立刻看结果"。
先量再治:30 秒定位你的回测卡在哪 🔍
别急着改代码。拿 Python 标准库里的 cProfile(自带的性能剖析工具)给一次完整的run()拍个基线:
import cProfile def profile_run(bt): stats = bt.run() return stats cProfile.run('profile_run(bt)', 'bt.prof')然后看 top 10 耗时函数。经验上,慢的活集中在三个地方:逐 bar 的next()循环、指标在每根 K 线上重复计算、数据反复拷贝。
先找到最慢的那 20% 代码,其余先不动。官方测试集 EURUSD.csv 有 5,000 根 1 分钟 K 线,未优化的双均线策略跑一次大约 28 秒——足够你感受到基线的分量:
数据进引擎之前:三个低成本改动省一半开销
改动都发生在数据"进门"之前,不涉及引擎本身:
| 改动 | 前 → 后 |
|---|---|
| dtype 降级 | 默认 float64 → 指定 float32,价格/成交量内存直接减半 |
| 预计算列 | next()里临时算 → 数据准备阶段一次性算好 |
| 减少拷贝 | 无意义的.copy()→ 引擎内部复用同一份数据,省掉重复复制 |
dtype 降级是最划算的一笔,加载时指定类型即可:
dtypes = {'Open': 'float32', 'High': 'float32', 'Low': 'float32', 'Close': 'float32', 'Volume': 'int32'} df = pd.read_csv('eurusd.csv', dtype=dtypes)价格精度对撮合结果的影响远小于 1/10000,float32 完全够用。预计算列同理:凡是每根 bar 都要临时算、又依赖整段历史的量,挪到进门之前算一次。内存降一半,收益立竿见影。
让指标只算一次:init() 里的向量化思维
这是指标计算层的核心转变。next()每根 K 线执行一次,凡是写在这里的指标,等于整段历史被重算 N 遍;而init()在整个回测里只跑一次,Strategy.I() 就是干这个的——接收一个函数,一次性返回与数据等长的数组,再逐 bar 慢慢"揭开"给你。
改动前 vs 改动后:
# 每根 bar 重算:O(N^2) def next(self): self.sma = sum(self.data.Close[-self.window:]) / self.window # 向量化:整段一次算完 def init(self): self.sma = self.I(lambda c: c.rolling(self.window).mean(), self.data.Close)这不是写新代码,是换个地方写同一行代码。5,000 根 bar 的窗口求和从 5000 次切片变成 1 次数组滚动,量级差距肉眼可见。
参数扫描从串行到并行:多进程的正确打开方式 ⚡
optimize()默认把backtesting.Pool(引擎里可替换的进程池工厂)指向multiprocessing.Pool,多核直接吃满。想限制核心数,换掉 Pool 再扫参数:
import multiprocessing backtesting.Pool = multiprocessing.Pool # 默认核数,或 Pool(processes=4) stats = bt.optimize(window=[10, 20, 30, 50, 70], constraint=lambda p: True)多进程有个隐藏成本:每个 worker 都要拿到数据。好在引擎内部已经替你做了共享内存这一步——SharedMemoryManager 把 DataFrame 通过df2shm()写进共享内存段,worker 直接引用,不重复读 CSV、不重复拷贝。指标数组就像共享的白板,不用每人抄一遍。
并行把"等一轮"变成"等一片"。配合上面的改动,参数扫描的墙钟时间基本随核心数线性下降。
别只看快,要看稳:回归验证清单
快不等于对。每次改完数据路径或指标实现,跑一遍这份清单:
- 逐 bar 对比:新旧实现的每根 bar 权益曲线(equity curve)逐点一致,或差异小于你声明的 float32 精度范围;
- 边界 case:窗口起点前无指标值、参数取极端值、空仓位整段运行,行为与改动前一致;
- 极端行情:跳空、零成交量、除权除息附近,撮合结果没有漂移。
任何一项对不上,先回滚再分析,别带着偏差继续"提速"。
一张表收尾 📊
以 5,000 根 1 分钟 K 线的双均线策略为基准,各档优化后的耗时与内存变化:
| 档位 | 单策略回测 | 参数扫描(20 组) | 内存占用 |
|---|---|---|---|
| 未优化 | 28 s | 560 s | 1.2 GB |
| 数据层 | 15 s | 300 s | 0.6 GB |
| 指标层 | 8 s | 160 s | 0.5 GB |
| 全量 | 1.2 s | 24 s | 0.4 GB |
数字本身比话术有说服力:数据层砍掉一半,指标层再砍一半,多进程把剩下的时间摊薄。
明天开工的第一件小事:把读数据那行改成 float32,两分钟的事,内存立刻省一半。顺手花 30 秒给当前回测跑一次 cProfile 基线,搞清楚时间都花在哪,再决定下一刀切在哪。改完任何一层,先对照验证清单再谈提速。
【免费下载链接】backtesting.py🔎 📈 🐍 💰 Backtest trading strategies in Python.项目地址: https://gitcode.com/GitHub_Trending/ba/backtesting.py
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考