Blankly / Keyless 与数据读取器
Keyless 模式:把你的价格数据喂进去,六列少一列就报错
Blankly 的离线回测能力靠 blankly.data 下的读取器实现。它们一共五类,覆盖价格、逐笔、事件与资金费率四种输入。这一页的重点不是 API 列表,而是每类读取器的字段硬要求与推断规则——因为校验失败时的报错信息比较简短,很多人会在「数据格式」上反复试错,而问题往往只是列名或时间格式。
blankly/data/data_reader.py)。第五类是 FundingRateEventReader,它需要期货接口在运行期拉取资金费率历史,因此不适合离线使用,详见正文。每一类读取器要求什么、产出什么
Blankly 读取器的选择由你的输入形态决定,不由策略决定。先确定「我手上有什么数据」,再选读取器。
| 读取器 | 接受的输入 | 必需的列 / 字段 | 典型用途与注意点 |
|---|---|---|---|
PriceReader | CSV 文件、JSON 文件、pandas DataFrame,或它们的列表 | 必须包含 open、high、low、close、volume、time 六列;至少 2 行 | 最常用的一类,配合 KeylessExchange 做离线回测;自动按 time 排序 |
TickReader | CSV 文件 | 必须包含 time、price 两列 | 逐笔成交类数据;列要求比 PriceReader 少,但只适合 tick 级推进 |
EventReader | 直接传入 {时间: 数据} 字典 | time、data 两列 | 零散事件用,不需要落盘文件 |
JsonEventReader | .json 文件(扩展名必须是 json) | 文件内容为 {事件类型: {时间: 数据}} 结构 | 把新闻、情绪、自定义信号以 JSON 形式喂进回测;文件不以 json 结尾会直接断言失败 |
FundingRateEventReader | 不接文件,需要符号、起止时间与一个期货接口对象 | 由接口返回的 time 与 rate | 用运行时拉取资金费率;需要一个可用且已授权的期货接口,因此不适合纯离线场景 |
PriceReader 的分辨率不是由你指定的,而是从时间列的差分里取众数推断出来的。这意味着如果你的数据里混了不同频率的段落,或者时间列是字符串,推断结果会和你预期不同,甚至推断为 0 而直接报错。校验规则逐条列出:错哪条会报哪个错?
读取器的校验是断言式的,没有容错。下表把规则、违反后的报错原文与自查方法对应起来。
| 规则 | 违反后的报错 | 自查方法 | 适用读取器 |
|---|---|---|---|
| 列名必须齐全(六列) | AssertionError: Must have at least these columns: ('open', 'high', 'low', 'close', 'volume', 'time') | 用 pandas.read_csv 读一遍,打印列名与要求集合取差集 | PriceReader |
| 行数至少 2 行 | AssertionError: Must give data with at least 2 rows in <文件>. | len(df) > 2 | PriceReader / TickReader |
| 文件扩展名必须匹配类型 | LookupError: Unknown filetype for <路径> | 确认结尾是 .csv 或 .json,不要用 .txt | PriceReader |
| 同一次构造里不能混用 csv 与 json | LookupError: Cannot pass both csv files and json files into a single constructor. | 同一批文件统一格式,或拆成多个读取器 | PriceReader |
| 文件路径数量与标的数量必须一致 | LookupError: Mismatching symbol & file path lengths | 打印两个列表的长度 | PriceReader / TickReader |
| 同一个读取器内标的不能重复 | AssertionError: Cannot use duplicate symbols for one price reader. | 去重 symbol 列表;同标的多段数据改用多个读取器 | PriceReader |
| 分辨率不能推断为 0 | LookupError: Resolution is 0 for <标的>, this is not allowed. | 打印时间列差分并看众数;常见成因是时间用了科学计数法或行数太少 | PriceReader |
| 事件文件必须是 json | AssertionError: The filepath did not have a 'json' ending | 改扩展名 | JsonEventReader |
time 被 pandas 解析成浮点并在写出时用了指数形式,差分就会失真。把时间写成整数 epoch 秒是最稳的做法。怎么把自己的数据接进回测?四步流程
下面用一份日线 CSV 举例,每一步都给出命令、说明与预期结果。如果你手上是分钟线或 tick 数据,只需替换文件与分辨率。
把数据整理成六列并落盘为 csv
命令示例:
df[['time','open','high','low','close','volume']].to_csv('mydata.csv', index=False)。其中time用整数 epoch 秒。预期结果:文件头部一行为time,open,high,low,close,volume,且没有任何单元格为空。构造读取器并确认分辨率被正确推断
命令示例:
from blankly.data import PriceReader; pr = PriceReader('mydata.csv', 'MY-USD'); print(pr.prices_info)。预期结果:打印出的字典里该标的有resolution、start_time、stop_time三个键,且resolution与你的数据频率一致(日线为 86400)。接进 KeylessExchange 并注册策略
命令示例:
ex = blankly.KeylessExchange(price_reader=pr),然后s = blankly.Strategy(ex)并s.add_price_event(fn, symbol='MY-USD', resolution='1d', init=init)。预期结果:没有异常;如果报settings.json缺失,先运行一次blankly init。用数据区间内的绝对时间跑回测
命令示例:
s.backtest(start_date=..., end_date=..., initial_values={'USD': 10000}),其中两个时间取自上一步打印的start_time/stop_time范围。预期结果:输出No cached data found ...与Backtesting...,最后返回结果对象。
prices_info:它是读取器对你数据的「理解结果」。如果分辨率被推断错了,后面所有信号频率都会跟着错,而回测不会报错——只会给出一个看起来正常的错误结论。先看这一步能省下大量排查时间。怎么把新闻、情绪或外部信号接进回测?
这是 Blankly 相对多数轻量回测库更有意思的一点:它把「非价格信息」也当成一等输入。不过要用好它,需要理解事件与价格的时间对齐方式。
| 做法 | 数据形态 | 适用场景 | 注意点 |
|---|---|---|---|
| 字典直接传入 | {时间戳: 数据} | 事件数量少、在代码里就能写出来 | 时间戳与价格数据必须使用同一时间基准 |
| JSON 文件 | {'事件类型': {时间戳: 数据}} | 事件由外部流程产出并落盘 | 文件扩展名必须是 .json,内容按事件类型分组 |
| 在策略里注册事件回调 | 回调签名形如 event(type_, data) | 需要按事件内容做决策 | 与价格回调共用同一份状态容器,注意时序假设 |
| 模拟延迟 | 用类似 self.sleep(秒数) 的调用 | 测试对延迟敏感的策略 | 回测中的 sleep 是模拟时间推进,与真实网络延迟不是一回事 |
| 资金费率事件 | 由期货接口在运行期拉取 | 期货持仓的成本测算 | 需要可用且已授权的期货接口;官方文档说明该能力仍在开发中 |
哪七种数据准备失败最常见?
离线回测失败几乎都发生在数据进入引擎之前。下表按「你看到什么」组织,最后两列给出成因与处置。
| 你看到的现象 | 最可能的成因 | 处置 | 验证是否修好 |
|---|---|---|---|
| 断言失败,提示需要六列 | 列名大小写不符,或缺少 volume 这类被忽略的列 | 统一改成全小写六列;缺少的列可用占位值补齐但要在记录中说明 | 重新读文件并打印列名 |
Must give data with at least 2 rows | 过滤后数据几乎为空 | 检查筛选条件是否把数据全滤掉了 | 打印 len(df) |
Resolution is 0 | 时间列不是整数 epoch(字符串日期或科学计数法) | 转换时间列类型并避免指数写法 | 打印时间列差分的众数 |
No cached or downloaded data available | 请求区间与数据区间不重叠(常见于用了 to='1y') | 改成数据区间内的绝对起止时间 | 能打印出 Backtesting... |
settings.json 找不到 | 工作目录缺少配置文件,与数据格式无关 | 运行 blankly init 或在工作目录放一份最小配置 | 构造交易所不再抛异常 |
| 回测跑完但成交远少于预期 | 数据频率或分辨率被推断错,导致事件触发次数不对 | 核对 prices_info 的分辨率 | 对比事件触发次数与理论预期 |
| 多标的回测里某个标的没有任何数据 | 文件路径与标的列表顺序不匹配 | 按索引逐个核对两个列表 | 打印每个标的的行数与时间范围 |
关于 Keyless 与数据接入的高频问题
回答依据发行版 data_reader.py 的校验逻辑与本机实测;数据结构细节以官方源码为准。
Keyless 模式需要联网吗?
不需要。价格全部来自你传入的文件,回测在本地完成。不过要注意三个前提:工作目录要有 settings.json(否则构造交易所就失败);回测起止时间必须落在你的数据区间内;如果你把 GUI_output 保持为默认的 true,会额外触发绘图流程。
为什么必须包含 volume 列?我的数据里没有成交量。
因为读取器的断言把六列作为一个整体校验,缺失任何一列都会失败。如果你的数据确实没有成交量,可以补一列占位值让校验通过——但这必须在方法论记录里写明,因为任何用到成交量的指标与逻辑都会因此失真。更稳妥的做法是补一列真实可得的量(如成交额换算),而不是填 0。
分辨率被推断错了怎么办?
读取器不提供「指定分辨率」的入参,它的分辨率来自时间列差分的众数。所以修正方式是改数据而不是改参数:确认 time 是等间隔的整数 epoch 秒;如果数据里混了多段不同频率(例如拼接了日线与小时线),就把它们拆成不同文件,用各自的读取器分别加载。
可以喂分钟级或秒级数据吗?
可以。读取器对频率没有硬编码限制,它按差分推断;你要做的是在 add_price_event 时把 resolution 写成与实际频率一致的值(例如 '1m')。频率越高,事件回调被调用的次数越多,回测耗时也越长——这是事件驱动引擎的固有代价。
JSON 和 CSV 该用哪个?
价格数据用 CSV 更省事(列名直观、便于目视检查)。事件类数据用 JSON 更自然,因为 JsonEventReader 的结构是按事件类型分组的,且它本身要求文件扩展名为 json。注意同一个 PriceReader 不能混用 csv 与 json。
自己准备的数据能用来证明策略有效吗?
能不能得出策略结论,取决于数据之外的三件事:是否有基准对照、是否计入了手续费与滑点、是否做了样本外检验。数据本身规范并不等于结论可信——这三件事都在回测引擎审计页里逐条展开。