日线:最常见也最稳
日线 OHLCV 是绝大多数中低频策略的默认选择:数据易得、噪声相对小,成交价假设的争议也最少。缺点是信号滞后一天,对短线策略的刻画能力有限。
Data · OHLCV 接入
Backtesting.py 对数据的要求很小:一张以时间为索引、含 Open / High / Low / Close(Volume 可选)列的 pandas DataFrame,就能驱动一次回测。真正花时间的不是「怎么喂数据」,而是把数据源口径、列名大小写、时间对齐与复权口径统一——这些做错不会报错,只会让结论悄悄失真。本页按「格式要求 → 数据源对照 → 分步接入 → 检查清单 → 质量排查」整理。
数据格式是硬要求:列名大小写、索引类型、是否单序列,任何一项不符都会在运行时报错或静默出错。下表把每一项的作用与「不满足时会怎样」放在一起看。
| 项 | 是否必需 | 作用 | 不满足时的现象 | 注意点 |
|---|---|---|---|---|
| Open | 必需 | 开盘价,撮合与成交价假设的基准之一 | 运行时报列缺失类错误 | 大小写敏感,需与官方要求完全一致 |
| High | 必需 | 区间内的价格高点,用于止损止盈与波动度量 | 同上;部分策略逻辑失效 | 必须 ≥ Low,需自查高低价关系 |
| Low | 必需 | 区间内的价格低点 | 同上 | 同上 |
| Close | 必需 | 收盘价,多数指标与信号的输入 | 指标算不出或全为空值 | 指标基于收盘价计算时尤其关键 |
| Volume | 可选 | 成交量,量能类策略与指标需要 | 依赖量能的策略无法运行或全为空 | 没有量能数据时省略即可,不要填 0 冒充 |
| DatetimeIndex | 必需 | 时间轴,决定 bar 顺序与逐 bar 迭代 | 列名对但顺序错,结果失真且不报错 | 须为时间类型且单调递增 |
| 单条价格序列 | 必需 | 一次回测对应一个标的的价格序列 | 多标的宽表会被当成一列多值处理 | 多标的需分别回测后自行汇总 |
| 自定义附加列 | 可选 | 预先把外部指标算好挂进 DataFrame | 不影响运行,但会增加数据维护成本 | 列名不要与 OHLC 冲突 |
import pandas as pd # 目标形态:index = DatetimeIndex,列 = Open/High/Low/Close[/Volume] df = pd.DataFrame(raw, columns=['Open', 'High', 'Low', 'Close', 'Volume']) df.index = pd.to_datetime(df.index) df = df.sort_index()
Backtesting.py 只内置一份示例数据用于跑通流程;真实研究的数据要靠你自己准备。下表按「取数方式 → 覆盖范围 → 适用场景 → 限制与注意点」对照常见来源。
| 取数方式 | 覆盖范围 | 适用场景 | 限制与注意点 |
|---|---|---|---|
| 内置示例数据 | 官方随包提供的一只美股示例 | 跑通流程、验证代码路径 | 只适合试跑,不能用于任何绩效结论 |
| 本地 CSV / Parquet + pandas | 取决于你的数据文件 | 离线复现、归档、内网环境 | 需自行整理列名与时间索引,并记录数据来源与采集日期 |
| tushare 等 A 股接口 | A 股行情与部分基本面 | 国内 A 股日线级研究 | 通常需要 Token 与积分,字段名与限频以数据源官方文档为准 |
| akshare 等开源接口 | A 股、基金、期货、指数等多类数据 | 个人研究、快速取数 | 接口稳定性与字段随上游变化,取数后必须做质量自检 |
| yfinance 等海外接口 | 美股等海外市场 | 海外标的研究 | 境内网络可达性不稳定,字段与复权口径以数据源文档为准 |
| 券商 / 终端导出 | 与你账户权限一致的市场 | 已付费数据的复现研究 | 导出格式各异,注意时间字段与时区 |
| 合成数据(随机游走等) | 无真实市场含义 | 调试指标与信号逻辑 | 不能用于任何绩效结论,仅验证代码是否正确 |
把数据交给 Backtesting.py 的五步按顺序做,每步给出真实代码、一句话说明与可验证的预期结果。以一张本地 CSV 为例,换成接口取数时只需替换取数那一步。
先用 pandas 把数据读进来看看长什么样:字段名、时间列位置、是否有代码列。先观察再改名,能省掉后面反复对不上的时间。
import pandas as pd
raw = pd.read_csv('600519.csv')
print(raw.head())
print(raw.columns.tolist())预期输出:前几行数据与列名清单。若时间列是字符串、或混着股票代码列,下一步统一处理。
把列名重命名为 Open/High/Low/Close(Volume 可选),并把时间列设为 DatetimeIndex——这一步决定了后面所有指标与信号能不能算。
df = raw.rename(columns={'日期': 'index', '开盘': 'Open', '高点': 'High',
'低点': 'Low', '收盘': 'Close', '成交量': 'Volume'})
df['index'] = pd.to_datetime(df['index'])
df = df.set_index('index').sort_index()
print(df.dtypes)预期输出:各列为数值类型、索引为时间类型。若价格为字符串,需要先转数值再继续。
在把数据交给任何回测之前先跑一遍检查:缺失值、重复时间、价格是否为 0 或负、时间是否单调。数据问题越早发现,返工成本越低。
print(df.index.is_monotonic_increasing, df.index.duplicated().sum()) print(df[['Open', 'High', 'Low', 'Close']].isna().sum().to_dict()) print(df.index.min(), df.index.max())
预期输出:True / 0 / 各列缺失数全为 0 / 起止日期覆盖你要回测的区间。任一项不符先回到上一步修数据。
这个库一次回测对应一条价格序列:多标的要先拆开分别回测,不要塞进一张宽表。同时确认区间足够长,能覆盖不同市场环境。
price = df['Close'] print(len(price), price.index[0], price.index[-1])
预期输出:样本条数与首尾日期。样本过少时,任何指标结论都不可信(区间选择标准以你自己的研究设计为准)。
把 DataFrame 按位置传给回测对象(它是回测的必要输入),跑一次最小回测确认数据能被正确读取,再进入策略与参数环节。
from backtesting import Backtest from backtesting.test import SMA bt = Backtest(df, MyStrategy, cash=10_000, commission=.002) stats = bt.run() print(stats['# Trades'])
预期输出:打印出成交笔数(具体数值随数据与策略变化)。若成交数为 0,先查列名、时间顺序与信号条件,而不是先怀疑策略逻辑。
下面这些检查都是 pandas 通用写法,不需要额外依赖;建议固化成脚本,每次换数据源都跑一次。表里的「预期结果」是基本要求,不是充分条件。
| 检查项 | 怎么查 | 预期结果 | 不合格怎么处理 |
|---|---|---|---|
| 列名是否齐全且大小写正确 | df.columns.tolist() | 含 Open/High/Low/Close(Volume 视情况) | 用 rename 统一列名,不要用大小写不一致的列 |
| 索引是否为时间类型 | df.index.dtype | datetime64 类时间类型 | 用 pd.to_datetime() 转换后再设为索引 |
| 时间是否单调递增 | df.index.is_monotonic_increasing | True | sort_index() 重新排序;乱序会让回测结果失真 |
| 是否有重复时间点 | df.index.duplicated().sum() | 0 | 定位重复行后去重,并确认数据源是否分多次拼接 |
| 关键列是否有缺失值 | df[['Open','High','Low','Close']].isna().sum() | 全为 0 | 先确认缺失成因(停牌/未收盘/接口缺失),再决定删除或补齐 |
| 价格是否为 0 或负值 | (df[['Open','High','Low','Close']] <= 0).sum() | 全为 0 | 多为复权或字段错位导致,需回数据源核对口径 |
| 高低价关系是否成立 | (df['High'] < df['Low']).sum() | 0 | 列错位或数据源异常,需回源核对,不能靠数值变换掩盖 |
| 时间区间是否覆盖回测区间 | df.index.min(), df.index.max() | 覆盖你设定的起止日期 | 补齐区间或缩小回测区间,避免「样本外其实是空」 |
| 成交量量级是否合理 | df['Volume'].describe()(若有) | 量级与标的规模相符,无异常 0 值 | 量能缺失时宁可不做量能类策略,也不要填 0 |
Backtesting.py 里数据问题最麻烦的地方在于「不报错但结论错」。下表按「现象 → 成因 → 处理方式 → 注意点」整理,处理顺序建议是先对齐时间轴、再处理缺失、最后才计算指标与信号。
| 现象 | 成因 | 处理方式 | 注意点 |
|---|---|---|---|
| 回测区间内出现大段空白 | 停牌或数据源本身缺失交易日 | 确认是真实停牌还是漏数据,再决定保留缺口或剔除区间 | 不要用「向前填充价格」掩盖停牌,会伪造出可成交的假象 |
| 指标跳空、收益率异常 | 复权口径不一致(不复权/前复权/后复权混用) | 同一回测内统一口径,并在记录里写明用的是哪种 | 复权算法与字段名以数据源官方文档为准 |
| 分红送转日价格断崖 | 未复权导致价格跳空 | 改用复权数据,或明确接受不复权口径并在结论里说明 | 口径不同会让收益率与回撤都不可比 |
| 把多个标的塞进一张表后结果混乱 | Backtesting.py 一次回测对应单资产价格序列 | 拆成多份单资产数据,分别回测后自行汇总 | 多资产组合能力不在这个库的能力范围内 |
| 策略在历史上表现异常好 | 用了当时不可能知道的信息(未来数据) | 检查信号是否用到当日收盘后才知道的数据,必要时整体后移一根 bar | 这是最容易被忽略、也最伤结论的一类错误 |
| 依赖量能的策略没有信号 | Volume 列缺失或全为 0 | 补齐量能数据,或换不依赖量能的策略 | Volume 本就可选,缺它就别用需要它的指标 |
| 时间对不上、交易日错位 | 时区或时间戳格式不一致 | 统一到同一时区与同一时间粒度后再对齐 | 跨市场数据尤其容易踩这一条 |
| 样本区间太短 | 只用了一小段行情 | 拉长区间覆盖不同市场环境,或明确标注结论适用范围 | 短样本下的参数结论通常不可复述 |
Backtesting.py 以逐 bar 迭代运行,bar 的粒度完全由你喂进来的数据频率决定——也就是说,能不能做日内回测,取决于你手上有没有分钟级数据。
日线 OHLCV 是绝大多数中低频策略的默认选择:数据易得、噪声相对小,成交价假设的争议也最少。缺点是信号滞后一天,对短线策略的刻画能力有限。
把分钟级 OHLCV 喂进来就能做日内回测,但成交时点、滑点与撮合假设的重要性会急剧上升;同一份数据换一个成交价假设,结论可能完全不同,需谨慎验证。
在策略里同时用日线与更长周期指标是可行的,但不同频率的数据对齐、重采样与「哪一根 bar 才可见」都需要你自己处理,Backtesting.py 不会替你完成频率转换。
把真实数据接进 Backtesting.py 时,下面这些现象出现频率较高。按「现象 → 可能原因 → 处理 → 提前规避」四列整理,报错原文以官方与解释器实际提示为准。
| 现象 | 可能原因 | 处理 | 提前规避 |
|---|---|---|---|
| 报列缺失类错误 | 列名大小写不一致或缺列 | 按官方要求统一为 Open/High/Low/Close | 取数后立刻打印列名清单核对 |
| 指标全是空值、没有成交 | 窗口长度超过数据长度或关键列有缺失 | 检查窗口与数据长度、补齐缺失值 | 先跑一次最小回测确认数据可用 |
| 结果明显好得不真实 | 前视偏差、区间过短或成本未计入 | 检查信号可见性、拉长区间、补上手续费与滑点 | 把成本假设固定写进回测参数并留档 |
| 换一段数据结论就翻转 | 参数是在同一段历史上挑出来的 | 做样本内外的切分再复核 | 数据准备好当天就规划好样本切分方式 |
| 图表打不开或显示异常 | 可视化依赖缺失或版本不匹配 | 按官方安装说明补齐依赖后重试 | 先把依赖装全再开始研究,避免中途卡住 |
| 同一份数据两次结果不同 | 数据文件被更新、或时间顺序被改动 | 固定数据快照并记录采集日期 | 首次取数成功后就落盘保存,回测只读本地文件 |
Backtesting.py 只认 Open / High / Low / Close(Volume 可选)四个列名加一个时间索引;国内数据源返回的字段名各不相同,这一步没做好就是「回测一开跑就报错」的最常见来源。
| 取数方式 | 实测返回字段 | 需要转换成的列 | 注意点 |
|---|---|---|---|
| 新浪 A 股日线 | date / open / high / low / close / volume / amount | Open / High / Low / Close / Volume + 时间索引 | 字段为小写,需改大小写并去掉 amount 等多余列 |
| 新浪港股日线 | date / open / high / low / close / volume / amount | 同上 | 不传区间会返回全历史,需自行截断 |
| 东财日线接口 | 中文列名(日期 / 开盘 / 收盘 / 最高 / 最低 / 成交量 …) | 同上 | 本次实测连续 4 次连接失败,可用性本身是变量 |
| 本地 CSV | 自定义列名与分隔符 | 四列必须齐全,Volume 可缺 | 索引必须是时间类型且单调递增,字符串日期要先解析 |
| 中文列名 CSV / Excel | 日期 / 开盘价 / 收盘价 / 最高价 / 最低价 / 成交量 | 按字典批量改名 | 列名带空格或全角字符会导致改名失败 |
| DataFrame 直接构造 | 自行生成的 OHLC | 列名大小写必须与库一致 | 列名写成 close(小写)会被判为缺少必要列 |
import pandas as pd
df = pd.read_csv('600519.csv') # 或 akshare 返回的 DataFrame
df['time'] = pd.to_datetime(df['日期']) # 先解析时间
df = df.rename(columns={'开盘': 'Open', '最高': 'High', '最低': 'Low',
'收盘': 'Close', '成交量': 'Volume'})
df = (df[['time', 'Open', 'High', 'Low', 'Close', 'Volume']]
.dropna()
.sort_values('time')
.drop_duplicates(subset='time') # 去重,避免同一天两行
.set_index('time')
.astype(float))
assert df.index.is_monotonic_increasing, '时间索引必须单调递增'
assert {'Open', 'High', 'Low', 'Close'} <= set(df.columns), '缺少必要的 OHLC 列'
Backtest(df, SmaCross, cash=100000, commission=ashare_cost).run()
数据问题不会以「数据错了」的形式出现,而是以「策略在某天做了奇怪的事」的形式出现。下面六项检查跑一次只要几秒,能挡掉大部分低级陷阱。
| 体检项 | 判据 | 本站实测结果 | 不合格时怎么办 |
|---|---|---|---|
| 重复日期 | 索引中同一天只出现一次 | 600519 / 00700 / 002594 / 601127 全部为 0 | 保留后一条或先核对来源,不要直接平均 |
| 索引单调递增 | 排序后不回退 | 各数据集均通过 | 排序后重建索引,并检查是否有跨市场拼接 |
| 零价与负价 | 开高低收全部大于 0 | 各数据集均为 0 行 | 定位来源,非交易时段或错误填充要剔除 |
| 价格越界 | 收盘价落在当日高低区间内 | 600519 为 0;00700 不复权序列出现 12 行越界 | 换接口复核,或改用可校验的复权口径 |
| 交易日历缺日 | 与标的所在市场日历一致 | 600519 缺 0 日;00700 相对 A 股日历缺 63 日(港股休市不同);601127 缺 4 日 | 确认是休市还是停牌,别用错市场的日历校验 |
| 接口可用性 | 同一标的有可用替代接口 | 东财接口本次 4 次重试全部失败,新浪接口可用 | 换接口重取,并记录你实际用的数据源与日期 |
def health_check(df, calendar=None, name=''):
out = {}
out['rows'] = len(df)
out['dup_dates'] = int(df.index.duplicated().sum())
out['monotonic'] = bool(df.index.is_monotonic_increasing)
out['nan_rows'] = int(df.isna().any(axis=1).sum())
out['bad_price'] = int((df[['Open', 'High', 'Low', 'Close']] <= 0).any(axis=1).sum())
out['close_out_of_range'] = int(((df.Close > df.High) | (df.Close < df.Low)).sum())
if calendar is not None:
lo, hi = df.index[0], df.index[-1]
out['missing_days'] = len(set(calendar[(calendar >= lo) & (calendar <= hi)]) - set(df.index))
return out
是,这个库按 Open/High/Low/Close(Volume 可选)这样的大小写识别列;写成小写或中文列名会导致运行时报列缺失。具体列名要求以官方 README 与文档为准,其余自定义列可以保留但要避免与 OHLC 冲突。
可以,Volume 是可选的:不依赖量能的指标与策略完全可以不带这一列。但要注意两点——不要用 0 填充冒充「无成交量」,也不要省略后仍去用需要量能的指标,否则指标会算不出来或全为空值。
可以,思路统一:接口取回数据后,把列名改成 Open/High/Low/Close(量能按需保留)、把时间列转成 DatetimeIndex 并排序,就能传进回测。但各接口的字段名、复权口径、是否需要 Token 与限频规则都不一样,一律以该数据源官方文档为准;国内接口的可用性与稳定性也需你自己评估。
不行。Backtesting.py 一次回测对应一条价格序列,不支持多资产组合,也不支持分数股——把多只股票塞进一张宽表,会被当成结构异常的数据处理,结果不可信。正确做法是每个标的单独回测,再自行汇总组合层面的结论。
能,bar 的粒度由数据频率决定,喂分钟级数据就按分钟推进。但日内回测对成交时点、滑点与撮合假设极其敏感:同一份数据换一个成交价假设可能得出相反结论。建议先用日线验证策略逻辑,再考虑是否需要下降到日内,且结论以你自己的成本假设为准。
可以省掉一部分取数工作:本机已核验的技能里有覆盖财经行情数据与技术指标分析的方向,一句话即可发起取数与基础分析;但拿到的数据仍需你自己做字段与口径校验才能喂进回测,两者是相互独立的两条路线,没有已证实集成关系。是否值得取决于你想把时间花在数据工程还是策略本身。