数据体系(Data)

期货数据、MongoDB、Parquet 等。

译文说明

  • 原文链接:docs/data.md
  • 原作者:Rob Carver
  • 对应版本:master
  • 译者:fanrong
  • 许可:GPL-3.0(见 GPL-3.0.txt

注:本文为非官方翻译,可能存在疏漏;请以原文为准。

本文档专门讨论如何存储和处理期货数据

相关文档:

全文分为四部分。第一部分期货数据工作流给出“从零开始准备期货数据”的整体流程:如何获取数据、如何存储、以及如何生成可用于回测和作为实盘初始状态的数据。读完这一部分,你也会对 pysystemtrade 中使用的数据类型有一个整体感知。其余部分会逐步深入细节:在第二部分中,我会说明各类数据对象之间是如何拼在一起的;第三部分会详细介绍每一种期货数据组件、以及它们在存储层的表示;在第四部分中,你会看到数据存储对象与回测 / 生产代码之间的接口设计。

Table of Contents

Part 1: A futures data workflow

本部分给出“从零开始准备期货数据”的完整流程。建议按照顺序阅读并动手操作一遍,即便你暂时只打算跑回测,也非常值得照做。

总体目标是:

  1. 为每个期货品种(instrument)准备合约配置与成本;
  2. 获取单个期货合约级别的历史价格数据;
  3. 生成换月日历(roll calendar);
  4. 基于换月日历和合约历史价格生成 multiple prices(包含当前、下一合约以及 carry 合约的价格序列);
  5. 再由 multiple prices 生成单一的回溯调整价(back adjusted prices);
  6. 同时准备好外汇(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.csv
  • data/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 文件。它会做两件事:

  1. 把文件重命名为 pysystemtrade 预期的命名规则;
  2. 读取 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

1
2
3
4
5
6
7
8
barchart_csv_config = ConfigCsvFuturesPrices(input_date_index_name="Date Time",
                                input_skiprows=1, input_skipfooter=1,
                                input_column_mapping=dict(OPEN='Open',
                                                          HIGH='High',
                                                          LOW='Low',
                                                          FINAL='Close',
                                                          VOLUME='Volume'
                                                          ))

可以看到:

  • Barchart 文件开头有一行可以跳过(标题行前的一行),结尾有一行 footer 也需要忽略;
  • 第二行才是真正的列名,其中 Date Time 列包含时间索引;
  • input_column_mapping 用来把我偏好的列名(大写)映射到文件中的列名。

另一个可选参数是 input_date_format,默认值为 %Y-%m-%d %H:%M:%S
调整这些选项后,你基本可以读取 99% 的第三方 CSV 数据源;实在特殊的,就需要自己写 parser 了。

真正的读取和写入逻辑在这里:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
def init_db_with_csv_futures_contract_prices_for_code(instrument_code: str, datapath: str,
                                                          csv_config=arg_not_supplied):
    print(instrument_code)
    csv_prices = csvFuturesContractPriceData(datapath, config=csv_config)
    db_prices = diag_prices.db_futures_contract_price_data

    csv_price_dict = csv_prices.get_merged_prices_for_instrument(instrument_code)

    for contract_date_str, prices_for_contract in csv_price_dict.items():
        print(contract_date_str)
        contract = futuresContract(instrument_code, contract_date_str)
        db_prices.write_merged_prices_for_contract_object(contract, prices_for_contract, ignore_duplication=True)

对象 csvFuturesContractPriceDatadb_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 使用第二个合约。
1
2
3
4
DATE_TIME,current_contract,next_contract,carry_contract
2020-02-28,20200300,20200600,20200600
2020-06-01,20200600,20200900,20200900
2020-08-31,20200900,20201200,20201200
  • 在 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)。

生成换月日历有三种方式:

  1. 基于你已有的单合约价格数据生成(见下面小节);
  2. 基于现有的 multiple prices CSV 推断(见Roll calendars from existing ‘multiple prices’ CSV files);
  3. 直接使用仓库内已经提供好的换月日历(见 /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 参数中的 ExpiryOffsetRollOffsetDays 等,计算一个初始的换月日历:

    1
    2
    
    roll_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 中的代码。

核心部分是:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
    @classmethod
    def create_from_prices(
        rollCalendar, dict_of_futures_contract_prices:dictFuturesContractFinalPrices,
            roll_parameters_object: rollParameters
    ):

        approx_calendar = generate_approximate_calendar(
            roll_parameters_object, dict_of_futures_contract_prices
        )

        adjusted_calendar = adjust_to_price_series(
            approx_calendar, dict_of_futures_contract_prices
        )

        roll_calendar = rollCalendar(adjusted_calendar)

流程可以理解为:

  1. 根据换月参数生成“理想换月日历”(approx_calendar),即在每个理论目标日期上做一次换月;
  2. 检查每个换月日期当天是否同时存在“当前合约”和“下一合约”的价格(我们需要二者都在场,才能计算回溯调整价);
  3. 若理想日期当天没有“匹配价格”(matching prices),则在时间上前后搜索最近的有匹配价格的日期,并将换月日历调整到该日期(adjust_to_price_series)。

如果在整个时间轴上都找不到匹配价格,则该品种的换月日历生成会报错。
此时你有几种选择:

  • 修改 roll 参数(例如使用后一合约,而不是前一合约);
  • 获取更多的单合约历史价格(让相邻合约价格区间有所重叠);
  • 在极端情况下,手工“伪造”一些价格,让两个合约在某些日期有重叠(严格来说这是作弊,因为你不知道真实的 roll spread,但有时候为了不浪费其他数据,只能这么做)。

Checks

生成换月日历后,我们需要对其做两类检查:

1
2
3
4
5
6
7
    # checks - this might fail
    roll_calendar.check_if_date_index_monotonic()

    # this should never fail
    roll_calendar.check_dates_are_valid_for_prices(
        dict_of_futures_contract_prices
    )
  • 单调性检查(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

脚本的大致逻辑是:

  1. csv_roll_data_path 读取换月日历(默认路径为 /data/futures/roll_calendars_csv;如果你遵照前面的建议把日历放在临时目录,请相应修改);
  2. 获取每个合约的收盘价(在 multiple / adjusted price 阶段,我们只需要收盘价,不需要完整 OHLC);
  3. 可选但推荐:用合约收盘价对换月日历进行“对齐调整”,确保换月日期都能找到匹配价格。
    • 如果你是通过“从单合约价格生成日历”的方式得到的日历,这一步可以省略(已经对齐过);
    • 如果是方法 2 或 3,或者你手工改动过日历文件,则强烈建议执行这一步。
  4. 在未来一周位置添加一个“phantom roll”(虚拟换月点),否则数据不会延伸到“今天”——第一次跑生产更新后这个问题会自动消失,但有些人不喜欢看到“数据只到几天前”。
  5. 按换月日历,把各期合约的数据“缝合”成 multiple prices。
  6. 按脚本参数决定:
    • 是否把 multiple prices 写入 csv_multiple_data_path(默认为 /data/futures/multiple_prices_csv);
    • 是否写入数据库(MongoDB / Parquet)。

我个人习惯同时写 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。
相关脚本示例为:

整体思路是:

  1. 先更新“需要采样的合约集合”和它们的历史价格(见生产文档中的:
  2. 用更新后的单合约价格为目标品种构造一份新的 roll calendar;
  3. 使用这份 roll calendar 和新的合约价格生成 multiple prices;
  4. 把新 multiple prices 与仓库原有 multiple prices 做“拼接”(splicing),得到一条更长的数据;
  5. 将拼接后的 multiple prices 写回数据库。

需要留意一个细节:
仓库中的某些 multiple prices(尤其是 mini 合约的历史价格由主合约推算而来时),其 carry offset 和 rollconfig.csv 中的设置不完全一致。如果你在策略中使用 carry 交易规则,最好检查这些设定是否一致(可以暂时修改 rollconfig.csv 进行对比)。

下面是伪代码式的示例流程:

先创建一些临时工作目录,然后从数据库价格构建 roll calendar:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
import os

roll_calendars_from_db = os.path.join('data', 'futures', 'roll_calendars_from_db')
if not os.path.exists(roll_calendars_from_db):
    os.makedirs(roll_calendars_from_db)

multiple_prices_from_db = os.path.join('data', 'futures', 'multiple_from_db')
if not os.path.exists(multiple_prices_from_db):
    os.makedirs(multiple_prices_from_db)

spliced_multiple_prices = os.path.join('data', 'futures', 'multiple_prices_csv_spliced')
if not os.path.exists(spliced_multiple_prices):
    os.makedirs(spliced_multiple_prices)

from sysinit.futures.rollcalendars_from_db_prices_to_csv import build_and_write_roll_calendar
instrument_code = 'GAS_US_mini' # for example
build_and_write_roll_calendar(instrument_code, 
    output_datapath=roll_calendars_from_db)

然后用我们更新后的价格 + 新生成的 roll calendar 来计算 multiple prices:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
from sysinit.futures.multipleprices_from_db_prices_and_csv_calendars_to_db import \
    process_multiple_prices_single_instrument

process_multiple_prices_single_instrument(
    instrument_code,
    csv_multiple_data_path=multiple_prices_from_db, 
    ADD_TO_DB=False,
    csv_roll_data_path=roll_calendars_from_db,
    ADD_TO_CSV=True
)

接着,把新生成的数据拼接到仓库原有的数据后面(并检查价格合约与 forward 合约的连续性):

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
supplied_file = os.path.join('data', 'futures', 'multiple_prices_csv', instrument_code + '.csv') # repo data
generated_file = os.path.join(multiple_prices_from_db, instrument_code + '.csv')

import pandas as pd
supplied = pd.read_csv(supplied_file, index_col=0, parse_dates=True)
generated = pd.read_csv(generated_file, index_col=0, parse_dates=True)

# get final datetime of the supplied multiple_prices for this instrument
last_supplied = supplied.index[-1] 

print(f"last datetime of supplied prices {last_supplied}, first datetime of updated prices is {generated.index[0]}")

# assuming the latter is later than the former, truncate the generated data:
generated = generated.loc[last_supplied:]

# if first datetime in generated is the same as last datetime in repo, skip that row
first_generated = generated.index[0] 
if first_generated == last_supplied:
    generated = generated.iloc[1:]

# check we're using the same price and forward contracts (i.e. no rolls missing, which there shouldn't be if there is date overlap)
assert(supplied.iloc[-1].PRICE_CONTRACT == generated.loc[last_supplied:].iloc[0].PRICE_CONTRACT)
assert(supplied.iloc[-1].FORWARD_CONTRACT == generated.loc[last_supplied:].iloc[0].FORWARD_CONTRACT)
# nb we don't assert that the CARRY_CONTRACT is the same for supplied and generated, as some of the rolls implicit in the supplied multiple_prices don't match the pattern in the rollconfig.csv

最后,将拼接后的 multiple prices 写入 CSV,并利用已有脚本写回数据库:

1
2
3
4
5
spliced = pd.concat([supplied, generated])
spliced.to_csv(os.path.join(spliced_multiple_prices, instrument_code+'.csv'))

from sysinit.futures.multiple_and_adjusted_from_csv_to_db import init_db_with_csv_prices_for_code
init_db_with_csv_prices_for_code(instrument_code, multiple_price_datapath=spliced_multiple_prices)

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 作为示例。

使用方式(示例):

  1. 去 investing.com 注册账号;
  2. 下载你需要的 FX 历史(例如 GBPUSD 等);
  3. 把下载的 CSV 文件放到一个“只包含 FX 文件”的目录中,文件名格式形如 GBPUSD.csv

要查看仓库中现有的 FX CSV 已经覆盖多少历史,可以运行:

1
2
3
from sysdata.csv.csv_spot_fx import *
data=csvFxPricesData()
data.get_fx_prices("GBPUSD")

然后使用脚本 /sysinit/futures/spotfx_from_csvAndInvestingDotCom_to_db.py

  • 该脚本会读入指定目录中的 CSV;
  • 视参数设置,将数据写入数据库和/或新的 CSV 文件;
  • 你可以调整脚本中的目录路径、列名映射与格式,来兼容其它数据源。

如果只想把随附 CSV 直接复制到数据库(不补充 investing.com 等额外历史),可以把脚本参数设为:

  • ADD_EXTRA_DATA = False
  • ADD_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

简化后的层级关系如下:

  1. 数据对象(data object)
    例如:

    • futuresInstrument:描述一个期货品种(合约乘数、tick size 等静态信息);
    • futuresContract:具体到某个合约月份的标识;
    • futuresContractFinalPrices:单个合约的最终日度价格;
    • futuresMultiplePrices:multiple prices;
    • futuresAdjustedPrices:回溯调整价;
    • spotFxPrices:即期 FX 价格; 等。
  2. 通用数据存储对象(generic data storage object)
    例如:

    • futuresInstrumentData
    • futuresContractPriceData
    • futuresMultiplePricesData
    • futuresAdjustedPricesData
    • fxPricesData 等。
  3. 具体后端实现(source-specific storage objects)
    对于每种数据类型,可以有多个“存储后端”实现,例如:

    • csvFuturesInstrumentData:用 CSV 存储品种配置;
    • mongoFuturesContractPriceData:用 MongoDB 存储单合约价格;
    • parquetFuturesMultiplePricesData:用 Parquet 存储 multiple prices;
    • csvFxPricesData / parquetFxPricesData 等。
  4. dataBlob

    • dataBlob 封装了一组 data storage 对象,并为上层提供统一访问接口;
    • 它负责创建各后端数据对象实例,并把它们挂在诸如 db_futures_multiple_pricesdb_fx_prices 这样的属性上;
    • 通过 dataBlob,可以在不关心“数据存在哪儿”的情况下使用统一 API。
  5. 回测接口(simData)与生产接口(/sysproduction/data 下的各类接口)

    • 回测时,simData 对象持有一个 dataBlob,并提供面向回测的高层方法(例如“获取某品种的 adjusted prices”);
    • 实盘时,sysproduction/data 下的各个接口类也持有 dataBlob,并在其基础上添加业务逻辑与安全检查。

文中偶尔提到 arctic,那是项目早期使用的时间序列存储方案(Arctic);现在已经被 Parquet 取代,只在文档中保留了一些历史说明。更多可见 backtesting 文档

这里只列出与“数据对象 / 存储 / 接口”相关的目录结构,便于你快速导航:

掌握这些目录结构后,你可以更容易地找到需要修改或扩展的代码位置。

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,包含本章前面提到的:
      • RollOffsetDays
      • ExpiryOffset
      • CarryOffset
      • 等等。
  • 数据存储对象

    • rollParametersData 及其后端实现;
    • rollconfig.csv 或其它来源读取这些参数。

Contract date with roll parameters

有时候我们需要把“合约日期信息”和“换月参数”一起使用,例如在构造换月日历时。

  • 数据对象

    • 组合了 contractDatesrollParameters 的混合对象(可视为一个方便的封装)。
  • 数据存储对象

    • 提供统一方法:既能拿到“某品种有哪些合约/月份”,又能同时拿到对应的 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)。

这种结构在构造换月日历和 multiple prices 时非常方便,因为这些逻辑需要同时访问多个合约的价格序列。

Roll calendar data object

换月日历在内部用一个 pandas DataFrame 表示,包含列:

  • current_contract
  • next_contract
  • carry_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 生成它;
  • 如何选择不同的拼接方法。

示例:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
from sysobjects.adjusted_prices import futuresAdjustedPrices
from sysproduction.data.prices import diagPrices

diag_prices = diagPrices()

# assuming we have some multiple prices
db_multiple_prices = diag_prices.db_futures_multiple_prices_data
multiple_prices = db_multiple_prices.get_multiple_prices("DAX")

adjusted_prices = futuresAdjustedPrices.stitch_multiple_prices(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)。

这些参数的设置优先级从高到低为:

  1. 构造 mongoDb() 实例时显式传入的参数

    • 例如:
      mongoDb(mongo_host='localhost', mongo_database_name='production', mongo_port=27017)
    • 然后将该实例以 mongo_db=... 的方式传递给各个数据对象。
  2. 私有配置文件 /private/private_config.yaml 中的配置项

    • mongo_hostmongo_dbmongo_port
  3. 系统默认配置文件 /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 是什么。示例:

1
2
3
4
5
from sysdata.data_blob import dataBlob
data = dataBlob()
data

dataBlob with elements: 

假设我们想从 Parquet 中获取 adjusted prices,可以这样做:

1
2
3
4
5
from sysdata.parquet.parquet_adjusted_prices import parquetFuturesAdjustedPricesData
data.add_class_object(parquetFuturesAdjustedPricesData)

data.db_futures_adjusted_prices.get_list_of_instruments()
['DAX', 'CAC', 'KR3', 'SMI', 'V2X', 'JPY', ....]

注意:我们访问的是 data.db_futures_adjusted_prices
这是因为 dataBlob 会自动做如下处理:

  • 动态创建传入的数据存储类的实例;
  • 把“数据源前缀”(例如 parquet)替换成 db
  • 去掉类名末尾的 Data
  • 并把 CamelCase 改写成下划线形式;
  • 然后挂到 data 实例上作为属性。

在回测和生产中,我们通常不会直接在业务代码里访问这些属性,而是通过更高一层的接口对象(例如 simDatadiagPrices 等)来使用它们,这样可以进一步隐藏底层细节。

再看一个例子:如果我们想从 CSV 获取调整价格:

1
2
3
4
5
6
7
from sysdata.csv.csv_adjusted_prices import csvFuturesAdjustedPricesData
data.add_class_list([csvFuturesAdjustedPricesData]) # 这里传的是类列表

2020-11-30:1535.48 {'type': ''} [Warning] No datapaths provided for .csv, will use defaults  (may break in production, should be fine in sim)

data.db_futures_adjusted_prices.get_list_of_instruments()
['DAX', 'CAC', 'KR3', 'SMI', 'V2X', 'JPY', ....]

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*Datamongo*Data 等实现。

A note about multiple configuration files

与期货品种相关的配置信息分散在多个文件中,例如:

  • 品种配置和成本信息:
    • data/futures/csvconfig/instrumentconfig.csv
    • data/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,例如:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
# add import
from sysdata.csv.csv_spot_fx import csvFxPricesData

# change the mapping for FX_DATA at line 70
use_sim_classes = {
    # FX_DATA: parquetFxPricesData,
    FX_DATA: csvFxPricesData,
    ROLL_PARAMETERS_DATA: csvRollParametersData,
    FUTURES_INSTRUMENT_DATA: csvFuturesInstrumentData,
    FUTURES_MULTIPLE_PRICE_DATA: parquetFuturesMultiplePricesData,
    FUTURES_ADJUSTED_PRICE_DATA: parquetFuturesAdjustedPricesData,
    STORED_SPREAD_DATA: mongoSpreadCostData,
}

然后在交互式环境中测试:

1
2
3
4
5
6
7
8
9
>>> from sysdata.sim.db_futures_sim_data import dbFuturesSimData
>>> from systems.provided.futures_chapter15.basesystem import futures_system
>>> system = futures_system(data=dbFuturesSimData())
2025-01-01 12:00:00 DEBUG config {'type': 'config', 'stage': 'config'} Adding config defaults
Private configuration private/private_config.yaml does not exist; no problem if running in sim mode
>>> system.data.data.db_futures_multiple_prices
parquetFuturesMultiplePricesData
>>> system.data.data.db_fx_prices
csvFxPricesData accessing data.futures.fx_prices_csv

可以看到:

  • multiple prices 仍来自 Parquet;
  • FX 价格改用 CSV 目录;
  • 其它逻辑保持不变。

你可以用同样方式组合任何数据源,只要它们实现了相应的通用数据存储接口。

Production interface

在生产环境中,我们使用 /sysproduction/data 模块中的一组接口对象,作为“生产代码与数据层之间”的桥梁:

  • 它们内部持有一个 dataBlob
  • 对外暴露“更高层次的业务方法”;
  • 还会封装部分业务逻辑与安全检查。

这些类大致可分为三类:

  • diag*:只读(diagnostic,诊断);
  • update*:只写(更新);
  • data*:读写皆可(读写频繁时没必要拆成两个类)。

主要接口包括(简要说明):

  • dataBacktest

    • 读写生产环境中运行的回测结果(pickled backtest state),通常位于 run_systems 产出目录。
  • 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

    • 成交量数据。

通过这些接口,生产代码可以以高度抽象、强类型的方法访问所有需要的数据,而不用了解底层存储细节;同时也方便单独测试和调试数据层逻辑。