译文说明
- 原文链接:docs/data.md
- 原作者:Rob Carver
- 对应版本:master
- 译者:fanrong
- 许可:GPL-3.0(见 GPL-3.0.txt)
注:本文为非官方翻译,可能存在疏漏;请以原文为准。
本文档专门讨论如何存储和处理期货数据。
相关文档:
全文分为四部分。第一部分期货数据工作流给出“从零开始准备期货数据”的整体流程:如何获取数据、如何存储、以及如何生成可用于回测和作为实盘初始状态的数据。读完这一部分,你也会对 pysystemtrade 中使用的数据类型有一个整体感知。其余部分会逐步深入细节:在第二部分中,我会说明各类数据对象之间是如何拼在一起的;第三部分会详细介绍每一种期货数据组件、以及它们在存储层的表示;在第四部分中,你会看到数据存储对象与回测 / 生产代码之间的接口设计。
Table of Contents
- 目录
- Part 1: A futures data workflow(期货数据工作流)
- A note on data storage(关于数据存储的说明)
- Instrument configuration and spread costs(品种配置与点差成本)
- Roll parameter configuration(换月参数配置)
- Getting historical data for individual futures contracts(获取单一期货合约的历史数据)
- Roll calendars(换月日历)
- Creating and storing multiple prices(创建与存储 multiple prices)
- Creating and storing back adjusted prices(创建与存储回溯调整价)
- Getting and storing FX data(获取与存储外汇数据)
- Updating the data(更新数据)
- Finished!(完成!)
- Part 2: Overview of futures data in pysystemtrade(pysystemtrade 中期货数据的总体结构)
- Part 3: Storing and representing futures data(期货数据的存储与表示)
- Futures data objects and their generic data storage objects(期货数据对象与其通用数据存储对象)
- Instruments(品种)
- Contract dates and expiries(合约日期与到期)
- Roll cycles(换月周期)
- Roll parameters(换月参数)
- Contract date with roll parameters(带换月参数的合约日期)
- Futures contracts(期货合约)
- Prices for individual futures contracts(单一期货合约价格)
- Final prices for individual futures contracts(最终期货合约价格)
- Named futures contract dicts(具名期货合约字典)
- Roll calendar data object(换月日历数据对象)
- Multiple prices(多价格序列)
- Adjusted prices(回溯调整价)
- Spot FX data(即期外汇数据)
- Data storage objects for specific sources(特定数据源的数据存储对象)
- Creating your own data storage objects for a new source(为新数据源创建数据存储对象)
- Futures data objects and their generic data storage objects(期货数据对象与其通用数据存储对象)
- Part 4: Interfaces(接口层)
Part 1: A futures data workflow
本部分给出“从零开始准备期货数据”的完整流程。建议按照顺序阅读并动手操作一遍,即便你暂时只打算跑回测,也非常值得照做。
总体目标是:
- 为每个期货品种(instrument)准备合约配置与成本;
- 获取单个期货合约级别的历史价格数据;
- 生成换月日历(roll calendar);
- 基于换月日历和合约历史价格生成 multiple prices(包含当前、下一合约以及 carry 合约的价格序列);
- 再由 multiple prices 生成单一的回溯调整价(back adjusted prices);
- 同时准备好外汇(FX)价格数据。
A note on data storage
在 pysystemtrade 中,同一类数据通常可以存放在不同后端,比如:
- CSV 文件;
- MongoDB;
- Parquet。
我个人推荐的模式是:
- 回测数据:可以使用 CSV 或 Parquet;
- 生产数据:合约级别历史价格等大量数据使用 MongoDB + Parquet;
- 一些需要手动修改、检查的配置(例如换月日历)适合放在 CSV。
在整套工作流的绝大多数阶段,你都可以自由选择“把结果存成 CSV”还是“写入数据库”,甚至两者都用。换月日历是一个特例:它们只以 CSV 形式存在,这是刻意的设计,因为:
- 换月日历偶尔需要人工“打补丁”;
- 创建/校验换月日历是一次性工作,只在系统从零初始化时用到。
Note on outdated shipped CSV data
仓库自带了一些 CSV 数据,用于快速跑起示例或小规模回测:
- multiple prices;
- adjusted prices;
- 一部分 FX 数据等。
这些数据在你克隆仓库时很可能已经“过期”了:
- 历史覆盖到某个时间点就停止;
- 对于 mini 合约等,有些历史价格是用主合约推算出来的;
- 之后新的合约、价格不会自动更新。
因此你在生产或严肃回测中应该假定:
- 仓库自带 CSV 只适合作为“初始样本”或示例;
- 你需要自己从 IB 或其它数据源下载最新数据,并按本文档流程导入 MongoDB / Parquet。
Instrument configuration and spread costs
在处理任何数据之前,你需要先配置“品种层”的信息,包括:
- 合约乘数(contract size)、最小跳动点(tick size);
- 交易时间;
- 所属资产类别等;
- 交易成本(主要是点差 spread)。
这些信息通常存放在:
data/futures/csvconfig/instrumentconfig.csvdata/futures/csvconfig/spreadcosts.csv
你可以按书中的示例或自身交易品种填写这些文件。
在回测和生产中:
instrumentconfig.csv提供静态品种配置;spreadcosts.csv提供买卖价差成本,用于成本建模。
如果你只做回测,且仅使用仓库自带的数据,可以暂时沿用默认配置;如果要在真实账户上交易,一定要认真校对这些参数。
Roll parameter configuration
要生成换月日历,需要先指定一些“换月参数”(roll parameters),例如:
- 每个月的哪一天“名义到期”(approximate expiry);
- 相对于到期日提前多少天开始换月;
- carry 合约应该选前一到期还是后一到期等。
这些参数存放在:
data/futures/csvconfig/rollconfig.csv
对每个品种,会有若干字段,其中比较关键的有:
‘RollOffsetDays’
RollOffsetDays 表示距离名义到期日提前多少天进行换月。例如:
- 若某合约的名义到期日是每月 20 号;
RollOffsetDays = 10;- 那么目标换月日期就是当月 10 号左右。
这个参数主要用于生成“理想的”换月日期,后面还会基于实际价格做进一步校正。
‘ExpiryOffset’
ExpiryOffset 表示“名义到期日”相对于合约实际到期日的偏移。
比如:
- 合约真实到期日是当月第 3 个星期三;
- 你可以在
rollconfig.csv中用“该月某日 + ExpiryOffset”这类近似方式描述; - 在生成初始换月日历时,用这个近似日期作为基准。
这些值只是为了构造“粗略正确”的换月日历,在真实交易中你最好从交易所或券商获得精确到期日。
‘CarryOffset’
CarryOffset 决定 carry 使用哪个到期月份:
CarryOffset = -1:使用前一到期月份作为 carry 合约(通常更合理);CarryOffset = +1:使用后一到期月份(当你只持有最前月合约时,有时候别无选择)。
需要注意:
- carry 的计算基于“定价用的 roll cycle”(priced roll cycle),而不仅仅是“持有用的 roll cycle(hold cycle)”;
- 例如“冬季原油”中,持有周期只有
Z(12 月),但定价用的 roll cycle 是全年月份FGHJKMNQUVXZ; - 若
CarryOffset = -1,在定价 roll cycle 中,12 月 (Z) 的前一个月份是 11 月 (X),因此 carry 使用 11 月合约。
更详细的背景可参考我第一本书《Systematic Trading》的附录 B。
Getting historical data for individual futures contracts
接下来,我们关注“单一期货合约”的历史价格数据。
这一步在以下情形中是必需的:
- 要运行生产代码(实盘);
- 或者需要比仓库随附 CSV 更新得多的历史数据。
如果你只想用仓库自带的 CSV 做回测,而且只是把 CSV 导入数据库、对数据新鲜度没有要求,那么可以跳到后面的 multiple prices 部分(见 Writing multiple prices from CSV to database)。
Getting data from the broker (Interactive brokers)
你可以使用脚本 /sysinit/futures/seed_price_data_from_IB.py 从 Interactive Brokers 批量获取尽可能多的历史数据:
- 包含已到期合约在内;
- 通常至少能获取最近一年的日度数据(具体取决于 IB 限制)。
这些数据会被写入 MongoDB / Parquet,后续步骤会直接使用。
Getting data from an external data source (Barchart)
从 IB 能拿到的历史数据长度有限,如果你需要更长的历史,就需要其它来源。
这里以 Barchart 为例(实际上可以替换成任何能导出 CSV 的数据源):
- 你可以使用 Barchart 网站的下载功能,或者使用 API;
- 本文以“下载 CSV 文件,然后用脚本读取”的方式为主。
(注意:不要同时从 Barchart 和 IB 获取同一合约的历史价格,否则可能互相覆盖:
- 如果先拉 IB 数据,再跑 Barchart 相关脚本,后者会覆盖前者;
- 如果先导入 Barchart 数据,再导 IB,则 IB 数据不会被写入。)
获取到的数据原则上可以存放在任意地方,但我推荐使用 Parquet:
- 对 pandas Series / DataFrame 的读写很方便;
- 磁盘占用和速度都比较适合时间序列数据。
出于许可和体积原因,我无法直接把 Barchart 之类的数据“打包上传”到 GitHub:
- 大量原始数据存 GitHub 并不合适(无论 CSV 还是 Mongo dump);
- 版权和使用条款也不允许我直接公开分发这些数据;
- 因此你必须自己拉取、存储和维护这些数据。
一个简单的批量下载方式是:
- 注册 Barchart Premier 账户(每天最多 250 次下载);
- 本仓已在
private/tooling/barchart/bcutils/内置了 bc-utils 的必要代码,配好.env中的BARCHART_USERNAME/BARCHART_PASSWORD后可直接运行相关私有脚本批量下载(不再要求同级../bc-utils仓库)。
如果你愿意“用时间换脚本”,也可以手工从 Barchart 网站的历史数据页面逐个下载,例如 Cotton #2 的页面示例。
然后使用脚本 /sysinit/futures/barchart_futures_contract_prices.py,该脚本内部调用 /sysinit/futures/contract_prices_from_csv_to_db.py:
- 虽然默认写死了 Barchart 的 CSV 格式;
- 但你可以基于它做适当修改,以适配其它类似 CSV 源。
脚本需要你指定一个目录,里面存放所有 Barchart 下载的 CSV 文件。它会做两件事:
- 把文件重命名为 pysystemtrade 预期的命名规则;
- 读取 Barchart 文件中的数据,并写入 MongoDB / Parquet。
Barchart 通过网站下载的 CSV 文件命名格式类似:
XXMYY_Barchart_Interactive_Chart*.csv
其中:
XX是两位 Barchart 品种代码,例如ZW表示小麦;M是合约月份字母(F=一月, G=二月, ..., Z=十二月);YY是两位年份;- 后面部分是无关紧要的后缀。
脚本中的小函数 strip_file_names 会把这些文件重命名为内部期望的格式:
NNNN_YYYYMMDD.csv
其中:
NNNN是我自己的品种代码(至少 4 个字母,通常更长);YYYYMM00是数值日期格式,例如20201200(后两位代表“日”,但永远用 00 占位,除非未来要交易周度到期品种)。
接下来,我们要定义 Barchart CSV 的内部格式,通过设置 barchart_csv_config:
| |
可以看到:
- Barchart 文件开头有一行可以跳过(标题行前的一行),结尾有一行 footer 也需要忽略;
- 第二行才是真正的列名,其中
Date Time列包含时间索引; input_column_mapping用来把我偏好的列名(大写)映射到文件中的列名。
另一个可选参数是 input_date_format,默认值为 %Y-%m-%d %H:%M:%S。
调整这些选项后,你基本可以读取 99% 的第三方 CSV 数据源;实在特殊的,就需要自己写 parser 了。
真正的读取和写入逻辑在这里:
| |
对象 csvFuturesContractPriceData 和 db_prices 是两端的数据“管道”(data pipeline):
- 前者从 CSV 读取合约价格;
- 后者把价格写入数据库;
- 两者有一套相同的方法接口,并都继承自更通用的
futuresContractPriceData。
这样我们就可以写与“存储后端”无关的处理逻辑。后文会看到更多类似的模式。
Roll calendars
现在我们准备开始创建换月日历(roll calendar)。
换月日历是一系列日期,每个日期上我们会从当前合约切换到下一合约。
如果不熟悉期货换月,可以先读一下我的博客文章:Rolling futures contracts。需要注意,文中有些细节针对的是我已经废弃的旧交易系统,并不完全等同于 pysystemtrade 的实现,但思路是一致的。
你可以在这里看到 DAX 期货的一个换月日历示例:/data/futures/roll_calendars_csv/DAX.csv。
在每一条记录中,我们会看到:
current_contract:当前持有的合约;next_contract:下一步要持有的合约(换月目标);carry_contract:用于计算 carry 的合约。
我们使用当前合约与 carry 合约之间的价差,来为 carry 交易规则生成预测值。
关键点是:每一个换月日,我们必须同时拥有 price 合约和 forward 合约的价格(carry 合约可以没有)。
下面是另一个换月日历片段的示例,这个品种:
- 按季度(IMM 日期 HMUZ)换月;
- 交易的是第一个合约;
- carry 使用第二个合约。
| |
- 在 2 月 28 日之前,我们交易的是 202003,carry 使用 202006;
- 2 月 28 日换入 202006,在 2 月 28 日到 6 月 1 日之间,我们交易 202006,carry 使用 202009;
- 6 月 1 日换入 202009,在 6 月 1 日到 8 月 31 日之间,我们交易 202009,carry 使用 202012;
- 8 月 31 日换入 202012;之后我们交易 202012(carry 合约虽未在示例中列出,但显然会使用 202103)。
生成换月日历有三种方式:
- 基于你已有的单合约价格数据生成(见下面小节);
- 基于现有的 multiple prices CSV 推断(见Roll calendars from existing ‘multiple prices’ CSV files);
- 直接使用仓库内已经提供好的换月日历(见
/data/futures/roll_calendars_csv)。
注意:
- 仓库内的 multiple prices 和 roll calendar 通常不会完全跟上最新市场数据;
- 如果你要在实际交易中使用,最好还是用第 1 种方式,从最新的单合约价格重新生成。
换月日历总是以 CSV 文件形式存储:
- 便于阅读、手工编辑和版本控制;
- 初始构建或发现问题时容易直接“开文件改一改”;
- 在系统真正跑起来之后,它们只在初始化阶段用到一次。
一旦有了换月日历,我们还可以根据实际的合约价格对它进行“对齐调整”:
- 比如你假设可以在到期前 10 天换月,但某一年那天是感恩节,市场休市,没有价格;
- 这时逻辑会自动找到“最近的可交易日”(向前或向后),把换月日期调整到那里;
- 如果找不到任何日期同时有当前合约和下一合约的价格,则会报错,需要你补数据或调整参数。
Generate a roll calendar from actual futures prices
这是从零开始、只有单合约价格时生成换月日历的方法。
相关脚本是 /sysinit/futures/rollcalendars_from_db_prices_to_csv.py,你需要调用其中的函数 build_and_write_roll_calendar。
该脚本一次只处理一个品种——生成换月日历是一件需要“精心制作”的事,并不适合全自动批量处理。
对每个品种,脚本做的事情大致是:
从数据库中获取单个期货合约的价格(上一小节已经完成);
从 CSV 读取我们之前配置好的换月参数(见 Roll parameter configuration);
根据 roll 参数中的
ExpiryOffset、RollOffsetDays等,计算一个初始的换月日历:1 2roll_calendar = rollCalendar.create_from_prices(dict_of_futures_contract_prices, roll_parameters)对生成的换月日历做一系列检查(单调性、有效性等,若有问题会给出 warning);
如果对结果满意,就把换月日历写入 CSV 文件。
强烈建议在调用脚本时指定一个输出目录(output_datapath),放在某个“临时数据目录”中,而不是覆盖仓库自带的换月日历(/data/futures/roll_calendars_csv)。
当然,真要覆盖也不致命,git pull 一下可以恢复,只是少了一个与“官方版本”对比的机会。
Calculate the roll calendar
真正生成换月日历的逻辑在 /sysobjects/roll_calendars.py,主要调用 /sysinit/futures/build_roll_calendars.py 中的代码。
核心部分是:
| |
流程可以理解为:
- 根据换月参数生成“理想换月日历”(
approx_calendar),即在每个理论目标日期上做一次换月; - 检查每个换月日期当天是否同时存在“当前合约”和“下一合约”的价格(我们需要二者都在场,才能计算回溯调整价);
- 若理想日期当天没有“匹配价格”(matching prices),则在时间上前后搜索最近的有匹配价格的日期,并将换月日历调整到该日期(
adjust_to_price_series)。
如果在整个时间轴上都找不到匹配价格,则该品种的换月日历生成会报错。
此时你有几种选择:
- 修改 roll 参数(例如使用后一合约,而不是前一合约);
- 获取更多的单合约历史价格(让相邻合约价格区间有所重叠);
- 在极端情况下,手工“伪造”一些价格,让两个合约在某些日期有重叠(严格来说这是作弊,因为你不知道真实的 roll spread,但有时候为了不浪费其他数据,只能这么做)。
Checks
生成换月日历后,我们需要对其做两类检查:
| |
单调性检查(monotonic):换月日期在索引上必须是严格递增的。
在数据比较“脏”的情况下,有可能出现非单调情况,此时没有自动修复的方法,只能回头清洗/重建数据(这也是换月日历以 CSV 存储的原因之一,以便你可以人为干预)。有效性检查(valid dates):每个换月日期上必须同时存在当前合约和下一合约的价格。
由于我们在生成日历时已经用价格序列进行了对齐,这一步理论上不应该再失败;如果失败,说明在生成阶段就已经抛出异常了。
Manually editing roll calendars
换月日历存放在 CSV 文件中(例如前面提到的 DAX 示例:/data/futures/roll_calendars_csv/DAX.csv)。
你当然可以把它们写入 MongoDB 或 Parquet,但我更喜欢把它们保留在 CSV:
- 方便手工修改(特别是当数据源更新频率不高时);
- 换月日历只在系统初始化时用到一次,平时不会反复读写,因此 CSV 十分够用;
- 你可以随时打开 CSV,检查日期是否单调、是否有效。
辅助检查函数是 check_saved_roll_calendar,只要确保用的是正确的 datapath 即可。
Roll calendars from existing ‘multiple prices’ CSV files
在下一节,我们会用“换月日历 + 单合约价格”一起构造 multiple prices(当前/forward/carry 三条价格序列及对应合约 ID)。
反过来,如果你已经有 multiple prices,也可以从中“倒推出”换月日历。
当然,要先有 multiple prices,就必须曾经有过一个有效的换月日历——是不是鸡生蛋蛋生鸡?
幸运的是,pysystemtrade 仓库里已经带了一批已经生成好的 multiple prices(见 /data/futures/multiple_prices_csv),这些数据就是我根据自己的旧系统生成的。
你可以运行脚本 /sysinit/futures/rollcalendars_from_providedcsv_prices.py:
- 默认会遍历 multiple prices 目录下所有已有品种;
- 对每个品种,生成一个对应的换月日历,并写入一个指定的临时目录。
缺点是:这些 multiple prices 数据不会持续保持最新。例如:
- 你按季度换月(HMUZ),持有周期也是 HMUZ;
- 如果仓库数据最后更新在 6 个月前,那么过去半年中可能已经经历了一次甚至两次换月;
- 这些换月就不会出现在生成出的日历里。
因此你需要手工在 CSV 中补充这些“遗漏的换月行”。
理论上可以写代码自动推断这些补行逻辑,但目前还没有,也欢迎你贡献这部分代码。
Roll calendars shipped in CSV files
第三种方式是“最省事”的:直接使用仓库内已经提供好的换月日历 CSV:
- 位置在
/data/futures/roll_calendars_csv; - 这些文件是我基于 multiple prices 推导出来的。
它们仍然有“可能略微落后于最新数据”的问题,但作为起点或样例非常有用:
- 你可以直接用它们来构造 multiple prices;
- 或者在此基础上做少量手工修改以反映最近的合约。
Creating and storing multiple prices
下一步,我们要创建并存储 multiple prices:
- 对每个日期,给出当前合约价格、下一合约价格、carry 合约价格;
- 同时给出对应的合约标识(contract IDs)。
multiple prices 在两个地方会直接使用:
- 作为生成回溯调整价(back adjusted prices)的输入;
- 在回测中,carry 交易规则直接使用 multiple prices(无需先合成成单一价格序列)。
创建 multiple prices 需要:
- 换月日历;
- 单合约历史价格。
你可以在 /data/futures/multiple_prices_csv/AEX.csv 中看到一个 CSV 示例,内部 DataFrame 的结构基本类似。
Creating multiple prices from contract prices
相关脚本为:/sysinit/futures/multipleprices_from_db_prices_and_csv_calendars_to_db.py。
脚本的大致逻辑是:
- 从
csv_roll_data_path读取换月日历(默认路径为/data/futures/roll_calendars_csv;如果你遵照前面的建议把日历放在临时目录,请相应修改); - 获取每个合约的收盘价(在 multiple / adjusted price 阶段,我们只需要收盘价,不需要完整 OHLC);
- 可选但推荐:用合约收盘价对换月日历进行“对齐调整”,确保换月日期都能找到匹配价格。
- 如果你是通过“从单合约价格生成日历”的方式得到的日历,这一步可以省略(已经对齐过);
- 如果是方法 2 或 3,或者你手工改动过日历文件,则强烈建议执行这一步。
- 在未来一周位置添加一个“phantom roll”(虚拟换月点),否则数据不会延伸到“今天”——第一次跑生产更新后这个问题会自动消失,但有些人不喜欢看到“数据只到几天前”。
- 按换月日历,把各期合约的数据“缝合”成 multiple prices。
- 按脚本参数决定:
- 是否把 multiple prices 写入
csv_multiple_data_path(默认为/data/futures/multiple_prices_csv); - 是否写入数据库(MongoDB / Parquet)。
- 是否把 multiple prices 写入
我个人习惯同时写 CSV 和 DB:
- DB 用于生产与日常回测;
- CSV 作为备份,同时有时也更方便做一次性分析。
在步骤 5 中,如果数据不太“干净”,可能会给出 warning 甚至直接 error,这通常意味着:
- 换月日历存在问题(冗余、缺失、非单调等);
- 或者个别合约价格缺失。
经验上,99.9% 的问题都出在“之前忽略了换月日历的某些 warning”;
因此一旦 multiple prices 生成失败,先回去把换月日历认真验证一遍:确保已经验证、单调、并且与真实价格对齐。
Writing multiple prices from CSV to database
本小节的用途是:
- 你接受“仓库自带的 multiple prices 虽然过期,但能用来做回测”的设定;
- 你又希望把这些数据搬到数据库(而不是直接从 CSV 回测);
- 你不打算去收集单合约价格,也不想自己生成换月日历。
这种情况下,你可以直接使用脚本 /sysinit/futures/multiple_and_adjusted_from_csv_to_db.py,该脚本会:
- 从 CSV(默认是“shipping 目录”
/data/futures/multiple_prices_csv)中读取 multiple prices 和 adjusted prices; - 直接拷入数据库。
完成这一过程后,你可以跳过 multiple prices 生成步骤,直接进入 FX 数据部分(见 Getting and storing FX data)。
Updating shipped multiple prices
如果你已经有一个 IB 账户,可能会希望把仓库自带的 multiple prices 更新到“更接近现在”的时间点,然后再生成 back adjusted prices。
相关脚本示例为:
整体思路是:
- 先更新“需要采样的合约集合”和它们的历史价格(见生产文档中的:
- Update sampled contracts daily
- Update futures contract historical price data daily 同时可以进行一次人工检查,留意历史价格中的 spike(参见 manual check)。
- 用更新后的单合约价格为目标品种构造一份新的 roll calendar;
- 使用这份 roll calendar 和新的合约价格生成 multiple prices;
- 把新 multiple prices 与仓库原有 multiple prices 做“拼接”(splicing),得到一条更长的数据;
- 将拼接后的 multiple prices 写回数据库。
需要留意一个细节:
仓库中的某些 multiple prices(尤其是 mini 合约的历史价格由主合约推算而来时),其 carry offset 和 rollconfig.csv 中的设置不完全一致。如果你在策略中使用 carry 交易规则,最好检查这些设定是否一致(可以暂时修改 rollconfig.csv 进行对比)。
下面是伪代码式的示例流程:
先创建一些临时工作目录,然后从数据库价格构建 roll calendar:
| |
然后用我们更新后的价格 + 新生成的 roll calendar 来计算 multiple prices:
| |
接着,把新生成的数据拼接到仓库原有的数据后面(并检查价格合约与 forward 合约的连续性):
| |
最后,将拼接后的 multiple prices 写入 CSV,并利用已有脚本写回数据库:
| |
Creating and storing back adjusted prices
一旦 multiple prices 准备好,我们就可以构造“回溯调整价”(back adjusted price series):
- 给定一个 multiple prices DataFrame;
- 使用某种“拼接方法”(stitching method)把不同合约对应的价格序列拼接为一条连续的合成价格;
- 存储在数据库/CSV 中,以供回测和生产使用。
相关脚本为:/sysinit/futures/adjustedprices_from_db_multiple_to_db.py:
- 从数据库中读取 multiple prices;
- 完成 back-adjustment;
- 再把 adjusted prices 写回数据库;
- 如有需要,也可以同时写到 CSV(用于备份或在回测中直接读取)。
Changing the stitching method
默认的拼接方法是“Panama stitching”。
如果你不喜欢 Panama,可以:
- 修改
futuresAdjustedPrices.stitch_multiple_prices的实现; - 或者写你自己的拼接函数,并在回测/生产配置中调用它。
更多关于调整方法的细节,可在本文档后续部分以及回测文档中找到。
Getting and storing FX data
严格意义上,FX 数据并不是期货价格,但除非你:
- 账户货币就是 USD;
- 所有期货品种都是美元计价;
否则你需要即期 FX 价格才能正确进行资金换算和仓位规模控制。
pysystemtrade 仓库中附带了一些 FX CSV 数据,但你大概率希望更新它们:
- 在实盘中,我们会通过 IB API 更新 FX 数据;
- 为了获得较长历史,这里选用免费的数据网站 investing.com 作为示例。
使用方式(示例):
- 去 investing.com 注册账号;
- 下载你需要的 FX 历史(例如
GBPUSD等); - 把下载的 CSV 文件放到一个“只包含 FX 文件”的目录中,文件名格式形如
GBPUSD.csv。
要查看仓库中现有的 FX CSV 已经覆盖多少历史,可以运行:
| |
然后使用脚本 /sysinit/futures/spotfx_from_csvAndInvestingDotCom_to_db.py:
- 该脚本会读入指定目录中的 CSV;
- 视参数设置,将数据写入数据库和/或新的 CSV 文件;
- 你可以调整脚本中的目录路径、列名映射与格式,来兼容其它数据源。
如果只想把随附 CSV 直接复制到数据库(不补充 investing.com 等额外历史),可以把脚本参数设为:
ADD_EXTRA_DATA = FalseADD_TO_CSV = True
此时脚本会做一个简单的“CSV → DB”拷贝:
数据会相对过期,但在生产环境中,会通过 IB 自动增量更新(前提是随附数据距离当前时间不超过一年——因为 IB 只提供约一年的日度历史)。
Updating the data
如果你希望数据能够持续更新(用于实盘或滚动回测),需要配置以下流程:
在 pysystemtrade 的生产环境中,如果你已经设置了调度(见 Scheduling),并启用了 run_daily_price_updates 脚本,这些步骤会每天自动运行一次。
即便如此,建议你在完成初始搭建后,手动按顺序跑一次上述三个步骤,尤其是在你刚刚为新的交易品种添加数据时。
Finished!
到这里,你已经完成了:
- 品种与合约层配置;
- 单合约历史价格;
- 换月日历;
- multiple prices;
- 回溯调整价;
- FX 数据。
也就是说,你已经具备了:
- 启动实盘交易所需的全部价格与配置数据;
- 或者,使用数据库而非 CSV 来跑回测的所有基础数据。
接下来几部分会从实现角度详细说明这些数据对象在 pysystemtrade 中是如何组织与存储的。
Part 2: Overview of futures data in pysystemtrade
本部分介绍 pysystemtrade 中“期货数据”的整体架构和心智模型。
总体范式是:
- 对每一种数据类型(例如品种配置、单合约价格、multiple prices、adjusted prices、FX 等),都有一个“数据对象”(data object);
- 每个数据对象对应一个或多个“数据存储对象”(data storage object),负责从某个后端读取/写入该类数据;
- 每个存储后端(CSV、MongoDB、Parquet、IB 等)会实现一套具体的 data storage class,并继承同一个“通用数据存储基类”;
- 在上层,我们通过
dataBlob和接口对象来屏蔽底层存储细节。
Hierarchy of data storage and access objects
简化后的层级关系如下:
数据对象(data object)
例如:futuresInstrument:描述一个期货品种(合约乘数、tick size 等静态信息);futuresContract:具体到某个合约月份的标识;futuresContractFinalPrices:单个合约的最终日度价格;futuresMultiplePrices:multiple prices;futuresAdjustedPrices:回溯调整价;spotFxPrices:即期 FX 价格; 等。
通用数据存储对象(generic data storage object)
例如:futuresInstrumentDatafuturesContractPriceDatafuturesMultiplePricesDatafuturesAdjustedPricesDatafxPricesData等。
具体后端实现(source-specific storage objects)
对于每种数据类型,可以有多个“存储后端”实现,例如:csvFuturesInstrumentData:用 CSV 存储品种配置;mongoFuturesContractPriceData:用 MongoDB 存储单合约价格;parquetFuturesMultiplePricesData:用 Parquet 存储 multiple prices;csvFxPricesData/parquetFxPricesData等。
dataBlob
dataBlob封装了一组 data storage 对象,并为上层提供统一访问接口;- 它负责创建各后端数据对象实例,并把它们挂在诸如
db_futures_multiple_prices、db_fx_prices这样的属性上; - 通过 dataBlob,可以在不关心“数据存在哪儿”的情况下使用统一 API。
回测接口(simData)与生产接口(/sysproduction/data 下的各类接口)
- 回测时,
simData对象持有一个dataBlob,并提供面向回测的高层方法(例如“获取某品种的 adjusted prices”); - 实盘时,
sysproduction/data下的各个接口类也持有dataBlob,并在其基础上添加业务逻辑与安全检查。
- 回测时,
文中偶尔提到 arctic,那是项目早期使用的时间序列存储方案(Arctic);现在已经被 Parquet 取代,只在文档中保留了一些历史说明。更多可见 backtesting 文档。
Directory structure (not the whole package! Just related to data objects, storage and interfaces)
这里只列出与“数据对象 / 存储 / 接口”相关的目录结构,便于你快速导航:
- /sysbrokers/IB/:IB 数据存储 / 访问对象
- /syscontrol/:进程控制数据对象
- /sysdata/:通用数据存储对象与 dataBlob
- /sysdata/futures/:期货数据存储对象(回测与生产),包括执行和日志
- /sysdata/production/:仅用于生产的数据存储对象
- /sysdata/fx/:即期 FX 数据存储对象
- /sysdata/mongodb/:MongoDB 数据存储对象
- /sysdata/parquet/:Parquet 数据存储对象
- /sysdata/arctic/:Arctic 数据存储对象(已弃用)
- /sysdata/csv/:CSV 数据存储对象
- /sysdata/sim/:回测接口层
- /sysexecution/:订单与订单栈(order stack)数据对象
- /sysobjects/:大多数通用数据对象定义(回测与生产共用)
- /sysproduction/data/:生产环境接口层
掌握这些目录结构后,你可以更容易地找到需要修改或扩展的代码位置。
Part 3: Storing and representing futures data
本部分详细说明各类“期货相关数据对象”及其对应的数据存储实现。
Futures data objects and their generic data storage objects
这一节逐类说明期货相关数据对象,以及它们对应的“通用数据存储类”。
Instruments
数据对象:
futuresInstrument,定义一个期货品种的静态信息:- 合约乘数、tick 大小;
- 交易时间;
- 品种名称、代码等。
数据存储对象:
futuresInstrumentData(通用基类);- 具体实现例如:
csvFuturesInstrumentData:在 CSV 中读取instrumentconfig.csv;- 未来也可以扩展
mongoFuturesInstrumentData等。
Contract dates and expiries
数据对象:
- 用来存储“合约日期”和“到期日”等信息的对象。
数据存储对象:
- 负责维护合约月份列表、名义到期日、以及用于生成换月日历所需的辅助信息。
这些信息通常来自:
- 配置文件;
- 交易所规范;
- 或者你手工维护的一组规则。
Roll cycles
数据对象:
- 定义一个品种的“换月周期”(roll cycle),即一年中哪些月份会有交易合约;
- 例如:
- 一些品种只交易
HMUZ(季度合约); - 有些品种每月都有合约。
- 一些品种只交易
数据存储对象:
- 存储并提供对 roll cycle 的查询。
Roll parameters
数据对象:
rollParameters,包含本章前面提到的:RollOffsetDaysExpiryOffsetCarryOffset- 等等。
数据存储对象:
rollParametersData及其后端实现;- 从
rollconfig.csv或其它来源读取这些参数。
Contract date with roll parameters
有时候我们需要把“合约日期信息”和“换月参数”一起使用,例如在构造换月日历时。
数据对象:
- 组合了
contractDates与rollParameters的混合对象(可视为一个方便的封装)。
- 组合了
数据存储对象:
- 提供统一方法:既能拿到“某品种有哪些合约/月份”,又能同时拿到对应的 roll 参数。
Futures contracts
数据对象:
futuresContract,表示“某个品种在某个合约月份”的一个具体合约;- 通常由
(instrument_code, yyyymm00)唯一标识。
数据存储对象:
- 辅助生成、解析和查询这些合约标识;
- 例如基于 roll cycle 生成接下来若干需要采样的合约列表。
Prices for individual futures contracts
数据对象:
futuresContractPriceData及相关对象,用于表示单个期货合约的 OHLCV 或日度收盘价。
数据存储对象:
csvFuturesContractPriceData:从 CSV 读取;mongoFuturesContractPriceData:从 MongoDB 读取;parquetFuturesContractPriceData:从 Parquet 读取;- 等等。
在前面的工作流中,我们已经使用这些对象从 IB 或 Barchart 导入合约历史价格,并写入 MongoDB / Parquet。
Final prices for individual futures contracts
有时我们需要“最终定稿”的合约价格(例如经过清洗、去除错误、填补缺失等):
- 数据对象:
futuresContractFinalPrices;
- 数据存储对象:
- 对应的
*FuturesContractFinalPricesData类; - 与普通合约价格类似,但语义上表示“可以用于生产和回测的最终版本”。
- 对应的
Named futures contract dicts
- 数据对象:
dictFuturesContractPrices或类似的“合约字典”:- key:合约 ID(如
20201200); - value:对应的价格时间序列(pandas Series 或 DataFrame)。
- key:合约 ID(如
这种结构在构造换月日历和 multiple prices 时非常方便,因为这些逻辑需要同时访问多个合约的价格序列。
Roll calendar data object
换月日历在内部用一个 pandas DataFrame 表示,包含列:
current_contractnext_contractcarry_contract
每一行代表一个换月日期:
从 current_contract(持仓合约)换到 next_contract,并指定用于 carry 的 carry_contract。
换月日历可以通过多种方式创建:
- 基于 roll 参数 + 单合约价格;
- 基于已有的 multiple prices(反推);
- 使用仓库自带的 CSV。
Multiple prices
multiple prices 对象本质上是一个 pandas DataFrame,其中典型列包括:
PRICE,CARRY,FORWARD:三个价格序列;PRICE_CONTRACT,CARRY_CONTRACT,FORWARD_CONTRACT:对应合约 ID。
通常我们通过“换月日历 + 单合约价格”来构建 multiple prices(详见 Creating and storing multiple prices)。
构建完成后,它们可以被存储并在回测/生产中反复读取。
Adjusted prices
回溯调整价在内部仅是一个 pandas Series,本身并不复杂;有趣的是:
- 如何根据 multiple prices 生成它;
- 如何选择不同的拼接方法。
示例:
| |
默认的拼接方法是 Panama,如果你想用其它方法,可以重写 futuresAdjustedPrices.stitch_multiple_prices。
Spot FX data
严格来说 FX 和期货无关,但在 pysystemtrade 中放在一起,是因为:
- 仓位规模、风控和记账都需要 FX 转换;
- 某些逻辑会频繁使用 FX 数据。
Data storage objects for specific sources
本节介绍针对具体数据源(CSV、MongoDB、Parquet、IB 等)的数据存储对象。
CSV data files
用 CSV 存数据看上去“有点老派”,但在以下场景中非常合适:
- 换月日历:有时需要手工修改、检查;
- 仓库自带的回测数据:作为项目的一部分存放在 Git 中。
需要注意:
- Git 对大文件/二进制文件支持不理想,虽然有一些 workaround(如 LFS),但我还没找到完全满意的方案;
- 因此只把相对小、可读的 CSV 放进仓库;
- 对 CSV 数据存储,一般只实现“读”和“写”方法,不做“删除”:
- 想删文件就直接在文件系统里删;
- 不需要让 Python 替你背这口锅。
MongoDB
对于生产环境和大规模数据(例如所有单合约价格),CSV 不再足够;我们需要更可靠的数据库,例如 MongoDB。
使用 MongoDB 时,你需要确保本地已有 MongoDB 实例:
- 在 Linux 上可以用
ps wuax | grep mongo查看是否已经在跑; - 如果有旧进程,可以先杀掉再按你想要的方式重启。
我个人习惯把 MongoDB 数据放在一个特定子目录,比如:
mongod --dbpath ~/data/mongodb/
这样可以更好地控制备份和空间。
Specifying a MongoDB connection
连接 MongoDB 时需要指定:
- 主机(host / IP);
- 数据库名称(database);
- 端口(port)。
这些参数的设置优先级从高到低为:
构造
mongoDb()实例时显式传入的参数- 例如:
mongoDb(mongo_host='localhost', mongo_database_name='production', mongo_port=27017) - 然后将该实例以
mongo_db=...的方式传递给各个数据对象。
- 例如:
私有配置文件
/private/private_config.yaml中的配置项mongo_host、mongo_db、mongo_port。
系统默认配置文件
/sysdata/config/defaults.yaml中的默认值。
注意:
localhost等价于127.0.0.1(本机);若使用非标准 MongoDB 端口或带用户名密码的 URL,需要用 URL 形式指定 host,例如:
mongo_host: mongodb://<username>:<password>@localhost:28018
如果 MongoDB 就跑在本机,且你接受默认的数据库名 production,那么可以直接使用默认配置。
如果:
- MongoDB 在另一台机器;
- 或者你想用不同的数据库名;
那么建议在 private_config.yaml 中显式配置上述参数。
Parquet
Parquet 是一种开源列式存储格式,非常适合时间序列数据:
- 与 pandas Series / DataFrame 的互操作性很好;
- 存储紧凑、读写速度快。
在 pysystemtrade 中:
- MongoDB 更适合存放“静态/结构化信息”(如配置、日志等);
- Parquet 更适合存放“时间序列价格数据”;
- 你可以视需求选择只用其中之一,或两者同时用。
Interactive Brokers
我们并不会把 IB 当作“持久化数据存储”,但会为 IB 实现一些“数据访问对象”,以便:
- 获取期货和 FX 价格(用于回填或增量更新);
- 作为生产层服务接口(创建订单、获取成交等)。
更多 IB 细节见文档:/docs/IB.md。
Creating your own data storage objects for a new source
如果你有新的数据源(例如某家付费数据提供商、内部数据库等),为其创建数据存储对象其实很简单——前提是它用于现有的数据对象类型(例如 futures contract prices,而不是全新的“奇怪数据类型”)。
约定如下:
- 新的数据存储类应放在
/sysdata的某个子目录下;- 目录名以数据源命名,例如
/sysdata/parquet、/sysdata/csv等;
- 目录名以数据源命名,例如
- 类名使用
sourceNameOfObjectData形式:- 例如:
class parquetFuturesContractPriceData(futuresContractPriceData); - 前缀是数据源名(parquet / csv / mongo / ib 等);
- 中间是 CamelCase 的数据对象名称;
- 末尾固定为
Data。
- 例如:
必须遵守这一命名约定,否则 dataBlob 的自动重命名机制会失效。
dataBlob 会把 sourceSomethingInCamelCaseData 自动重命名为:
db_something_in_camel_case(如果该对象属于“数据存储”);- 或
broker_xxx(如果是券商接口)。
如果你添加了新的数据源,需要在 dataBlob 的“解析字典”中加入这个源的前缀。
对于数据库类数据源,你可能还需要创建类似 /sysdata/mongodb/mongo_connection.py 的“连接对象”,将底层数据库操作抽象成简单的读写/更新/删除方法。
Part 4: Interfaces
本部分介绍若干“接口层”对象,它们位于数据存储对象与上层业务逻辑之间,包括:
dataBlob;- 回测侧的
simData; - 生产侧的
sysproduction/data接口。
其目的都是“隐藏底层存储细节”,让回测和生产代码尽量只关心“要什么数据”,而不是“数据从哪儿来”。
Data blobs
先看 dataBlob 是什么。示例:
| |
假设我们想从 Parquet 中获取 adjusted prices,可以这样做:
| |
注意:我们访问的是 data.db_futures_adjusted_prices。
这是因为 dataBlob 会自动做如下处理:
- 动态创建传入的数据存储类的实例;
- 把“数据源前缀”(例如
parquet)替换成db; - 去掉类名末尾的
Data; - 并把 CamelCase 改写成下划线形式;
- 然后挂到
data实例上作为属性。
在回测和生产中,我们通常不会直接在业务代码里访问这些属性,而是通过更高一层的接口对象(例如 simData、diagPrices 等)来使用它们,这样可以进一步隐藏底层细节。
再看一个例子:如果我们想从 CSV 获取调整价格:
| |
在 dataBlob 看来,CSV 只是另一种“数据库”:
- 此时
db_futures_adjusted_prices属性会指向 CSV 版本的存储对象; - 如果之后再添加 Parquet 版本的同名存储对象,它会被后添加的覆盖。
dataBlob 还支持一些其它实用功能(这里只做简短概述):
- 可以一次性添加多个类;
- 可以方便地查看当前有哪些数据对象可用;
- 可以用于构建回测和生产中的“统一数据层”。
simData objects
在回测中,我们不会直接在系统代码中访问 dataBlob,而是通过 simData 对象:
simData内部持有一个dataBlob;- 对外暴露“第二层接口”,例如:
- 获取某品种的 adjusted prices;
- 获取 FX 价格;
- 获取品种配置、交易成本等;
- 回测中的
System对象会把simData视为system.data阶段。
这样做的好处是:
- 回测代码只需要考虑“需要哪些数据”;
- “这些数据来自 CSV 还是 MongoDB 还是 Parquet”由
simData/dataBlob决定; - 在不同项目中可以自由切换数据源,而不改动核心系统逻辑。
Provided simData objects
pysystemtrade 提供了若干现成的 simData 实现,主要是:
csvFuturesSimData()
- 只使用 CSV 数据源;
- 适合快速上手和小规模实验;
- 读取仓库内自带的 multiple / adjusted prices 和配置文件。
dbFuturesSimData()
- 使用 MongoDB / Parquet 等数据库作为主要数据源;
- 适合生产级回测和与实盘数据共享数据源的场景;
- 会使用前面提到的各类
parquet*Data、mongo*Data等实现。
A note about multiple configuration files
与期货品种相关的配置信息分散在多个文件中,例如:
- 品种配置和成本信息:
data/futures/csvconfig/instrumentconfig.csvdata/futures/csvconfig/spreadcosts.csv
- 换月配置:
data/futures/csvconfig/rollconfig.csv
- IB 相关配置:
/sysbrokers/IB/config/ib_config_spot_FX.csv/sysbrokers/IB/config/ib_config_futures.csv
这些文件中的品种列表不一定完全一致。但按照 DRY 原则:
- 列名(字段)不应重复;
- 不应在多个文件中维护同一配置项。
在回测中:
system.get_instrument_list()用于决定要交易哪些市场;- 如果系统配置中没有显式给出品种列表,它会退化到调用
system.data.get_instrument_list(); - 在提供的
simData实现中,该调用最终会落到“获取 adjusted prices 的数据存储对象”的get_instrument_list方法上(或其覆盖实现)。
换句话说:
- 只要你有某个品种的 adjusted prices,就可以在回测中交易它;
- 但如果你有 adjusted prices 却没有对应的配置(instrument / roll 等),系统会报错;
- 因此允许“配置文件中的品种集合是 adjusted prices 品种集合的超集”,但不允许反过来。
Modifying simData objects
simData 的构造方式使得它们非常容易“按需定制”。
下面举一个稍微“怪异”但易于类推的例子:
Getting data from another source
假设你想:
- 用 Parquet / MongoDB 存储大部分回测数据;
- 但希望 FX 价格来自某个自定义的 CSV 目录。
做法是修改 /sysdata/sim/db_futures_sim_data.py,例如:
| |
然后在交互式环境中测试:
| |
可以看到:
- multiple prices 仍来自 Parquet;
- FX 价格改用 CSV 目录;
- 其它逻辑保持不变。
你可以用同样方式组合任何数据源,只要它们实现了相应的通用数据存储接口。
Production interface
在生产环境中,我们使用 /sysproduction/data 模块中的一组接口对象,作为“生产代码与数据层之间”的桥梁:
- 它们内部持有一个
dataBlob; - 对外暴露“更高层次的业务方法”;
- 还会封装部分业务逻辑与安全检查。
这些类大致可分为三类:
diag*:只读(diagnostic,诊断);update*:只写(更新);data*:读写皆可(读写频繁时没必要拆成两个类)。
主要接口包括(简要说明):
dataBacktest:- 读写生产环境中运行的回测结果(pickled backtest state),通常位于
run_systems产出目录。
- 读写生产环境中运行的回测结果(pickled backtest state),通常位于
dataBroker:- 与券商交互(下单、获取成交等)。
dataCapital:- 读写总资金与策略级资金。
diagContracts/updateContracts:- 读写单个期货合约的信息(上市/到期日期、合约规模等)。
dataLocks:- 读写“锁定信息”(例如在某些冲突情况下暂时禁止某个品种交易)。
diagOverrides/updateOverrides:- 读写“覆盖规则”(例如强制减仓或禁止加仓)。
dataControlProcess,diagProcessConfig:- 控制和诊断生产进程的启停。
dataPositionLimits:- 读写仓位限制;
dataTradeLimits:- 读写单笔交易限制。
diagInstruments:- 获取品种配置。
dataOrders:- 读写历史订单与当前订单“栈”(order stack)。
diagPositions/updatePositions:- 读写历史与当前持仓。
dataOptimalPositions:- 读写“最优持仓”(optimal positions)数据。
diagPrices/updatePrices:- 读写期货价格数据(adjusted、multiple、单合约)。
dataCurrency:- 读写 FX 数据并执行货币转换。
dataSimData:- 为生产中的“回放回测”创建一个
simData对象。
- 为生产中的“回放回测”创建一个
diagStrategiesConfig:- 策略配置(资金分配、回测配置、订单生成器等)。
diagVolumes:- 成交量数据。
通过这些接口,生产代码可以以高度抽象、强类型的方法访问所有需要的数据,而不用了解底层存储细节;同时也方便单独测试和调试数据层逻辑。