Backtesting.py / Architecture

Backtesting.py 的两类 API:Backtest 与 Strategy 组件与生命周期

Backtesting.py 的架构围绕两个核心类展开:Backtest 负责引擎(现金、佣金、撮合、统计),Strategy 负责策略逻辑(init 计算指标、next 逐 bar 交易)。把这两者的分工、调用顺序与可配置项看一遍,再读官方 Quickstart 或别人的脚本就不会卡在「这行代码归谁管」上。本页按官方 README 与文档整理,具体字段与行为以官方文档为准。

Backtest:回测引擎Strategy:策略基类lib / test:工具与示例
Backtest引擎 · 撮合 · 统计
Strategy.init()预计算指标
Strategy.next()逐 bar 交易
backtesting.libcrossover / SMA 工具
Backtesting.py 组件架构示意(schematic,基于官方 README 整理,非官方架构图)。
Components

模块划分:每个环节归谁管

Backtesting.py 的模块划分:读脚本时最先要分清的是「这行代码属于引擎还是属于策略」。下表按官方 README 的模块划分整理,最后一列是实际用起来最容易忽略的地方。

模块 / 环节负责什么你怎么用注意点
backtesting.Backtest回测引擎:初始资金、佣金、订单撮合、执行回测与汇总统计Backtest(数据, 策略类, ...) 创建实例,再调用 run() / optimize() / plot()成交时点与成本假设由引擎决定,策略里改不了;假设差异要回到引擎参数上看
backtesting.Strategy策略基类:只描述「什么时候买、什么时候卖」,不做撮合继承它并实现 init()next(),把信号写成下单调用策略里不要试图干预成交价与手续费,那是引擎的职责
Strategy.init()回测开始前调用一次,用于预计算指标self.I(...) 注册指标,之后在 next() 里按下标取值这里只做「准备」不做交易;把下单写进 init 会让回测在无行情的时刻就下单
Strategy.next()每根 bar 调用一次,在这里判断条件并下单self.buy() / self.sell() / self.position.close()此刻只能拿到「到当前 bar 为止」的信息,用未来数据会引入前瞻偏差
backtesting.lib组合式工具函数(如交叉判断),把常见写法封装成可复用函数from backtesting.lib import crossover 后在 next() 中使用它只是工具集合,不含撮合与统计,别把它当成独立的回测入口
backtesting.test内置示例数据与示例指标,用来跑通流程from backtesting.test import SMA, GOOG示例数据只用于验证代码路径,不能拿它的结果说明任何策略优劣
Backtest.run()按当前配置执行一次完整回测并返回绩效结果stats = bt.run(),随后打印或取值返回结果的字段命名与口径以官方文档与实现为准,跨版本可能调整
Backtest.optimize()内置优化器:按参数网格重复回测并给出对比结果传入参数范围后取扫描结果,配合热力图观察网格越密越容易在样本内挑出「看起来好」的参数,需配合样本外验证
一句话记法:数据与成本假设归引擎,指标与买卖判断归策略;两边混写是回测读不懂、结果对不上的主要原因。模块名与调用方式以官方 README 与文档为准。
Lifecycle

一次回测的调用顺序

下面五步是用 Backtesting.py 跑通一次回测的最小顺序,每步给出示例代码、一句话说明与预期结果;代码取自官方 Quickstart 的写法,替换标的与参数即可复现。

  1. 准备 OHLCV 数据

    from backtesting.test import GOOG   # 示例数据
    print(GOOG.tail())                  # 确认列名与时间索引

    引擎只接受带时间索引、含开高低收(可选成交量)的单标的数据;先确认数据形状对不对,再谈策略。

    预期结果:打印出以时间为索引、包含 Open/High/Low/Close/Volume 若干行的表格。

  2. 写策略类:init 预计算指标

    class SmaCross(Strategy):
        def init(self):
            self.ma1 = self.I(SMA, self.data.Close, 10)

    把指标计算集中放在 init,用 self.I() 注册;注册后的序列可以在 next 里按下标访问。

    预期结果:回测启动时指标被算出一遍,next 里能直接取到 self.ma1[-1] 这类最新值。

  3. 在 next 里写买卖判断

        def next(self):
            if crossover(self.ma1, self.ma2):
                self.buy()
            elif crossover(self.ma2, self.ma1):
                self.position.close()

    next 每根 bar 执行一次,只做「判断 + 下单」;是否成交、以什么价格成交交给引擎。

    预期结果:出现交叉的 bar 上产生一笔记账订单,未交叉的 bar 不产生订单。

  4. 创建 Backtest 并运行

    bt = Backtest(GOOG, SmaCross, cash=10000,
                  commission=.002, exclusive_orders=True)
    stats = bt.run()

    资金、佣金、订单互斥等成本与撮合假设都在这一步设定,属于引擎参数。

    预期结果:stats 返回一次回测的绩效汇总,可直接打印查看。

  5. 看结果与可视化

    bt.plot()          # 交互式图表
    bt.optimize(...)   # 需要时做参数扫描

    先看权益与回撤曲线,再决定要不要做参数扫描;扫描结果要用样本外区间复核。

    预期结果:输出可交互的图表对象(在 Notebook 或浏览器中查看),参数扫描时返回多组参数的对比结果。

顺序不能颠倒:数据形状不对、指标放进 next 里逐 bar 重算、成本假设放在最后补——这三件事都会让结果看起来「差一点」却查不出原因。示例代码的接口名与行为以官方 README 与文档为准。
Timing

生命周期:两个方法分别在什么时候被调用

Backtesting.py 的策略类只有两个关键方法,调试回测时最常见的问题是「这行代码到底跑了没有、跑在什么时候」。下表把调用时机与常见坑放在一起对照。

环节调用时机能做什么常见坑
init()回测开始前,只调用一次注册并预计算指标、准备常量与参数在里面写交易逻辑:此时还没有逐 bar 的上下文,行为与预期不一致
next()每根 bar 调用一次读取当前及历史指标值、判断信号、下单把指标计算写在 next 里逐 bar 重算,既慢又容易和 init 里的版本不一致
取指标最新值next 执行过程中[-1] 取最新、[-2] 取上一根,用于比较与交叉判断把下标写错一位就等于用了未来数据,回测会异常好看
读行情next 执行过程中通过 self.data 访问开高低收与成交量序列直接用外部变量取全量数据做判断,会绕过「当前时点」约束
下单next 执行过程中调用买入、卖出或平仓接口提交订单默认撮合时点与「看见信号的那根 bar」并不相同,需对照引擎参数确认
读仓位与权益next 执行过程中查看是否持仓、当前权益,用于加仓、减仓与风控判断把权益变化当成收益结论:它包含了未平仓浮盈浮亏
回测结束run() 返回之后读取绩效汇总与交易明细,做可视化只看一个总收益数字就下结论,忽略区间、成本与交易笔数
Backtest Engine

引擎的可配置项:改一个变量,结果差多少

同一个策略换一组 Backtesting.py 引擎参数,绩效数字会明显不同。下表列出最常被改动、也最容易被忽略的几项,参数名与默认值以官方文档为准。

配置作用典型写法适用场景注意点
cash初始资金,决定每笔能下多大cash=10000单标的策略验证资金过小时受最小交易单位限制,会出现「想买买不进」,结果失真
commission单边手续费率commission=.002想把成本假设写进回测只表达手续费;滑点与冲击成本需要自己折算进去,否则高频策略会虚高
exclusive_orders订单是否互斥,避免同方向重复挂单exclusive_orders=True信号密集、容易出现同向重复下单的策略开启后新订单会取代未成交的旧订单,适合单一时点只持有一笔的写法
trade_on_close是否用当前 bar 收盘价成交trade_on_close=True希望缩短「出信号」到「成交」的延迟与默认成交时点的假设不同,切换前要明确两种口径的差异,别把两种结果混着比较
传入的行情数据决定回测标的与可用的数据列Backtest(data, SmaCross)单标的(股票 / 期货 / 加密)日线或分钟线需要带时间索引的 OHLC(V) 结构;多资产组合不在支持范围内
策略类的参数把策略里的窗口、阈值变成可扫描的属性在策略子类上声明属性后交给 optimize()参数调优与稳健性检查可扫描的参数写法与范围定义以官方文档为准;扫描出的较优参数需样本外复核
建议做法:把「数据区间 + 引擎参数 + 策略参数」三件套写进结论旁边。只报一个收益率、不报这三项的回测,别人无法复核,也无法与其它结果对照。
Outputs

run() 之后你拿到什么

在 Backtesting.py 里跑完一次回测,返回值不只有一个总收益数字,还有交易明细与图表可以交叉核对。下表按「拿到什么 / 怎么用」整理,字段名与结构以官方文档与实际输出为准。

拿到什么含义怎么用注意点
绩效汇总一次回测的指标集合,run() 直接返回打印查看,或取值与其它区间对照指标口径与字段命名以官方文档为准;换区间这些数字会整体变化
交易明细逐笔交易记录,以表结构保存核对每笔的进出时点、手续费与持仓变化列名与结构以官方实现为准;笔数太少时统计指标本身不稳
交互式图表价格、买卖点、权益与回撤的可视化目视检查回撤段、信号密集处与成本拖累交互式输出,做静态报告需自行导出;图上「很好看」不代表策略稳健
参数扫描结果optimize() 给出的多组参数对比找连成一片的稳定区间,而不是单点峰值扫描越密越容易过拟合;结果必须在样本外区间复核
收益与回撤指标历史区间的统计量先看区间与成本假设,再看大小历史表现不构成未来收益预期,也不构成投资建议
信号与成交的差异信号时点与成交时点之间的偏差结合 trade_on_close 与默认成交假设解释差异差异来源与默认假设以官方文档说明为准,不要凭感觉归因
Pitfalls

读脚本时的四类结构性偏差

下面几种情况在 Backtesting.py 里不会报错,但会让结果系统性偏乐观或偏悲观;它们都出在「引擎与策略的分工」上。

情形你会看到什么修正方向为什么
用未来信息做判断回测曲线异常平滑、回撤极小只用 [-1] 及更早的值,或把信号整体后移一根 bar当下时点拿不到尚未发生的行情,用到了就是前瞻偏差
指标在 next 里逐 bar 重算跑得慢,且结果与 init 版本对不上指标统一放 init,用 self.I() 注册一次重复计算不仅慢,还容易在两处写出不同参数
成本假设缺失交易越频繁,收益越漂亮补上手续费,并把滑点折算进成本真实交易每次进出都要付成本,缺了这项等于给高频策略额外加分
只看总收益一个数字很好,但说不清怎么来的同时看交易笔数、回撤与区间总收益是区间与成本的函数,脱离这两项无法复核
参数在样本内挑较优样本内漂亮、样本外立刻变差留出样本外区间,优先选连成片的参数区间单点峰值多半是历史噪声,不是可复现的规律
拿示例数据下结论示例跑得通,结论却无法推广换成自己的标的数据重跑一遍内置示例只用于验证代码路径,不代表任何市场表现
Key API

策略里最常用的 API 速查

下表覆盖用 Backtesting.py 写一个单标的策略所需的最小集合;写法示例基于官方 README,具体签名与返回值以官方文档为准。

API含义典型写法注意点
self.data.Close当前标的收盘价序列作为指标输入传入 self.I()另有开高低与成交量列,列名以官方实现为准
self.I()注册并预计算指标self.ma = self.I(SMA, self.data.Close, 10)只在 init 里注册;注册后按索引取值
self.buy()提交买入在满足条件的 bar 上调用是否成交、以什么价格成交由引擎决定
self.sell()提交卖出同样在 next 中按条件调用exclusive_orders 配合时要确认订单互斥行为
self.position当前持仓状态判断是否已有仓位、是否多头或空头用它做加仓判断时要注意与订单互斥设置的配合
self.position.close()平掉当前仓位反向信号出现时调用平仓同样会产生成本,需计入假设
self.equity当前权益(含未平仓浮盈亏)用于观察权益曲线或做风控判断不是已实现收益;把它当收益结论会误判
crossover()判断两条序列是否发生交叉crossover(self.ma1, self.ma2)来自 backtesting.lib,属工具函数而非引擎能力
Scenarios

这套架构适合什么、不适合什么

Backtesting.py 的能力边界和它的架构一样清晰:为「单标的 + 写得清楚」而生,超出这个范围就该换工具。

场景适合吗说明更合适的方向
单标的均线 / 形态信号验证适合Backtest + Strategy 两件套就能写完,接口少、可读性高
策略原型与教学演示适合几十行代码跑通完整流程,适合讲清「信号 → 成交 → 统计」的链路
小范围参数扫描与稳健性检查适合内置优化器 + 热力图即可观察参数邻域是否成片网格规模过大时需分批运行
多资产组合与权重分配不适合架构面向单标的,组合层面的资金分配不在支持范围内选择支持组合回测的框架,或自行拆分标的分别回测后汇总
分数股 / 精细资金管理不适合不做分数股交易,资金较小或需要按比例建仓时会有偏差改用其它支持分数股假设的框架
高频与撮合细节研究不适合成交假设做了简化,不建模排队、部分成交与冲击成本需要更细撮合模型时选低层事件驱动框架

先跑通再优化

用示例数据把「信号 → 成交 → 统计」这条链路跑通,确认每一环都能解释清楚,再替换成自己的标的与参数。顺序反了容易把数据问题误判成策略问题。

把假设写在结论旁边

区间、手续费、成交时点是结论的一部分。换掉其中任一项,绩效数字都会变,只报收益率等于不给复核条件。

超出边界就换工具

需要多资产组合、分数股或更细的撮合模型时,这套架构不是「加几行代码」能补的,换到对应的框架比打补丁更省时间。

边界与免责:本页内容用于理解项目结构与安装判断,回测结果只反映历史区间,不构成收益预期或投资建议;实盘涉及真实资金与滑点差异,需自行评估。
FAQ

架构常见问题

init 和 next 有什么区别?

init 在回测开始前调用一次,用来预计算指标;next 每根 bar 调用一次,用来判断信号并下单。判断标准很简单:只做「准备」的写进 init,涉及「当前时点该不该交易」的写进 next。两者的调用细节以官方文档为准。

它属于事件驱动还是向量化框架?

它以逐 bar 迭代的方式运行:每个 bar 调用一次 next,因此更接近轻量级事件驱动写法,而不是一次性整块矩阵运算的向量化框架。这决定了它的可读性优势,也决定了它不适合超大规模的参数与标的批量运算。判断口径以官方实现为准。

self.I() 到底做了什么?

在 Backtesting.py 中,它把指标函数与输入数据注册到策略上,在回测开始前统一计算一次,之后可以在 next 里通过注册后的变量按下标取值。这样做的好处是既避免逐 bar 重复计算,也让指标与信号的取值位置有明确约定。返回结构与内部实现以官方文档为准。

Backtest 和 Strategy 能分开复用吗?

可以。数据与成本假设在 Backtest 一侧,交易逻辑在 Strategy 一侧,同一个策略类可以换标的、换区间、换参数重复使用;同理,同一个标的也可以换不同策略类做对照。边界在于策略类只能拿到引擎提供的数据与仓位视图,拿不到的字段以官方文档为准。

改了佣金或成交时点,结果差很多正常吗?

正常。手续费直接削减每笔交易的净收益,成交时点决定「看到信号」与「实际成交」之间隔了几根 bar,两者都会改变绩效数字。所以对照两版结果前,要先确认这两项是否一致;假设差异与默认行为以官方文档说明为准,本页不给出具体的收益差异数值。

能用它做多资产组合或实盘吗?

多资产组合与分数股交易不在支持范围内,这是架构层面的边界而不是配置问题;实盘执行也不在它的职责里,它只负责回测与统计。需要组合或实盘时,通常是换框架、或在回测结论确认后自行对接交易通道,并自行评估风险。能力范围以官方 README 与文档为准。

下一步

理解分工之后,用首次运行示例把 Backtest + Strategy 串起来,或直接进入策略开发与参数优化。