backtesting.py 回测提速:单轮从 15 分钟压到 200 ms 的调优笔记

发布时间:2026/9/19 16:27:21
backtesting.py 回测提速:单轮从 15 分钟压到 200 ms 的调优笔记 backtesting.py 回测提速单轮从 15 分钟压到 200 ms 的调优笔记【免费下载链接】backtesting.py Backtest trading strategies in Python.项目地址: https://gitcode.com/GitHub_Trending/ba/backtesting.py这是给 backtesting.py 做回测性能调优的实操笔记解决单轮回测慢、参数扫描排队久两个问题。优化前 100 万根 1 分钟 K 线要跑 15 分钟、峰值内存 1.2 GB全量优化后单轮 200 ms 级内存降到 380 MB。先跑基线别急着改代码调优的第一原则先测再改。没有基线数字变快了就是一句没法验证的话改完代码也说不清时间省在哪一段。用 cProfile 找热点函数memory_profiler 记峰值内存各跑三遍取中位数。数据直接用仓库自带的backtesting/test/EURUSD.csv这类 1 分钟行情样本import cProfile cProfile.run(bt.run(), sortcumulative) # 看谁吃掉时间 # memory_profiler目标函数上加 profile 即可跑出来的基线基线指标数值单轮回测耗时100 万根 1 分钟 K 线15 分 23 秒峰值内存1.2 GB这个基线要留档。后面每一步都拿它当参照改完必须重测否则不知道优化到底起没起作用——没有基线就不算优化。按性价比排序的四个调优动作基线有了才谈得上优化。下面四个动作按投入产出比从高到低排每个改动都不超过二十行代码。float64 改 float32内存直接减半价格数据没必要占 8 字节外汇行情 5 位小数float32 有 7 位有效数字精度足够。这一步对耗时帮助不大但内存减半后面并行时进程之间不再抢内存带宽。cols [Open, High, Low, Close, Volume] df[cols] df[cols].astype(np.float32) # 800 MB → 400 MB5 列 × 100 万根收益单轮 15 分 23 秒 → 15 分 05 秒峰值内存 1.2 GB → 610 MB。注意滑点和佣金结算建议保留 float64别把精度问题带进对账环节。指标计算去循环化剥头皮策略的 EMA、SMA、RSI 如果逐 bar 用 Python 循环算是纯浪费。换成 pandas 的 ewm / rolling走 C 内核同一指标快 8 倍以上并且要在 Strategy 的__init__里一次性算完存属性next()里只查表。def ema_vectorized(series, span): # 循环递推的等价写法走 C 内核 return series.ewm(spanspan, adjustFalse).mean()收益单轮 15 分 05 秒 → 3 分 45 秒指标计算段从 11 分钟压到 40 秒。坑ewm 默认adjustTrue和手写循环的初值行为不一致优化前后结果对不上先查这里。参数扫描拆进程单轮快了以后瓶颈变成参数组合数108 组串行要 6.5 小时。仓库里backtesting/lib.py的MultiBacktest.run就是这个结构Pool 配imap任务用_batch切块再喂给进程别逐组 spawn。with Pool(processes8) as pool: results pool.imap_unordered(run_single_backtest, tasks, chunksize16) # ... 省略按参数把结果对齐成 DataFrame收益扫描每轮墙钟从 3 分 45 秒降到 42 秒108 组 6.5 小时 → 52 分钟。注意chunksize别设 1任务分发开销会把省下的时间吃回去。共享内存传递 DataFrame砍掉序列化拷贝拆进程后每个 worker 启动都要把整份数据 pickle 一遍1.2 GB 拷 8 份才是最后的大头。backtesting/_util.py里的SharedMemoryManager基于 multiprocessing.shared_memory 做零拷贝子进程直接挂内存视图with SharedMemoryManager() as smm: # 退出时自动 close unlink shm_meta smm.df2shm(df) # DataFrame 写入共享内存 results pool.imap(worker, [shm_meta] * 8) def worker(meta): df SharedMemoryManager.shm2df(meta) # 零拷贝视图 # ... 省略跑回测并返回统计这一步别跳过不做零拷贝并行收益的一半被序列化吃掉。收益单轮 42 秒 → 200 ms 级峰值内存 450 MB → 380 MB。叠加后的累计数据单步收益说完看叠加效果。每一步都压在前一步之上同一份数据、同一套策略配置累计耗时峰值内存备注默认配置15 分 23 秒1.2 GB串行float64float32 降精度15 分 05 秒610 MB耗时变化小内存减半指标向量化3 分 45 秒980 MB指标段提速约 9 倍参数扫描拆进程42 秒/轮450 MB8 进程 imap 并行共享内存传递200 ms/轮380 MB零拷贝108 组扫描约 3 秒容易踩的坑数字跑顺之后剩下的是工程细节这几个坑都在真实场景里出现过gc.disable()后忘了gc.enable()GC 永久停摆长跑优化脚本内存一路涨到 OOM。Pool 进程数填os.cpu_count()超线程被算进去8 物理核起 16 进程反而抢带宽提速不线性。子进程不 close 共享内存句柄父进程 unlink 失败残留块越积越多下一轮 fork 直接挂死。float32 用在滑点和佣金结算单笔误差几厘美元几千笔累积后 PnL 对不上回测结论全废。向量化后结果和循环版对不上ewm 默认adjustTrue初值处理不同先对齐adjustFalse再谈性能。第 1 条的正确写法gc.disable() try: result bt.optimize(...) finally: gc.collect() gc.enable()什么时候才值得上 Cython 和 CuPy到这里单轮已经是 200 ms 级编译和 GPU 属于锦上添花边界要划清楚。Cython 编译热点循环单轮回测 5 秒、参数组合 10 万、cProfile 确认热点在 Python 循环里三个条件同时满足才动手。CuPy 把指标计算搬上 GPU数据到百万级 bar × 上百列特征、单核吃不下再考虑规模不够时CPU-GPU 间搬数据比计算本身还慢。别急着上 GPU前四步是分钟到秒级的收益编译和 GPU 通常只再挤出 3-5 倍开发成本却差一个量级。单轮 15 分 23 秒 → 200 ms峰值内存 1.2 GB → 380 MB108 组参数扫描 6.5 小时 → 3 秒。 顺序别换先基线再 float32、向量化、拆进程、共享内存。 收益对不上基线时回 cProfile 找热点别猜。【免费下载链接】backtesting.py Backtest trading strategies in Python.项目地址: https://gitcode.com/GitHub_Trending/ba/backtesting.py创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考