Backtesting.py / A 股适配

Backtesting.py 适配 A 股与港股:T+1、涨跌停、复权与交易成本怎么处理

Backtesting.py 是一个通用的 OHLCV 回测引擎,它本身不含 A 股交易制度:T+1、涨跌停、整手、印花税与复权口径都要由使用者声明或近似建模。本文把「哪些能原生支持、哪些能用代码模拟、哪些只能近似」逐条分清,并给出 6 只 A 股与 1 只港股在同一区间、同一策略下的本站实测数字。

核验方式:本站实测(backtesting 0.6.6)数据:日线 · 前复权为主 · 2018-01-02~2026-09-18实测日期:2026-09-24
T+1 与成交时点撮合顺序决定跨日
涨跌停与停牌库不判可成交性
费用与税费非对称成本靠 callable
复权口径改成交价与基准
按 0.6.6 源码与本站实测整理的 A 股/港股适配因素;非官方架构图,具体行为以官方文档与你安装的版本为准。
测试版本backtesting 0.6.6(PyPI 发布版)Python 版本本站实测 3.11.9;官方要求以 README / PyPI 为准最后核验2026-09-29(实验数据 2026-09-24 运行)数据来源新浪日线前复权(6 只 A 股 + 港股 00700),2018-01-02~2026-09-18适用市场A 股 · 港股可直接复现是(规则模拟写法与自检清单可直接复现)含交易成本是(佣金 2.5bp 双边 + 过户费 0.1bp 双边 + 卖出印花税 5bp)样本外验证部分(做了成本与复权口径对照,未做参数寻优)
6 只 A 股 + 1 只港股同一区间同一策略实测
0 / 6 跑赢买入持有双均线基准对照
成本假设差 13.8pp-64.44% → -78.22%
执行时点差 33.94pp默认 vs trade_on_close
Rule map

A 股规则里,Backtesting.py 原生支持哪些、要自己补哪些?

先分清三档再看细节:库原生支持的是「下单、成交、统计」这一层;交易制度要你用代码约束;数据口径它完全不介入。下表逐条对应本文后面的实测章节。

A 股/港股规则Backtesting.py 的默认行为你要做什么属于哪一档
T+1(当日买入不可当日卖出)先处理上一根 bar 的挂单再调用 next(),成交天然跨日默认设置即可,无需额外防护原生支持(由撮合顺序决定)
成交时点(当日收盘 / 次日开盘)默认按下一根 bar 开盘成交;trade_on_close=True 时按本根收盘成交明确声明并在报告里记录你用的是哪一种原生支持
涨跌停不可成交不判断涨跌停,一律按开盘价(或收盘价)成交自行过滤不可成交日或把订单延后只能近似
停牌没有停牌概念,只按你给的数据行推进在数据层处理缺日与零成交只能近似
整手(100 股)支持整数股,但不校验是否为一手整数倍自己按 100 股取整后再下单可代码模拟
佣金 / 印花税 / 过户费单一 commission 在买卖各收一次;也可传 callable 自定义用 callable 建模「卖出才收印花税」的非对称费用原生支持(结构上有取舍)
成交量约束与冲击成本没有成交量模型,订单数量不受当日成交量限制自建约束或接受偏差并写明无法精确模拟
复权口径不关心复权,按你传入的价格计算损益自己选定口径并声明「复权价不等于可成交价」只能近似(口径问题)
多资产组合与共享资金单资产引擎;MultiBacktest 是每个标的各跑一套独立账户组合级资金分配需换支持组合的框架不支持
怎么用这张表:可直接照用的那几项先照用;需要你写代码的那几项,写在策略里的约束能不能被回测如实反映,取决于你有没有把约束写在「下单之前」;第三档意味着无论怎么写都只是近似,必须在结论里声明。把三档混在一起谈,是「回测能跑」与「回测可信」之间的落差。
T+1

Backtesting.py 怎么处理 A 股 T+1 与成交时点?

T+1 不是靠参数开关实现的,而是撮合顺序的自然结果:订单在 next() 里创建,要等下一根 bar 才处理。真正需要你决定的是「按哪个价格成交」——它比多数参数调优更能左右结论。

撮合设置区间收益(贵州茅台 600519)交易笔数当日买当日卖首笔成交
默认:订单在下一根 bar 开盘成交-69.16%1230 笔2018-01-31 买入
trade_on_close=True:本根 bar 收盘成交-35.22%1230 笔2018-01-30 买入
刻意「同根 bar 买后立刻平」的策略(默认设置)见下代码10570 笔最短持有 1 天
同上策略,trade_on_close=True见下代码10570 笔最短持有 1 天
默认撮合(内置 GOOG 样例 2004-08-19~2013-03-01)+469.86%940 笔—
两套设置合计(SmaCross 123×2 + 刻意往返策略 1057×2)—23600 笔最短持有均为 1 天
class SameBar(Strategy):
    """故意在每根 bar 上「买完就平」,用来验证会不会出现当日往返"""
    def init(self):
        pass
    def next(self):
        if not self.position:
            self.buy(size=.9)     # 本根 bar 挂单
        else:
            self.position.close() # 下一根 bar 才处理

stats = Backtest(data, SameBar, cash=100000, trade_on_close=True).run()
t = stats['_trades']
print((t.EntryTime.dt.date == t.ExitTime.dt.date).sum())   # 实测输出:0
print((t.ExitTime - t.EntryTime).dt.days.min())            # 实测输出:1
三点实证结论:①1057 笔刻意同 bar 往返的交易里,当日买当日卖 = 0 笔,最短持有 1 天——A 股 T+1 无需额外防护;②反过来看,T+0 日内回转策略无法用它如实模拟,因为最短持有被撮合顺序固定为 1 根 bar;③只改成交时点,同一策略收益从 -69.16% 变成 -35.22%,差 33.94 个百分点——写结论时必须写明用的是哪一种,否则读者无法判断你的数字是哪一套假设下的。
Orders

Backtesting.py 遇到涨跌停与停牌怎么成交?实测给出答案

库不判断「这一天能不能成交」:涨跌停日照样按开盘价撮合,停牌则因为你给的数据里没有那一行而被跳过。下面先看 6 只 A 股样本里的涨跌停分布与真实成交情况。

标的涨停日跌停日一字板日策略在涨停日买入成交策略在跌停日卖出成交
贵州茅台 60051901100
平安银行 00000131000
招商银行 60003610000
比亚迪 002594135000
宁德时代 300750234700
赛力斯 6011278519341
实测样例(赛力斯,涨停日买入成交):2020-02-27 当日 +10.06%,成交价 9.63 元 = 当日开盘价;2020-06-04 当日 +9.98%,成交价 10.08 元;2021-02-19 当日 +9.91%,成交价 14.34 元。6 只样本合计 125 个涨停日、30 个跌停日、11 个一字板日,策略有 4 笔买在涨停日、1 笔卖在跌停日。
边界(不夸大):本次样本里没有一笔成交落在一字板日(0 笔),所以不能说「实测导致了不可能成交的成交」。能确定的是:机制上库不会阻止这种成交,一字板日是否需要过滤要靠你自己加。若你的策略信号密集出现在小盘股或题材股上,这个偏差会明显放大——请在报告里写明是否处理了不可成交日。
# 把「不可成交日」的订单延后一天:用 tag 记录待执行意图
class LimitAware(Strategy):
    def init(self):
        self.bars_since_limit = 0
    def next(self):
        prev_pct = self.data.Close[-2] / self.data.Close[-3] - 1 if len(self.data) > 2 else 0
        limit_up = prev_pct >= 0.098    # 主板 10%;创业板/科创板 20%;ST 5%,需按板块取值
        if limit_up:
            self.bars_since_limit = 1      # 当日不可成交:跳过本次下单
            return
        ...  # 正常的信号与下单逻辑
Costs

Backtesting.py 里 A 股交易成本怎么建模才不离谱?

佣金、过户费与印花税并不是同一件事:前两者双边收,印花税只在卖出侧收。用单一对称 commission 近似,会系统性低估卖出侧成本。下面的 callable 把三者分开建模。

def ashare_cost(order_size, price, comm=0.00025, transfer=0.00001, stamp=0.0005):
    """order_size 为负代表卖出(空头),这也是判断印花税侧的依据"""
    notional = abs(order_size) * price
    fee = notional * comm + notional * transfer   # 佣金 + 过户费:双边
    if order_size < 0:
        fee += notional * stamp                   # 印花税:仅卖出侧
    return fee

bt = Backtest(data, SmaCross, cash=100000, commission=ashare_cost)
佣金卖出印花税区间收益费用合计Sharpe最大回撤
00-64.44%0-0.448-82.88%
05bp-67.00%5243-0.482-83.37%
2.5bp0-66.97%5171-0.482-83.36%
2.5bp(A 股常用)5bp(卖出)-69.20%10119-0.515-83.86%
5bp0-69.14%10043-0.514-83.85%
5bp5bp-70.64%14875-0.536-84.30%
10bp0-72.22%19229-0.562-84.63%
10bp5bp-73.63%23468-0.587-84.96%
20bp0-78.22%34545-0.678-86.21%
20bp5bp-79.46%37907-0.707-86.48%
读表要点:同一策略、同一标的,只改成本假设,收益从 -64.44% 掉到 -79.46%(差 15.02 个百分点),期间买入持有是 +107.61%——加不加手续费不改变「跑输」这个结论,但会改变你以为策略离盈亏平衡有多远。另外两点实测提醒:①把最低佣金(如 5 元)写进 callable,实测收益只差 0.06pp(-69.16% → -69.10%),因为库会把佣金摊进成交价,小额订单的下单数量会随之被调整;②真实费率随时间、券商与板块变化,本页数字是「近似建模」而非精确复现,以券商与交易所公告为准。
Adjust

前复权数据能直接喂给 Backtesting.py 做回测吗?

可以拿来算信号,但不能拿来说「这就是我能成交的价格」:前复权把历史价格按累计分红送股向下调整,成交价、股数、现金占用与基准都会随之变化。同一策略换口径,结论能差几十个百分点。

复权口径区间收益买入持有基准交易笔数首日收盘价首笔成交价
前复权(qfq)-69.16%+107.61%123578.82607.72(2018-01-31)
不复权(raw)-71.90%+70.73%125703.85739.00(2018-01-31)
前复权 · 零成本对照-64.44%+107.61%123578.82607.72(2018-01-31)
00700 前复权 · 零成本+5.03%+1.43%113365.98—
00700 前复权 · 计入港股费用-20.91%+1.43%113365.98—
前复权 / 不复权 累计调整幅度(600519)前复权价 ÷ 不复权价 在样本内介于 0.8224 ~ 1.0000 之间,即八年内最大向下调整约 17.8%
口径算法含义适合用来做什么不适合做什么 / 注意点
前复权 qfq以最新价为基准向前调整历史价看长期走势、算均线与回撤历史价被下调,不能当成当时的真实成交价;分红越多偏差越大
后复权 hfq以上市首日为基准向后调整做跨期累计收益对比价格量级会失真,看图与计算仓位时容易误判
不复权 raw原样保留当日成交价核对真实成交价与税费、做成本核算除权除息日会出现价格跳空,均线类指标会被污染
复权因子自算用复权因子自行换算价格需要精确控制口径的严谨研究要自己校验因子来源与拼接方式,出错不易发现
港股复权同样受分红与拆股影响港股长周期回测与 A 股除权除息节奏不同,跨市场拼接不可直接相加
指数 / ETF 复权指数点位本身不含分红,价格指数与全收益指数不同做基准对比时明确用的是哪一种用价格指数当基准会低估「买入持有」,从而高估策略
实测要点:换口径后,前 3 笔交易的日期完全一致(2018-01-31 / 03-05 / 03-16),但成交价与股数不同:前复权 607.72 / 607.05 / 614.80(164 股),不复权 739.00 / 738.18 / 747.60(135 股)。也就是说复权不改变信号,却改变现金占用、费用与「买入持有」这条基准线(+70.73% vs +107.61%,相差 36.88 个百分点)。口径必须写进报告,否则别人无法复现你的结论。
Lots

Backtesting.py 的下单数量有哪些硬约束?

A 股按「手」下单,而库只保证「整数股」。这两者之间的差距要由你自己的代码补上,下面是从 0.6.6 源码核验到的三条硬规则与对应写法。

规则源码/实测表现正确写法写错的后果
股数必须是整数buy()/sell() 断言 round(size) == size;传 size=100.5 实测抛 AssertionError下单前用 int() 取整直接报错中断回测
不校验一手(100 股)库接受 1 股的订单,不会自动取整到 100size = int(shares // 100 * 100)回测买了现实中买不到的零股
比例下单的取整size 在 0~1 时按可用权益与含费价格向下取整,取整到 0 时订单被取消并给出告警资金边界处留余量,或改用绝对股数小额账户频繁出现「下单被取消」
未平仓交易是否计入统计finalize_trades 默认 False;改为 True 会在最后一根 bar 平掉未平仓交易按你的报告口径显式设置同一策略出现两个收益(实测 -68.99%/122 笔 与 -69.16%/123 笔)
做空方向sell() 的订单数为负,库支持空头A 股策略自行约束不做空回测出现现实中无法执行的空头持仓
每手股数(100 股)库只校验「整数股」,不校验是否为一手整数倍下单前 size = int(shares // 100 * 100)出现零股成交,小资金偏差被放大
class Lots(Strategy):
    def next(self):
        if not self.position and crossover(self.ma1, self.ma2):
            lots = int(self.equity * 0.95 // (self.data.Close[-1] * 100))   # 想买几手
            if lots >= 1:
                self.buy(size=lots * 100)   # 传绝对股数:必须是 100 的整数倍
为什么值得单独写一节:零股、取整方向、以及「比例下单会被取消」这三件事,都会让你的回测在资金边界附近与实际账户产生系统性差异;差异不是随机噪声,而是随资金规模变化的偏差——小资金账户受影响最大。
Data

Backtesting.py 的 A 股与港股数据源有哪些字段和日历坑?

回测的所有结论都建立在数据上,而 A 股数据的坑集中在三处:接口字段名不同、交易日历与港股不一致、以及偶发的数据源异常。下面是本站实测的数据体检结果与字段映射。

数据集(实测)根数重复日期收盘价越界(超出当日高低)相对 A 股日历缺日备注
600519 前复权2116000通过全部体检项
600519 不复权2116000通过全部体检项
00700 前复权21440063缺日 = 港股休市与 A 股不同
00700 不复权214401263数据源异常,使用前需处理
002594 前复权2116000通过全部体检项
601127 前复权2112004缺日需与停牌核对
取数方式返回字段(实测)需要转换成的列注意点
新浪 A 股日线date / open / high / low / close / volume / amountOpen / High / Low / Close / Volume + 时间索引小写字段名,需改大小写
新浪港股日线date / open / high / low / close / volume / amount同上不传区间会返回全历史,需自行截断
东财日线中文列名(日期/开盘/收盘/最高/最低/成交量…)同上本次实测 4 次重试均连接失败,可用性本身是变量
本地 CSV自定义必须含开高低收,Volume 可选索引必须是时间类型且单调递增,否则回测报错
指数 / ETF 日线日期 + 开高低收(+ 成交量)同上,Volume 可省略或置 0指数无「成交量」语义,别把成交额当成交量
Tushare 日线(本站未实测)—按官方字段字典映射为 OHLCV本站未实测该接口,字段与复权参数以 Tushare 官方文档为准
三个必须记住的坑:①港股与 A 股的休市日不同——用 A 股日历去校验港股,会得到 63 个「缺日」的假告警;②不同复权口径下的同一标的,字段与量级都可能不一致(实测不复权港股有 12 行收盘价落在当日高低之外);③接口可用性会波动(同一天里东财接口连续 4 次失败、新浪可用),所以「数据获取失败」时先换接口试,而不是先怀疑策略代码。
Checklist

Backtesting.py 做 A 股回测要检查哪些规则?适配自检清单

下面 8 项按「先数据、后规则、再成本」的顺序排列,每一项都给出判据与不合格的典型表现;这份清单同时适用于你自己写的策略和网上下载的示例代码。

检查项怎么查不合格的典型表现
数据列与索引确认存在开高低收四列、索引为时间类型且单调递增回测一开跑就报列名或索引相关异常
复权口径确认用的是前复权/后复权/不复权中的哪一种,并写进记录均线在除权日出现跳空,回撤计算失真
交易日历用与标的匹配的市场日历核对缺日用 A 股日历校验港股,误报大量缺日
T+1 与成交时点确认 trade_on_close 取值,并检查是否存在同 bar 往返假设策略收益对时点设置高度敏感而不自知
涨跌停与停牌统计样本内涨停/跌停/一字板天数,确认成交是否落在这些日子回测在不可能成交的价格上有成交记录
成本模型确认卖出印花税是否单独建模、费率是否为当前口径费用被低估,策略「接近盈亏平衡」的假象
整手与取整检查下单数量是否按 100 股取整出现零股成交,小资金账户偏差明显
结论声明口径、区间、成本、基准是否都写在结论旁边别人换个假设就跑出完全相反的结论
FAQ

A 股适配常见问题

Backtesting.py 支持 A 股吗?

更准确的说法是:它提供通用 OHLCV 回测引擎,A 股交易规则需要由使用者自行扩展或近似建模。实测中,T+1 由撮合顺序天然满足,但涨跌停、停牌、整手、印花税等要你自己处理;具体能力边界以官方 README 与你安装的版本源码为准。

需要为 A 股写一个自定义券商类吗?

多数情况下不需要。成本可以用 commission 的 callable 形式建模,股数与整手可以在下单前处理,成交限制可以通过延后下单近似。只有在需要更细的撮合细节(逐笔、盘口、部分成交)时,才需要考虑自己实现订单处理逻辑,而那通常意味着要换框架或自建撮合层。

印花税只在卖出时收,单一 commission 参数怎么办?

用 callable:commission=lambda order_size, price: ...,订单数量为负时代表卖出方向,在那里加上印花税即可。要注意库会把佣金摊进成交价,因此带最低佣金的模型会轻微改变下单数量——实测影响在 0.1 个百分点以内,但值得知道。

回测里的「买入持有」基准可以信吗?

要看口径:统计里的买入持有是从「所有指标预热结束后的首根 bar」起算的,不是从数据首根开始。实测中把均线窗口换成 200 日后,同一区间的买入持有基准从 117.19% 变成 187.65%(差 70.46 个百分点)。所以报基准时要写清楚起点,具体实现以官方源码为准。

为什么加了涨跌停过滤后收益变化不大?

因为大多数标的的涨停日很少,而均线类策略的信号日期是分散的。实测中 6 只样本合计 125 个涨停日,只有 4 笔买入落在涨停日。但如果你做的是小盘股或题材股短线策略,命中率会明显上升——判断依据是「先统计你自己的策略有多少笔成交落在不可成交日」,而不是凭感觉。

港股和 A 股能用同一套代码吗?

代码可以复用,但成本模型、交易日历与复权口径要分别处理:港股没有印花税的卖出单边结构(费率与征收方式不同),无涨跌停,休市日与 A 股不一致。实测中腾讯 00700 双均线在零成本下略跑赢买入持有(+5.03% vs +1.43%),计入费用后变成 -20.91%。

这些实测数字能直接用在我的标的上吗?

不能。本文所有数字来自 2018-01-02~2026-09-18 的 6 只 A 股与 1 只港股、单一策略与单一成本假设,只说明方法差异有多大,不代表任何标的的未来表现,也不构成投资建议。要得到你自己的结论,请用同一套可复现的记录方式重跑。

本页结论A 股规则只能近似建模:T+1 与涨跌停能用代码模拟,整手与印花税只能近似,流动性无法精确建模。
回测局限「不可成交」用当日 OHLC 阈值近似判定,一字板与部分成交无法还原;港股交易规则差异未逐条覆盖。
风险提示回测只反映历史区间的表现,不代表未来收益,也不构成投资建议;换区间、换标的或换成本假设,结论都可能改变。

下一步:怎么判断 A 股回测结果可不可信?

适配完 A 股规则,还需要审计结论本身:未来函数、过拟合、成本敏感性与基准口径——这四件事决定了你的回测数字值不值得信。