First run · SmaCross

Backtesting.py 首次运行:SmaCross 双均线示例教程

装好之后的首要任务不是写复杂策略,而是用官方 README 的 SmaCross 双均线交叉示例跑通一遍完整链路:导入模块 → 定义策略 → 创建回测 → 出结果与图。这个示例用库内置的 GOOG 日线数据,不需要联网、也不需要自备行情文件,几分钟内就能看到一份真实可解释的输出。本页把它拆成「可复制代码 + 每步预期输出 + 结果字段怎么读 + 首跑常见坑」,让你确认环境与理解链路,而不是只看到一串数字。之所以从这里开始,是因为 Backtesting.py 的绝大多数「结果不对」都发生在数据与口径环节,而不是策略写法:先用内置数据确认代码链路,再把数据换成自己的,出问题时才好定位是哪一环变了。

示例:SmaCross(10/20 均线)数据:内置 GOOG 日线输出:stats + 交互图前置:已装 backtesting 与 bokeh
安装环境pip install backtesting
定义 SmaCross继承 Strategy
bt.run()执行回测
读 stats / 出图bt.plot()
基于官方 README 快速开始的首次运行流程示意(schematic),非真实运行结果。
测试版本backtesting 0.6.6(PyPI 发布版)Python 版本本站实测 3.11.9;官方要求以 README / PyPI 为准最后核验2026-09-29(实验数据 2026-09-24 运行)数据来源官方内置 GOOG 样例 + 新浪日线 600519 前复权,2018-01-02~2026-09-18适用市场A 股 · 美股样例可直接复现是(含复现记录模板与输出字段)含交易成本是(默认成本 / 零成本 / 收盘成交三种口径对照)样本外验证否(本页先跑通链路,样本外流程见回测可信度审计页)
Quickstart

完整快速开始代码是什么?Backtesting.py 的 SmaCross 双均线示例

下面这段代码来自 Backtesting.py 官方 README 的快速开始(中文注释为本页补充,2026-09 核验)。它一次性展示四件事:怎么导入、怎么定义策略、怎么创建回测、怎么取结果与出图。整段代码不到 20 行,却覆盖了后面所有复杂策略的同一个骨架——先用它把「能跑通」和「看得懂」这两件事同时拿到手。

from backtesting import Backtest, Strategy
from backtesting.lib import crossover
from backtesting.test import SMA, GOOG

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

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

bt = Backtest(GOOG, SmaCross, cash=10_000, commission=.002, exclusive_orders=True)
stats = bt.run()
print(stats)
bt.plot()
先看懂三个参数再改:cash 是初始资金、commission 是单边手续费比例、exclusive_orders=True 表示每次下单前先平掉反向仓位(不会同时持有多空)。参数名与默认值以官方文档为准;换成自己的数据前,建议先原样跑通一次,确认环境没问题,也能顺手记下这组基线结果作为后续对照。
Steps

Backtesting.py 首次运行怎么分步跑通?五步走

把这段示例拆成五步,每一步都给出代码、说明和可以直接核对的预期输出。这样做的原因是:报错信息往往指向最后一行,而真正的起因经常在前面几步;跑不到预期结果时就停在这一步排查,而不是继续往下写代码。

  1. 导入模块与示例数据

    从 backtesting 取 Backtest / Strategy 两个类,从 backtesting.lib 取交叉判断函数,从 backtesting.test 取示例指标 SMA 与示例数据 GOOG——这三行同时验证了 Backtesting.py 是否装在了当前解释器里。「包没装对」比「策略写错」更常见,也更容易被误判成策略问题。

    from backtesting import Backtest, Strategy
    from backtesting.lib import crossover
    from backtesting.test import SMA, GOOG

    预期输出:无报错。若提示 ModuleNotFoundError,说明当前解释器里没有装包(见安装页自检清单)。

  2. 定义策略类

    继承 Strategy,在 init() 里计算指标(这里用 10 日与 20 日两条均线),在 next() 里逐 bar 写交易逻辑;指标通过 self.I() 注册后,策略里随时可以按序列取用。

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

    预期输出:类定义完成,无输出。若把窗口改得比数据长度还大,指标会全是空值,回测不会报错但不会成交——这类「静默失效」是首跑最常见的坑,所以下面专门给了一张对照表。

  3. 写交易规则

    用 crossover() 判断两条均线谁上穿谁:快线上穿慢线时 self.buy(),慢线上穿快线时 self.sell()。这一段就是策略的全部「决策逻辑」。

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

    预期输出:无输出。真正的成交记录要到 run() 之后才能看到。

  4. 创建回测并运行

    把数据、策略类与资金/成本参数交给 Backtest;run() 会按 bar 推进并返回统计结果。exclusive_orders=True 让每次信号先平反向仓,避免同时多空。

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

    预期输出:若无异常,说明整条链路已跑通;耗时通常在一秒级(随机器与数据长度变化)。

  5. 读结果与出图

    print(stats) 会打印一份纵向的统计 Series;bt.plot() 打开基于 bokeh 的交互式图表,可缩放查看价格、买卖点与权益曲线。

    print(stats)
    bt.plot()

    预期输出:终端打印以 Return [%]、Max. Drawdown [%]、Sharpe Ratio、# Trades 等字段开头的结果面板;浏览器打开一张可交互图表(价格 + 买卖点 + 权益曲线)。字段名以实际返回为准,不同版本可能略有差异。

Results

Backtesting.py 回测结果字段怎么读

Backtesting.py 的 run() 返回的是一份统计结果(可按 Series 处理)。下面这八项是初次跑完最该看的,重点不是数值大小,而是「这个数字在什么前提下才算可信」——同一份代码换区间、换成本假设,读数就会变,所以字段含义要和对它的判断方式一起记。字段名与计算口径以官方实现为准。

字段含义怎么判断常见误读
Equity Final [$]回测结束时的账户权益与初始资金对比看盈亏方向把它当成「能拿到的钱」,忽略成本与滑点假设
Return [%]区间总收益率先确认区间长度,再和基准对比拿不同区间的收益率直接互相比较
Buy & Hold Return [%]同期买入持有的收益率策略跑不赢它,就要问为什么还要交易完全忽略这条基准,只看策略绝对收益
Max. Drawdown [%]区间内回撤幅度先问自己能否承受这个回撤再谈收益当成「未来的回撤上限」——换区间就会变
Sharpe Ratio单位波动的超额收益配合交易次数看,样本太少时意义有限把它当成「必赚」或跨标的可比的绝对分数
Win Rate [%]盈利交易占比结合盈亏比一起看,高胜率也可能亏只看胜率高低,忽略单笔盈亏大小
# Trades成交笔数笔数太少说明结论不稳,太多说明成本吃收益认为笔数越多越可信
Commission [$] 等成本项手续费等成本合计看成本占收益的比例是否过高用零成本参数跑完就下结论
Start / End / Duration回测区间与跨度确认区间是否覆盖完整涨跌、是否够长不看区间就横向比较两个策略
Profit Factor / SQN盈亏比与系统质量类指标样本笔数足够时再看,作为稳定性参考把单一指标当成挑选策略的全部依据
口径提醒:同一份代码换一个区间、换一组成本假设,上述数字都会变;比较之前先把区间、手续费与成交时点统一。结果仅代表历史表现,不构成收益保证或投资建议;Backtesting.py 统计项的字段名与计算方式以官方实现为准,跨版本对比前先核对字段。
First-run pitfalls

Backtesting.py 首跑最容易出问题的六处

示例能跑通不代表结论可信。下面这六种情况在初次换数据时最常见,共同点是「不报错但结论不对」,所以要主动检查而不是等报错。检查顺序建议固定下来:先看索引与列名,再看缺失与复权,最后才动策略参数——顺序反了会把数据问题误判成策略问题。

现象成因处理方式提前规避
跑完几乎不成交均线窗口比数据长度还长,或数据段太短缩短窗口或换更长区间,先让信号出现跑之前先打印数据行数与起止日期
报找不到列或列全是空列名大小写/拼写与要求不一致(如 close 与 Close)把列名统一成 Open/High/Low/Close/Volume加载数据后立刻打印 df.columns
结果明显好得反常时间索引未按升序排列,或存在重复日期排序并去重后重跑,对比差异每次加载后检查索引是否单调递增
某些区间整段无交易缺失值 / 停牌日未处理,序列出现断点补齐或明确剔除,并在结论里说明处理方式统计缺失比例,超过容忍度就换数据源
换数据后收益暴涨未复权或复权口径不同,价格跳空被当成机会统一复权口径,注明数据来源与处理方式在结论里写清口径,别让两次实验混在一起
同一份数据越跑越慢在 next() 里做逐 bar 重计算等低效操作把指标计算移到 init(),只为需要的位置取数先跑小样本验证逻辑,再上全量数据
改了代码结果不变运行的仍是缓存/旧文件,或改的不是被导入的那个模块确认文件路径与运行命令,必要时重启解释器改完先跑一次「明显会变」的小改动做对照
数据本身也是假设:列名、复权口径、交易日历与缺失处理方式都会改变结论。这些问题的表现往往不是报错而是「结果看起来很怪」,所以更要在跑之前就检查。具体字段要求以官方文档为准,本页给的是排查顺序。
Plot & save

出图与结果保存怎么做?把一次回测变成可复核的材料

图不只是好看——它是排查「买卖点是不是打在意料之外的位置」最快的工具。首次运行之后通常有三个后续动作:看交互图的项目、把图表存成文件、把结果拿去二次分析。三条都做完,这次回测才算留下可复核的材料,而不是屏幕上闪过的一串数字。

交互式图表看什么

价格走势叠加买卖标记,下方是权益曲线。重点看两类异常:信号是否密集出现在同一段震荡里(成本会被吃掉),以及权益曲线是否由少数几笔交易撑起来——后者通常意味着结论不稳。

把图表存成文件

图表基于 bokeh 渲染,可以输出成独立的 HTML 文件,便于归档与分享;如果要嵌到报告里,也可以在环境里导出静态图片。归档时建议连参数与数据区间一起记下来,否则过几周再看这张图,很难还原当时的假设。具体导出方式与依赖以 bokeh 官方文档为准。

结果再加工

stats 可按 Series 处理,也能取到交易记录等明细用于二次分析(如按年统计、按持仓时长分组)。把结论落到 Excel 或 CSV 之前,先确认字段口径与区间,避免与其他区间混用。

Verify

Backtesting.py 回测有哪些陷阱?验证清单

首次运行只是把链路跑通。要让结论站得住,还需要在下结论前过一遍下面这张清单——它们比「换个更聪明的参数」更能决定你的结论对不对。执行顺序上,先排掉会系统性放大收益的那几项(成交时点、成本假设),再看稳定性类问题(参数邻域、样本长度)。

陷阱表现规避做法注意点
前瞻偏差用当根 bar 的收盘信息决定当根成交明确成交时点假设;默认下一根 bar 成交,若开启按收盘成交要说明理由改这个开关会立刻改变收益,属于结论级变量
参数过拟合参数微小变化收益断崖式下跌看参数邻域是否成片稳定,而不是只看单点高点样本外验证比「再多扫几组」更有价值
成本假设过低把手续费设成 0 或远低于实际按真实费率与滑点假设重跑一遍对比高频信号策略对成本最敏感
样本太短只跑了几个月就下结论至少覆盖一轮完整涨跌,并分段验证交易笔数太少时胜率没有统计意义
单一标的结论外推把一只标的的表现当成规律换标的、换区间交叉验证本项目面向单资产回测,多资产需另做组合层面的评估
只跑一遍不记录参数与数据版本没留档,无法复现记录数据区间、参数、成本假设与代码版本不能复现的结论等于没有结论
把回测读数当预期用历史收益估算未来收益把它当成「策略在历史区间的表现」这一事实描述回测不构成收益保证或投资建议
忽略成交价口径用开盘价、收盘价或中间价成交却未说明在结论里写明成交价假设,并做敏感性对比同一策略换成交口径,收益可能量级不同
合规提示:以上清单用于让研究结论更可靠,不改变「历史表现不等于未来收益」这一前提。任何资金决策请自行评估风险。
Real data run

用真实 A 股数据跑一次,Backtesting.py 的结果长什么样?

把内置 GOOG 换成贵州茅台 600519 的日线(前复权,2018-01-02~2026-09-18),策略与参数一个字不改,结果会完全不同——这就是本站实测的起点。下面三步给出完整可跑代码,每步都写清预期输出与「数字是从哪来的」。

  1. 1. 取数并改成 Backtesting.py 要的列

    用数据接口取日线,再把字段统一成 Open / High / Low / Close / Volume,并把日期列设为时间索引。列名与索引是回测里最容易出错的一环,取完先打印一次确认。

    import akshare as ak, pandas as pd
    
    df = ak.stock_zh_a_daily(symbol="sh600519", start_date="20180101", end_date="20260918", adjust="qfq")
    df["time"] = pd.to_datetime(df["date"])
    df = df.rename(columns={"open": "Open", "high": "High", "low": "Low",
                            "close": "Close", "volume": "Volume"})
    data = df[["time", "Open", "High", "Low", "Close", "Volume"]].set_index("time").astype(float)
    print(len(data), data.index[0].date(), data.index[-1].date())

    预期:打印 2116 2018-01-02 2026-09-18。若行数明显偏少,多数是接口或复权参数与本文不同(本站实测使用新浪接口 adjust="qfq")。

  2. 2. 套用同一个 SmaCross,并声明 A 股成本

    策略代码与官方示例一致;区别只在成本:A 股的佣金与过户费双边收、印花税只在卖出侧收,所以用一个 callable 把它写清楚。

    def ashare_cost(size, price, comm=0.00025, transfer=0.00001, stamp=0.0005):
        notional = abs(size) * price
        fee = notional * comm + notional * transfer      # 佣金 + 过户费:双边
        if size < 0:
            fee += notional * stamp                      # 印花税:仅卖出侧(订单为负代表卖出)
        return fee
    
    bt = Backtest(data, SmaCross, cash=100000, commission=ashare_cost,
                  exclusive_orders=True, finalize_trades=True)
    stats = bt.run()

    预期:run() 正常返回统计结果,交易笔数在十几到上百笔之间(本站实测 123 笔)。若一笔都不成交,先检查数据行数与均线窗口。

  3. 3. 把结果和买入持有放在一起读

    只看策略收益没有意义——同一区间的「买入持有」才是最直接的对照:如果你的规则连它都跑不赢,就要先回答「为什么还要交易」。

    print(stats[["Return [%]", "Buy & Hold Return [%]", "# Trades",
                 "Sharpe Ratio", "Max. Drawdown [%]"]])

    预期:本站实测输出为 Return -69.16%、买入持有 +107.61%、123 笔、Sharpe -0.514、最大回撤 -83.87%。字段名与口径以官方实现为准。

口径(同一套 SmaCross 10/20)策略收益买入持有交易笔数Sharpe最大回撤
内置 GOOG 样例(2004-08-19~2013-03-01,2148 根,commission=0.002)+469.86%+607.37%940.599-34.12%
贵州茅台 600519 前复权 + A 股成本(本站实测)-69.16%+107.61%123-0.514-83.87%
同标的不计任何成本-64.44%+107.61%123-0.448-82.88%
同标的按 20bp 对称佣金-78.22%+107.61%123-0.678-86.21%
同标的 trade_on_close=True(按本根收盘成交)-35.22%+107.61%123-0.188-77.94%
腾讯 00700 前复权 + 港股费用近似-20.91%+1.43%113-0.079-56.85%
三个实测结论:①官方内置样例本身就跑输基准(+469.86% 对 +607.37%),说明「跑通」不等于「跑赢」;②换成真实 A 股后,同一套规则在 8 年长牛股上得到 -69.16%,对买入持有 +107.61%——跑输 176.77 个百分点;③只改成本假设差 13.78pp、只改成交时点差 33.94pp,所以报告里必须写明这两项,否则别人无法判断你的数字是哪一套假设下的。数字只代表该区间该标的,不构成投资建议。
Repro record

Backtesting.py 的回测怎么记账才算可复现?

本站每个实测数字都能用下面这张记录表复现。反过来说:缺任何一项,几周后连你自己也说不清那份结果是在什么假设下跑出来的——而「说不清假设」是回测最常见的失效方式,比代码写错更隐蔽。

记录项本站实测值示例为什么必须记漏记会怎样
Python 版本3.11.9不同版本的依赖与行为差异会影响运行结果复现时结果对不上,却先去怀疑代码
backtesting 版本0.6.6统计字段、默认参数与校验行为随版本变化字段缺失或口径不同,两次实验不可比
pandas / numpy / bokeh2.2.3 / 2.4.6 / 3.3.0索引、年化与绘图行为受这些依赖影响绘图失败或年化口径不一致
数据来源与复权口径新浪接口日线 · 前复权 · 2018-01-02~2026-09-18 · 2116 根换口径会改变基准:实测同标的 +107.61% 与 +70.73%同一策略在不同口径下差几十个百分点
成本与撮合设置佣金 2.5bp 双边 + 过户费 0.1bp 双边 + 卖出印花税 5bp;trade_on_close=False实测仅改成交时点就差 33.94pp无法判断结论属于哪套假设,等于没有结论
参数与输出文件n1=10 / n2=20 / cash=100000;stats、交易明细 CSV、权益曲线 CSV没有输出就没有可比对的依据只能重跑一遍,而重跑未必得到同一结果
# 复现记录模板:建议与结果文件放在同一目录,随结果一起归档
环境  : Python 3.11.9 / backtesting 0.6.6 / pandas 2.2.3 / numpy 2.4.6 / bokeh 3.3.0
数据  : 新浪日线 · 600519 · 前复权 · 2018-01-02~2026-09-18 · 2116 根
成本  : 佣金 2.5bp 双边 + 过户费 0.1bp 双边 + 卖出印花税 5bp(callable)
撮合  : trade_on_close=False, exclusive_orders=True, finalize_trades=True
策略  : SmaCross(n1=10, n2=20), cash=100000
输出  : Return -69.16% / Buy&Hold +107.61% / #Trades 123 / MDD -83.87%
结论  : 该区间跑输买入持有 176.77pp;换成本或成交时点结论量级会变
为什么反复强调「假设」:本次实测里,同一份代码只改两个开关就能得到完全不同的读数——成本(不计成本 -64.44% 对 20bp 佣金 -78.22%,差 13.78pp)与成交时点(默认 -69.16% 对按收盘成交 -35.22%,差 33.94pp)。这些开关不改变策略逻辑,却改变结论的强弱,所以它们必须和结果写在一起。边界:记录模板的作用是让实验可复现,不代表回测结果能预测未来表现;数据字段与统计口径以官方文档和你安装的版本为准。
FAQ

首次运行常见问题

为什么示例用内置数据,而不是自己的行情?

内置示例数据(backtesting.test 里的示例标的)不需要联网、不需要自备文件,能先把「代码链路对不对」和「数据对不对」这两件事分开验证。等示例跑通、字段看懂了,再换成自己的 OHLCV 数据,出问题时也更容易定位。示例数据的具体来源与字段以官方文档为准。等换成自己的数据之后,也建议保留这份示例作为「环境是否正常」的对照基线。

plot() 打不开图表怎么办?

先确认绘图依赖已按官方要求装好(bokeh 版本需满足官方 README 与 PyPI 上的要求),再确认是在能启动浏览器的环境里运行;如果是服务器环境,可改为导出 HTML 文件后本地打开。具体用法与要求以官方文档为准。

stats 里的收益率可信吗?

它是「在这段历史区间、这组成本假设下」的统计结果,可信的部分是链路与口径是否清楚,不可信的部分是把它当成未来收益预期。判断可信度先看三件事:区间多长、成本是否计入、交易笔数够不够;统计项只是把这三件事算成了数,前提仍要你自己确认。结论不构成收益保证或投资建议。

能直接换成自己的股票数据吗?要什么格式?

可以。把示例数据换成自己的 OHLCV 数据即可,核心要求是「列名可识别 + 时间索引有序」:列一般需要 Open/High/Low/Close/Volume,索引为日期且单调递增、无重复。列名规则、是否允许额外列等细节以官方文档为准;换数据后建议先跑一遍数据自检再回测。

跑不出结果或报错,先查什么?

按顺序查四件事:①数据行数与索引是否正常(排序、重复、缺失);②列名与数据类型是否匹配要求(数值列别是字符串);③是否运行在装了包的那个 Python 环境里;④信号与成交时点假设是否写成了「用当根信息做当根决策」。多数首跑「跑不通」都落在这四点里。排查时建议一次只改一个变量,改完立刻对比,否则很难判断是哪一处真正起了作用。

这条路和本机技能路线是什么关系?

两条独立路线:本页是「自己写代码、自己准备数据」的回测框架路线;本机技能路线是用对话方式做行情、指标与风险摘要,适合先快速把问题问清楚。两者没有已证实集成,不能互相替代;选哪条取决于你要的是可复现的策略工程,还是先快速验证想法。能力边界以各自官方说明为准。

本页结论官方样例与真实 A 股都能跑通;600519 区间收益 -69.16% 对买入持有 +107.61%,跑输 176.77pp。
回测局限单一标的单区间,未做参数寻优与样本外验证,也不含滑点与冲击成本。
风险提示回测只反映历史区间的表现,不代表未来收益,也不构成投资建议;换区间、换标的或换成本假设,结论都可能改变。

跑通之后怎么把策略与数据换成自己的?

Backtesting.py 的示例跑通说明环境与链路都没问题;接下来理解策略类的生命周期与参数优化,再把行情数据换成本地可用来源,才能得到针对自己标的的结论。