译文说明
- 原文链接:docs/production.md
- 原作者:Rob Carver
- 对应版本:master
- 译者:fanrong
- 许可:GPL-3.0(见 GPL-3.0.txt)
注:本文为非官方翻译,可能存在疏漏;请以原文为准。
本文档专门讨论如何使用 pysystemtrade 进行实盘生产环境交易。
本文件涵盖以下内容:
- 获取价格数据
- 生成目标交易指令
- 执行交易
- 获取账户与记账信息
相关文档(建议在阅读本篇之前先读完):
以及建议在本篇之后阅读的文档:
重要提示:务必确认你清楚自己在做什么。所有金融交易都可能带来亏损。杠杆交易(例如期货交易)可能会导致你亏光全部资金,甚至出现欠款。回测结果并不能保证未来表现。本软件不提供任何形式的担保或保证。因使用 pysystemtrade 进行实盘交易而造成的任何损失,本人概不负责。使用本软件风险自负。
目录
- 目录
- 快速开始指南
- 生产系统数据流
- 生产系统概览
- 仓位与订单层级
- 一张订单的旅程
- Scripts
- Script 调用方式
- Script 命名约定
- Run processes
- 核心生产系统组件(Core production system components)
- 用于修改数据的交互式脚本
- 菜单驱动的交互式脚本
- Interactive controls
- 交互式诊断(Interactive diagnostics)
- 交互式订单栈(Interactive order stack)
- 查看(View)
- 创建订单
- 从 instrument 订单生成 contract 订单
- 创建强制滚动 contract 订单(Create force roll contract orders)
- 创建(并尝试执行)IB broker 订单(Create (and try to execute…) IB broker orders)
- 平衡交易:创建一系列交易并立即填充(不会真实执行)(Balance trade: Create a series of trades and immediately fill them (not actually executed))
- 平衡 instrument 交易:只在策略层创建并填充(不真实执行)(Balance instrument trade: Create a trade just at the strategy level and fill (not actually executed))
- 手工交易:创建一系列将被执行的交易(Manual trade: Create a series of trades to be executed)
- 现金外汇交易(Cash FX trade)
- 净额处理、取消与锁定
- 删除与清理
- 报表、日常维护与备份脚本
- 非 Linux 系统下的脚本
- 调度(Scheduling)
- 生产系统核心概念(Production system concepts)
- 崩溃后的恢复:哪些能救、怎么救,哪些救不了(Recovering from a crash - what you can save and how, and what you can’t)
- 报告(Reporting)
以上目录由 gh-md-toc 自动生成。
快速开始指南
本“快速开始”指南基于如下假设:
- 你在一台安装了合适 Linux 发行版的机器上运行(作者使用的是 Linux Mint)。Windows / Mac 用户在某些步骤上需要稍作调整
- 你使用 Interactive Brokers 作为券商
- 你使用 MongoDB 存储数据
- 你已经有一个自己认可的回测结果
- 你愿意将数据和配置存放在 pysystemtrade 安装目录下的
/private目录中
你需要完成以下工作:
- 仔细、完整地阅读本篇文档!
- 前置条件:
- 安装 git,安装或升级 python3。你可能还会需要一个简单的文本编辑器(如 emacs)来做微调;如果你在无头服务器上工作,x11vnc 会很有帮助。
- 在
~/.profile文件中添加以下环境变量(目录可按需修改):MONGO_DATA=/home/user_name/data/mongodb/PYSYS_CODE=/home/user_name/pysystemtradeSCRIPT_PATH=/home/user_name/pysystemtrade/sysproduction/linux/scriptsECHO_PATH=/home/user_name/echosMONGO_BACKUP_PATH=/media/shared_network/drive/mongo_backup
- 将
SCRIPT_PATH指向的目录添加到PATH环境变量中。 - 创建以下目录(同样可以使用其它目录,但要同步修改
.profile和private_config.yaml中对应路径):/home/user_name/data/mongodb//home/user_name/data/parquet/home/user_name/echos//home/user_name/data/mongo_dump/home/user_name/data/backups_csv/home/user_name/data/backtests/home/user_name/data/reports
- 在目录
$PYSYS_CODE中安装 pysystemtrade 包,并安装或更新其依赖(也可以安装在其它目录,但需要同步修改上面的环境变量)。如果在 home 目录下用git clone,通常会创建目录/home/user_name/pysystemtrade/。 - 按照 IB 文档 设置 Interactive Brokers,并启动一个 gateway。
- 安装 MongoDB。
- 在 pysystemtrade 的
private目录中创建private_config.yaml文件;可选地在同一目录下创建private_control_config.yaml。详情见这里。 - 在
private_config.yaml中将parquet_store字段设为你之前创建的 Parquet 目录。 - 检查 MongoDB server 是否用正确的数据目录运行,命令行:
mongod --dbpath $MONGO_DATA。 - 启动一个 IB gateway(根据你的安全设置,也可以自动启动)。
- 外汇(FX)数据:
- 使用脚本 repocsv_spotfx_prices.py 从 CSV 文件初始化即期外汇数据(这些数据会有些过期,但稍后你会更新)。
- 使用 Interactive Brokers 更新 FX 价格数据:命令行:
. /home/your_user_name/pysystemtrade/sysproduction/linux/scripts/update_fx_prices。
- 品种配置(Instrument configuration):
- 使用脚本 repocsv_spread_costs.py 设置各期货品种的买卖价差成本。
- 期货合约价格:
- 换月日历(Roll calendars):
- 确保你正在采样所有你希望采样的合约。
- 调整后的期货价格(Adjusted futures prices):
- 使用交互式诊断工具检查所有价格是否已正确准备就绪。
- 实盘生产回测(Live production backtest):
- 创建一个 YAML 配置文件,用于运行“实盘生产回测”系统。为了提高速度,建议不要在此处重新估计参数,而是使用固定参数;可以通过
systemDiag的 yaml_config_with_estimated_parameters 方法,将已估计好的参数输出到一个 YAML 文件中。
- 创建一个 YAML 配置文件,用于运行“实盘生产回测”系统。为了提高速度,建议不要在此处重新估计参数,而是使用固定参数;可以通过
- 调度(Scheduling):
- 初始化随附的 crontab 配置。注意,如果你的代码或 echo 文件放在其它目录,需要修改 crontab 顶部的目录引用。
- 所有由 crontab 执行的脚本都必须具有可执行权限,因此请执行:
cd $SCRIPT_PATH;sudo chmod +x *.*。
- 考虑添加仓位与交易限额。
- 通读并审查可用的配置选项。
在开始交易之前,以及每次重启机器之后,你都应该:
- 检查 MongoDB server 是否以正确的数据目录运行,命令:
mongod --dbpath $MONGO_DATA(随附的 crontab 应该会自动执行这一点) - 启动一个 IB gateway(根据你的安全设置,也可以自动启动)
- 确认所有进程都已标记为 “close” 状态
注意:除非你手动启动那些原本由 crontab 或其他调度器触发的进程,否则系统要到第二天才会真正开始交易。如果你希望在不保持终端窗口打开的情况下启动进程(例如在无头服务器上),可以使用 Linux screen。
另外,请参考这一节,了解系统崩溃后的恢复(这里指交易系统崩溃,而不是市场崩盘——后者就只能靠你自己了)。
在实际交易时,你需要定期执行以下任务:
生产系统数据流
下面概述生产系统中的主要数据流和处理步骤:
- 输入:IB FX prices(IB 外汇价格)
- 输出:Spot FX prices(即期外汇价格)
- 输入:人工判断决策,现有 multiple price series(多价格序列)
- 输出:当前一组 active contracts(在市合约)的价格、carry 和 forward 信息;以及隐含在更新后 multiple price series 中的换月日历(roll calendar)
- 输入:multiple price series 中隐含的当前 active contracts 的价格、carry 和 forward 信息
- 输出:需要被历史数据模块采样的合约集合
- 输入:要采样的合约列表、IB futures prices(IB 期货价格)
- 输出:逐合约的 futures prices(期货价格序列)
Update multiple adjusted prices
- 输入:逐合约期货价格、现有 multiple price series、现有 adjusted price series(调整后价格序列)
- 输出:更新后的 adjusted price series 和 multiple price series
- 输入:来自 IB 的 brokerage account value(券商账户总价值)
- 输出:Total capital(总资本)、账户层面的 P&L(盈亏)
- 输入:Total capital(总资本)
- 输出:按策略分配后的 strategy capital(每个策略的资本分配)
- 输入:按策略分配的资本、adjusted futures prices、multiple price series、spot FX prices
- 输出:每个策略的 optimal positions(最优持仓)及缓冲区(buffers),以及序列化(pickled)的回测状态对象
- 输入:每个策略的 optimal positions 和 buffers
- 输出:instrument orders(品种层订单)
- 输入:instrument orders
- 输出:trades(成交记录)、历史订单更新、持仓更新
生产系统概览
下面是搭建一个生产系统需要遵循的主要步骤。假设你已经在 pysystemtrade 中有一个经过回测的系统,并且相关的 Python 库等都已就绪:
- 考虑你的实现方案选项
- 确保有一个用于系统代码与配置的私有区域
- 最终确定并保存你的回测系统配置
- 如果你希望自动执行交易或从券商获取数据,则需要配置一个 broker
- 配置你所需的其他数据源
- 搭建用于存储的数据仓库(例如 MongoDB),并配置备份方案
- 制定报表、诊断与日志策略
- 编写一组脚本,用于启动以下进程:获取数据、获取账户信息、计算最优持仓、执行交易、生成报表、执行备份和日常维护
- 将这些脚本按计划定期调度执行
- 定期监控系统运行状况,并处理出现的各种问题
实现方案选项
pysystemtrade 的标准实现是一套在单台本地机器上运行的全自动系统。本节简要介绍一些你可以考虑的替代方案。
作者自己的实现运行在一台 Linux 机器上,因此本文中的某些细节是 Linux 特有的。欢迎 Windows 和 Mac 用户补充这两类平台上的差异做法。
自动化选项
你可以把 pysystemtrade 跑成一个全自动系统,从抓取价格、计算信号到发送交易指令全部自动完成。
如果采用全自动模式,IBC 会非常有用。
但也存在其他模式是合理的,尤其是你可能希望手动交易:先用系统抓价、生成最优持仓,然后由你手工下单。同样也可以做到“手动交易,但让 pysystemtrade 自动从券商处读取成交”,而不是手工录入成交。例如:
- 你暂时还不完全信任系统(这很正常)
- 你希望有更多人工控制
- 你认为自己的执行优于算法执行
- 你正在做测试
- 或者你使用的券商不提供 API
在这些场景下,可以按如下方式处理:
- 在
private_control_config.yaml中run_stack_handler对应的 process configuration 里,删除方法create_broker_orders_from_contract_orders - 运行
interactive_order_stack,查看系统产生了哪些合约订单(contract orders) - 自己在券商端完成交易
- 在
interactive_order_stack中使用 “manually fill broker or contract order” 功能,录入成交细节
除此之外,其余部分可以继续按正常全自动方式运行。
机器、容器与云环境
pysystemtrade 可以在单机本地以普通方式运行;也可以考虑容器化(参见作者的 Docker 博文),甚至部署在 AWS 或其他云环境上。你也可以把整体实现拆分到多台本地机器上。
如果要拆分到多台机器,需要注意:
Interactive Brokers
- IB Gateway 需要在其白名单中加入所有会连接到它的机器 IP
- 需要在
private_config.yaml的系统配置中修改ib_ipaddress字段,例如:ib_ipaddress: '192.168.0.10'
MongoDB
- 在
/etc/mongod.conf文件中的bind_ip行加入允许访问的 IP,例如:bind_ip=localhost, 192.168.0.10 - 可能需要调整防火墙设置:例如使用 UFW(
sudo ufw enable,sudo ufw allow 27017 from 192.168.0.10)或 iptables - 在
private_config.yaml中修改mongo_host字段,使之指向新的 MongoDB 主机,例如:mongo_host: 192.168.0.13 - 视情况实现更多安全控制(可参考 MongoDB 官方安全检查清单)
- 在
- 在私有 YAML 配置中,为每个进程指定不同的机器名称,以便调度系统知道要在哪台机器上运行哪个进程
备机
如果你的实现运行在本地机器,或者运行在“非云”的远程服务器上,那么非常有必要准备一台备机(backup machine)。
备机应当:
- 拥有与主机一致、较新的运行环境:包括应用、代码和依赖库
- 定期同步主机上的本地数据(见数据备份 部分)
备机不一定要一直开机,除非你的交易方式决定“一小时不交易”也是不可接受的风险。在作者的经验中,只要代码保持最新、数据不超过 24 小时落后,一般一小时内可以把备机切换上线。
同时,建议定期做“计划内切换(failover)”:
- 在周末这类相对安全的时间关闭当前生产机;
- 把必要的数据拷贝到备机;
- 启动备机作为新的生产机;
- 原生产机则转为新的备机。
多个系统
你可能希望在同一台机器上运行多套交易系统,常见场景包括:
- 运行相对价值(relative value)策略组合 *
- 为不同时间尺度运行不同系统(例如日内系统 + 慢频系统) *
- 为不同资产类别(如股票/ETF 与期货)分别运行不同系统
- 想在不同交易账户上跑同一套系统(pysystemtrade 本身不支持多账户)
- 同时运行一套纸面交易(paper trading)系统和一套真实交易系统
- 对于其中若干场景,作者计划在 future 版本中在同一系统内直接支持。
一个可行的做法是:为每个系统准备一份独立的 pysystemtrade 环境拷贝。它们可以共用一个 crontab,但需要为每个系统准备各自的 scripts 目录、echos 目录以及其它工作目录。
同时需要:
- 在每个系统的 private config 中,配置不同的 MongoDB 数据库名;
- 如果不希望重复存储某些数据(例如价格),可以在相应代码中直接写死
database_name,例如:mongo_db = mongoDb(database_name='whatever')。
关于数据存储的更多细节,可以参考“存储期货与即期外汇数据”文档。
最后,还需要在 private/private_config.yaml 中设置 ib_idoffset 字段,避免不同系统之间出现重复的 IB clientid 连接。例如,一个系统设置偏移 1,另一个设置偏移 1000,如此即可基本避免冲突。
代码与配置管理
你的交易策略由三部分组成:pysystemtrade 主代码、特定的配置文件,以及(可选的)自定义代码。可以用两种方式组织这些内容:
- 在一个单独环境中开发,把 pysystemtrade 当作普通依赖库引入;
- 所有内容都放在 pysystemtrade 目录下,把所有自定义代码和配置都放在被 git 忽略的
private目录下。
作者个人更偏好第二种方式,因为这样更像是一个“自包含”的系统,但选择权在你。
管理分离的代码与配置目录
强烈建议使用代码仓库(如 git)来管理非 pysystemtrade 部分的代码和配置。由于代码与配置主要是文本或类文本(YAML 文件等),git 非常适合做版本管理。
不建议把配置直接存放在需要单独备份的数据库文件中,因为这样会让“归档并按需恢复旧配置”变得更复杂。
管理 private 目录
private 目录被排除在主仓库的 git 版本控制之外(以避免出现在公共 GitHub 仓库中),因此需要单独管理。作者自己为 private 内容单独建了一个仓库,在本地挂载在 ~/private 目录下。现在 GitHub 也提供免费的私有仓库,这也是一个选择。
在此基础上,他使用一个 bash 脚本,来替代常规的 git add / commit / push 流程,同时对公共代码和私有代码进行提交:
| |
第二个脚本用来替代普通的 git pull:
| |
自定义 private 目录位置
如果你希望把 private 配置放在 pysystemtrade 目录结构之外,也完全可行。可以通过设置环境变量 PYSYS_PRIVATE_CONFIG_DIR 来指定 private 目录的完整路径,例如:
| |
如果只想在某个脚本的上下文中临时指定自定义目录,可以:
| |
最终确定回测配置
理论上,你可以每天重新跑一遍完整回测来生成持仓。但这通常意味着你会不断重新拟合诸如品种权重、forecast scalars 等参数——这是没必要的、缓慢的、浪费时间的,甚至是危险的。
在实盘系统中,更推荐对所有拟合参数使用固定值。
下面这个便捷函数可以从你已有的回测系统中提取估计好的参数,并生成一个包含所有“固定参数值”的字典:
| |
可以根据需要调整 attr_names 列表,控制要导出的参数范围。随后可以把生成的 YAML 文件合并到你的生产回测 YAML 配置中。
不要忘记关闭以下布尔标志:use_forecast_div_mult_estimates、use_forecast_scale_estimates、use_forecast_weight_estimates、use_instrument_div_mult_estimates 和 use_instrument_weight_estimates。forecast_mapping 的相关标志无需更改,因为默认不会对其做估计。
连接券商
实盘中,你很可能希望把系统连接到券商,以实现以下一种或多种功能:
- 获取行情价格
- 获取账户价值和盈亏情况
- 执行交易指令
- 获取成交(fills)
当然,其中一部分也可以手工完成。
你现在应该阅读文档:connecting pysystemtrade to interactive brokers。
在 private 配置文件中,需要设置好 broker_account、ib_ipaddress、ib_port 和 ib_idoffset 这些字段。
其他数据源
你可以把所有数据都从券商获取,但通常也有理由额外接入其他数据源:
- 多个数据源可以提高数据准确性
- 多个数据源可以在某个行情源故障时提供冗余
- 某些数据券商不提供
- 某些数据在其他地方更便宜
你可以阅读 getting and storing futures and spot FX data,其中有关于为其他数据源编写 API 封装层的一些提示。
数据存储
pysystemtrade 生产系统会使用多种形式的数据文件,大致可以分为以下类别:
- 账户与盈亏(accounting,P&L 计算)
- 诊断数据(diagnostics)
- 价格数据(详见 存储期货与即期外汇数据)
- 持仓数据(positions)
- 其他状态与控制信息(state & control information)
- 静态配置文件(YAML、CSV 等)
默认情况下,除配置文件外的其它数据都存入 MongoDB 数据库;配置文件则以 YAML 和 CSV 形式存储。时间序列数据使用 Parquet 文件保存。
实际使用的数据库名字由 private 配置中参数 mongo_db 的取值决定。
Arctic
在项目早期版本中,时间序列数据曾使用 Arctic 存储,而不是 Parquet;该方案现在已被废弃。更多细节可以参考回测指南。
在生产环境中使用 Arctic 替代 Parquet
如果你仍想使用旧方案,也不是不可以。在生产环境中,可以修改 sysproduction.data.production_data_objects.py 中的 use_production_classes 映射,让相关数据类型指向 Arctic 类而不是 Parquet 类,例如:
| |
同时,还需要取消相关 Arctic 类的 import 注释。
数据备份
MongoDB 数据
假设你使用默认的 MongoDB 来存储数据,推荐每天使用 mongodump 做一次备份。也可以根据需要采用更复杂的备份方式(参见 MongoDB 官方备份文档)。在迁移数据到新机器时也同样适用。
为避免冲突,备份应安排在系统的“deadtime”(系统不交易的时间段)进行,具体调度可参考调度章节。
Linux 示例:
| |
上述操作既可以由计划任务(见调度)完成,也可以通过文档中 backup_mongodb_dump 所述脚本执行。
恢复示例(Linux 命令行):
| |
Parquet 数据
以 Parquet 文件存储的时间序列数据,可以像普通文件一样备份,例如:
| |
在默认配置中,这一步也包含在定期备份流程内,详见文档中的 backup_parquet。
MongoDB / CSV 数据
作者本人比较“偏执”,还会把所有 MongoDB 中的数据导出为 CSV 文件,并定期备份。这样即便 MongoDB 文件损坏,也可以通过 CSV 数据完成系统恢复。
当前支持导出为 CSV 的内容包括:FX 价格、单个期货合约价格、multiple prices、adjusted prices、持仓数据、历史成交、资本、合约元数据、spread costs、optimal positions 等。
另外,数据库中还存储了一些与交易和进程控制相关的状态信息,这部分在仅依赖 CSV 备份时会丢失,但一般可以通过一定工作量恢复,例如:换月状态(roll status)、交易限额(trade limits)、仓位限额(position limits)以及 overrides 等。日志数据也会丢失,不过仍可以根据归档的 echo 文件 进行检索。
Linux 脚本示例:
| |
Echos、日志、诊断与报告
尤其在系统完全自动化运行时,我们必须清楚系统当前在做什么。通常需要通过以下方式实现:
- 将各个进程的标准输出(stdout)重定向到 echo 文件
- 将日志输出写入带有标记(tag)的日志文件,便于识别
- 将诊断信息按键(key)存入数据库
- 提供定时与临时(ad-hoc)两类报表,并可以选择自动通过邮件发送
Echos:stdout 输出
随附的 crontab 配置 中包含类似如下的行:
| |
上述行会运行脚本 updatefxprices,但其输出不会写到终端 stdout,而是写入 updatefxprices.txt。
这些 echo 文件在进程崩溃时尤其有用,你可以在其中查看堆栈信息;但在多数情况下,常规日志文件会更好用。
清理旧 echo 文件
随着时间推移,echo 文件会变得非常大(作者日志默认非常详细)。
为避免文件无限膨胀,系统提供了一个每日清理流程:
- 将旧 echo 文件归档,并带上日期后缀;
- 删除超过一个月的历史 echo 文件。
注意:如果你不用 .txt 作为扩展名,需要在 private_config.yaml 中修改配置变量 echo_extension,否则清理脚本无法正确识别文件。
日志(Logging)
pysystemtrade 使用 Python logging 模块。
有关模拟(sim)模式下 logging 的更多细节,见回测文档中的 logging 部分。
Python logging 功能强大、可配置性高,你可以通过自定义配置,让日志以任意格式输出到几乎任何位置(参见官方文档中的 logging 高级教程)。
本节则描述默认提供的“生产环境”日志配置。
在生产环境中,需求比回测模拟更复杂。除了模拟中已有的“上下文属性”(context attributes)外,还需要:
- 支持多个进程写入同一个日志文件
- 对 echo 文件输出到控制台(console)
- 在出现 CRITICAL 级别的日志时触发邮件通知
可以通过设置以下环境变量启用默认的生产日志配置:
| |
在客户端(pysystemtrade 端)有三个 handler:socket、console 和 email。
其中 socket handler 依赖一个单独进程的日志服务器。下面分别介绍。
socket
Python 本身不支持多个进程同时向同一个文件安全写入日志。
因此在客户端,日志消息会被序列化,通过网络发送到日志服务器;服务器端是一个简单的 TCP socket server,用于接收、反序列化并将日志写入磁盘。
使用前,需要先启动 socket server。最简单的方式是:
| |
但这样会把日志写到当前工作目录,通常并不是你想要的。更合理的方式是显式指定日志文件路径:
| |
默认情况下,server 监听 6020 端口;如果你想使用其他端口,可以:
| |
socket server 还负责按天轮转日志文件:默认每天午夜生成一个新的日志文件,并保留最近 5 天的日志。因此在运行一周后,日志目录大致会长这样:
| |
日志服务器需要长期运行:在后台运行、重启后自动启动、异常退出后能自动重启等。
因此更好的方式是把它做成一个系统服务。
将 socket server 作为系统服务运行
仓库中提供了一个 Linux systemd service 的示例,见 examples/logging/logging_server.service,并在这篇指南中说明了基本设置步骤。
在 Debian/Ubuntu 上的基本配置流程:
- 在
/etc/systemd/system/logging_server.service新建一个文件 - 将示例 service 文件的内容粘贴进去
- 根据实际情况更新
ExecStart中的路径;如果使用 virtualenv,要确保 Python 路径正确 - 更新
User和Group字段,避免日志文件由 root 拥有 - 如果使用自定义 private 配置目录,更新
Environment中的路径 - 然后用下面的命令来重载、启用、查看状态或启动/停止/重启服务:
| |
console
和回测模式类似,所有日志消息也会被发送到控制台。
因此,前面提到的 crontab 条目在运行时,其输出也会被重定向到 echo 文件中。
系统提供了一个专用的 SMTP handler,仅用于发送 CRITICAL 级别的日志消息。
该 handler 会使用 pysystemtrade 配置中的邮件设置,将这些消息以邮件形式发出。
在你自己的代码中添加日志
使用示例可以参考 Python logging 文档。
在 pysystemtrade 的封装中,有四种方式合并“上下文属性”(context attributes):
- overwrite:将新传入的属性与现有属性合并,遇到重复 key 时覆盖旧值(默认行为)
- preserve:将新传入的属性与现有属性合并,遇到重复 key 时保留旧值
- clear:先清空已有属性,再加入本次传入的属性
- temp:本次传入的属性只在这一条日志中生效(临时覆盖),下一条日志恢复原值
示例
| |
清理旧日志
Echos
项目中提供了清理 echo 文件的代码,该代码会由每日日志清理流程自动调用。
Python 示例:
| |
默认会删除 30 天之前的 echo 文件。
日志文件
在默认的生产日志配置下,log 文件的清理由 Python 日志服务器负责:
默认只保留最近 5 天的生产环境日志。如需调整保留策略,可查看并修改 syslogging/server.py。
psysystemtrade 报表
系统会定期运行报表,用于帮助你监控系统状态并决定是否需要人工干预。
你也可以选择将报表通过邮件发送。为此,必须在 private_config.yaml 中配置好发件邮箱、密码、SMTP 服务器和收件地址(收件人可以与发件人相同),例如:
| |
pysystemtrade 在连接 SMTP 服务器时,会自动尝试协商使用 TLS 加密;只有在 TLS 无法使用时才会退回到明文通信。
如果你打算通过 Google 的 SMTP 服务器发邮件,又不想在配置文件中保存明文密码,可以为 pysystemtrade 创建一个专门的 “App password”:
- 打开 Google 帐号安全设置;
- 在 “登录 Google” 区域点击“两步验证(2-Step Verification)”,进入详情页;
- 在该页底部找到“应用专用密码(App passwords)”,点击进入,或者在登录状态下直接访问:https://myaccount.google.com/apppasswords;
- 确认已启用“两步验证(2-step verification)”;
- 在 “App passwords” 页面中生成新密码:
- Select app 选择:Mail
- Select device 选择:Other,并命名为
pysystemtrade - 点击 “Generate”,复制生成的 16 位密码(例如
abcd efgh ijkl mnop)
随后,将配置修改为使用这个 app password,例如:
| |
报表会由 run reports 进程每天自动运行,你也可以在 interactive diagnostics 工具中临时(ad-hoc)运行报表;临时报表既可以以邮件形式发送,也可以直接在屏幕上显示。
关于各类报表的详细说明,见这里。
Scripts
Scripts 用于执行一系列 Python 代码,主要做以下事情:
- 运行交易系统的不同部分,例如:
- 获取价格数据
- 获取 FX 数据
- 计算持仓
- 执行交易
- 获取账户和记账数据
- 修复问题或以交互方式“手动干预”系统
- 定期或临时运行报表与诊断
- 执行日常维护任务,例如截断日志文件、执行备份等
Scripts 通常由调度器调用,也可以在命令行中按需手动执行。
Script 调用方式
作者提供的脚本是在 Linux 下运行的,但它们最终都只是调用简单的 Python 函数,因此在其他操作系统上编写等价脚本应该很容易。
关于如何创建跨平台可执行脚本的一个方法,参见这里。
例如,运行报表的脚本 如下:
| |
用白话说,这段脚本会调用 /sysproduction/run_reports.py 中的 Python 函数 run_reports()。
按约定,所有“顶层”Python 函数都应放在该目录中,并且文件名、脚本名和顶层函数名应保持一致。
这些脚本通过一个 Linux 便捷脚本运行,它只是用单个参数(即函数代码路径)调用 run.py:
| |
run.py 稍微复杂一些,它允许调用带参数的 Python 函数,比如 interactive_update_roll_status,并在运行时向用户询问这些参数(带类型提示)。
Script 命名约定
脚本前缀采用如下规则:
_backup:执行备份任务_clean:执行日常维护 / 清理任务_interactive:运行交互式流程,用于检查或修复系统,避免每次出问题都要手动进 Python_update:更新系统中的数据(相当于执行系统的某个阶段)startup:在机器启动时执行_run:运行定期调度的进程
通常,你既可以按需直接调用某个 process(例如 _backup_files),也可以让它通过一个“run” 进程定期调用(例如 run_backups,会依次运行所有备份脚本)。
各类 “run” 进程本身稍微复杂一些,因为作者写了一套自己的调度代码,详情见 pysystemtrade 调度部分。
例外情况包括:
- 一些交互式脚本只在被显式调用时运行;
run_stack_handler没有单独的脚本,而是直接通过 run 进程运行。
Run processes
下面列出各类 run process,便于查阅,具体细节会在各自脚本所在章节展开说明:
run_backups:依次运行run_capital_updates:依次运行run_cleaners:依次运行run_daily_price_updates:依次运行run_daily_fx_and_contract_updates:依次运行run_daily_update_multiple_adjusted_prices:运行- update_multiple_adjusted_prices,日度更新多价格与调整后价格
run_reports:运行所有报表run_systems:运行 update_system_backtests,即执行回测以确定需要的最优仓位run_strategy_order_generator:运行 update_strategy_orders,基于run_systems的输出生成交易- run_stack_handler:执行由
run_strategy_order_generator放到栈上的订单
核心生产系统组件(Core production system components)
本节介绍系统核心功能对应的组件。
从 IB 获取 spot FX 数据并写入 Parquet(每日)
Python:
| |
Linux 脚本:
| |
被 run_daily_fx_and_contract_updates 调用。
该流程会检查 FX 汇率中的“spikes”(异常大幅变动),包括“新数据与历史数据的对比”和“新数据内部自身的变化”。
- 若发现某个合约的数据中存在 spike,则该合约数据不会被写入;
- 同时系统会尝试通过邮件通知用户;
- 用户随后需要手动检查 FX 数据。
spike 阈值由 defaults.yaml 中的 max_price_spike 参数(或 private config 覆盖)控制。
spike 被定义为“价格的绝对日变动的若干倍”:
例如,若某价格通常每天变动 0.5 单位,max_price_spike=6,则单日变动超过 3 单位就会被视为 spike。
更新 sampled contracts(每日)
该流程确保当前正在采样的是 active 合约,并更新合约到期日。
Python:
| |
Linux 脚本:
| |
被 run_daily_fx_and_contract_updates 调用。
更新期货合约历史价格数据(每日)
该流程从 IB 获取所有在 MongoDB contracts 数据库中标记为“需要采样”的期货合约的历史日度数据,并将其写入 Parquet。
如果还未运行“update sampled contracts”,那么可能不会为所有你需要的合约获取数据。
Python:
| |
Linux 脚本:
| |
被 run_daily_price_updates 调用。
该流程会获取日收盘价,并按 defaults.yaml 中参数 intraday_frequency 指定的频率获取日内数据(private YAML 可以覆盖)。默认 'H' 表示按小时采样。
价格会分别存为 Daily、Hourly 和 Merged 三种频率。
同时会按 YAML 配置(括号内为默认值)对数据做如下清洗:
- 若
ignore_future_prices=True(默认 True),则忽略时间戳在未来的价格(假设所有时间都已转换为本地时间),以避免过早填入亚洲时区的收盘价; - 若
ignore_prices_with_zero_volumes=True(默认 True),则忽略成交量为零的 bar,减少坏数据; - 若
ignore_zero_prices=True(默认 True),则忽略价格恰为零的记录(通常是错误数据); - 若
ignore_negative_prices=True(默认 False),则忽略负价格。考虑到 2020 年 3 月原油价格暴跌事件,作者倾向于允许负价格,并希望 spike 检查能捕捉到其中错误。
此外,该流程也会检查价格中的 “spikes”:
- 即在“新旧数据对比”或“新数据内部”出现异常大变动时;
- 一旦在某合约数据中发现 spike,就不会写入该合约的数据;
- 系统会尝试通过邮件通知用户;
- 用户随后需要手动检查期货历史价格数据。
spike 阈值同样由 defaults.yaml 或 private config 中的 max_price_spike 控制。
例如,若某价格通常每天变化 0.5 单位,max_price_spike=8,则单日变化超过 4 单位会触发 spike。
关于行情订阅(market data subscriptions)的说明
- 过去 IB 获取历史数据需要行情订阅,但在 2023 年初这一点发生了变化;
- 该变更没有官方公告,API 文档也仍声称需要订阅,因此未来可能仍会调整;
- IB 有多种行情数据:
- 历史数据;
- 流式数据(live 或 delayed)。
- 实时流式数据需要订阅,延迟流式数据则不需要。
- pysystemtrade 默认使用流式数据来管理交易:
- 如果你没有某个 instrument 的实时流式数据,需要切换到不依赖实时流式数据的执行算法;
- 可在
sysexecution.orders.broker_orders.py中查看支持的订单类型,在sysexecution.algos中查看可用的交易算法; - 然后在配置中重写
execution_algos以匹配你的实际行情订阅。
- 作者自己的行情订阅在这篇博文中有详细说明,这里摘录如下:
| Name | Cost per month |\n\t|:—————————:|:————–:|\n\t| Cboe One | USD 1.00 |\n\t| CFE Enhanced | USD 4.50 | \n\t| Eurex Core | EUR 8.75 | \n\t| Eurex Retail Europe | EUR 2.00 | \n\t| Euronext Data Bundle | EUR 3.00 | \n\t| Korea Stock Exchange | USD 2.00 | \n\t| Singapore Exchange | SGD 2.00 |\n\t| Osaka Exchange | JPY 200 |\n\t
最新详情和价格请参考 Interactive Brokers 官方页面。
为不同区域设置下载时间
为提高效率,你可能不希望在每天结束时一次性下载全部数据,而是按区域分时段下载。
可以在 private_control_config.yaml 中添加如下配置:
| |
这样系统会:
- 在本地时间早上 7 点下载亚洲区域的合约数据;
- 在 18 点下载欧洲 / 中东 / 非洲(EMEA)区域的数据;
- 在 20 点下载美国区域的数据。
各区域归属在 instrument 配置中定义(通过 CSV 文件提供,再通过 interactive_controls 的“更新配置”功能写入数据库)。
同时,还需要确保:
- 在
private_control_config.yaml中为run_daily_price_updates设置一个早于早上 7 点的启动时间; - 并由 crontab 或其他调度器在 7 点之前启动该进程。
更新 multiple 与 adjusted prices(每日)
该流程会使用最新的“逐合约期货价格数据”,来更新 multiple prices 和 adjusted prices。
一般应安排在单个合约的日度价格更新完成后再运行,但也可以单独调度执行(不依赖前置过程),以避免受单个合约下载缓慢的影响。
Python:
| |
Linux 脚本:
| |
由 run_daily_update_multiple_adjusted_prices 调用。
不会对 multiple prices 和 adjusted prices 做 spike 检查,因为在正常情况下,如果逐合约价格干净,则它们应该是“自动干净”的。
通过轮询券商账户更新资本与 P&L
关于资本如何运作,见 capital 一节。
我们需要每天检查券商账户价值的变化,用于更新系统总可用资本,并将其分配给各个策略。
Python:
| |
Linux 脚本:
| |
由 run_capital_update 调用。
如果券商账户价值变动超过 10%,系统不会直接调整资本,而是向你发送一封邮件。
此时你需要运行 modify_account_values:
- 该流程会再次轮询券商账户数值;
- 并让你确认这次大幅变动是否真实;
- 在确认后,下次
update_total_capital运行时将不会再视其为异常,而是正常进行调整。
按策略分配资本
该流程将总资本分配给各个策略,更多细节见 strategy capital。
如果尚未运行过 update_total_capital,或者资本是通过 update_capital_manual 手工初始化的,则本流程无法正常工作。
Python:
| |
Linux 脚本:
| |
由 run_capital_update 调用。
夜间运行回测系统,更新一个或多个策略
(通常在夜间执行)
pysystemtrade 的基本理念是:每天夜间跑一次新的回测,生成一组参数,供第二天的交易引擎使用。
对于核心代码定义的基础系统来说,这些参数是每个 instrument 对应的一对“仓位缓冲区(position buffers)”,交易引擎只在当前仓位位于缓冲区之外时才交易。
这一范式可以很容易适配其他类型的交易系统。例如:
- 对于均值回复系统:夜间回测可以输出价格区间上下边界作为目标价位;
- 对于日内系统:夜间回测可以输出目标仓位大小以及入场 / 出场点;
这样可以显著减少白天交易引擎的计算量。
Python:
| |
Linux 脚本:
| |
由 run_systems 调用。
用于运行各策略回测的代码由 control_config.yaml(可被 private_control_config.yaml 覆盖)中的配置项指定:
- 路径:
process_configuration_methods/run_systems/strategy_name/。
示例:
| |
其中:
object:负责运行回测的类,例如sysproduction.strategy_code.run_system_classic.runSystemClassic;- 该类必须提供
run_system方法(只要继承自runSystem即可)。
- 该类必须提供
max_executions:每次运行run_systems时最多调用该策略回测的次数。- 常规情况下为 1;
- 若为日内系统,也许可设为更大。
为每个策略生成订单
该流程会根据 nightly 回测的结果(optimal positions 等)生成实际要下单的交易(instrument 订单),详见策略订单生成器。
Python:
| |
Linux 脚本:
| |
由 run_strategy_order_generator 调用。
用于生成每个策略订单的代码,同样在 control_config.yaml(或 private_control_config.yaml)中配置,路径为:
process_configuration_methods/run_strategy_order_generator/strategy_name/。
示例:
| |
其中:
object:生成订单的类,例如sysexecution.strategies.classic_buffered_positions.orderGeneratorForBufferedPositions;- 该类必须提供
get_and_place_orders方法(只要继承自orderGeneratorForStrategy即可)。
- 该类必须提供
以下可选参数仅被 run_strategy_order_generator 使用:
max_executions:在每次run_systems执行时,该生成器最多运行几次。通常为 1,除非你有非常“奇葩”的日内系统(可省略)。frequency:生成器的运行频率(分钟)。默认 60(仅在max_executions>1时有意义,可省略)。
更多细节参见 system order generator 以及 调度配置 部分。
执行订单(Execute orders)
当 instrument 栈上已有订单(由 order generator 放入)时,我们需要执行它们。这由 stack handler 完成,它负责管理三层订单栈:
- instrument 栈
- contract 栈
- broker 栈
Python:
| |
Linux 脚本:
| |
stack handler 仅以 run process 形式存在,设计上是“全天运行”的。
其行为相当复杂(在阅读本节前,建议先回顾一遍仓位与订单层级)。
在正常情况下,一笔订单会经历如下路径:
- 由策略订单生成器创建 instrument 订单;
- 从 instrument 订单生成 contract 订单;
- 从 contract 订单生成 broker 订单,并提交给券商;
- 管理订单执行(技术上由执行 algo 完成,但由 stack handler 调用),记录从券商返回的成交;
- 自下而上回传 fills:
- 当 broker 订单成交时,相应 contract 订单应反映这一成交;
- 当 contract 订单成交时,相应 instrument 订单应反映这一成交;
- 在收到成交时更新仓位表;
- 处理完成的订单(完全成交):
- 将其从订单栈中删除;
- 在删除前先复制到历史订单表。
此外,stack handler 还会:
- 检查券商与数据库在 contract 层的仓位是否一致;
- 若不一致,则锁定该 instrument,禁止交易;
- 当仓位再次对上时可以自动解锁,也可以通过 interactive_order_stack 手工解锁。
- 当某 instrument 的 roll status 为 Force、Force Outright 或 Close 时,根据状态生成相应订单;
- 在每日结束或进程停止时,安全地清空订单栈:
- 取消所有现有订单;
- 将它们从订单栈中删除。
由于 stack handler 的职责非常多,因此需要借助 interactive_order_stack 来监控与调试。
最后,stack handler 还会定期采样所有 instrument 的买卖价差(bid/ask spread),这些数据会用于成本分析,详见相关报表 部分。
交互式脚本:修改数据
手工检查期货合约历史价格数据
(按需运行)
当正常价格采集流程发现“spike”(异常价格跳变)时,你应该运行这些脚本(如果配置了邮件通知,会收到提醒)。
Python:
| |
Linux 脚本:
| |
该脚本会从 IB 拉取新数据,同时读取已有历史数据。
整体行为与 update_historical_prices 类似,但你可以在开始之前交互式修改“价格过滤配置(price filter config)”。默认配置为:
| |
(如果你在自己的配置中覆盖了这些参数,默认值可能不同。)
然后脚本会检查是否存在 spikes:
- 如果某个合约数据中发现 spike,系统会交互式询问你:
- (a) 接受该异常价格;
- (b) 用前一时间段的价格替代;
- (c) 手工输入一个价格值。
在决定之前,你应该去其它数据源核对该 spike 是否真实:
- 若为真实价格变动,则选择接受;
- 否则应手工输入正确的值。
- 使用前一时间段价格只适用于你“基本确认该变动不真实、且又没有可靠数据源”的情况。
如果你手动输入了新价格,该价格也会再经过一次 spike 检查,以避免“手滑输入”的错误。
因此你可能会被再次问及“是否接受刚刚手工输入的价格”。
已经被接受或采用前值的价格不会再做 spike 检查。
spike 只会在每个 bar 的“最终价格(FINAL price)”上检查,且用户只可以修改最终价格。
如果最终价格被修改,OPEN / HIGH / LOW 也会同步调整:
- 它们会加上或减去与最终价格相同的调整量。
系统目前并未使用 OHLC,但你需要意识到这可能引入一定误差。
如果价格被修正,成交量(VOLUME)不会被改变。
对某个合约完成全部 spike 检查后,修正后的数据会写入数据库,然后脚本会进入下一合约。
手工检查 FX 价格数据
(按需运行)
当 FX 正常价格采集流程发现 spike 时,你也应该运行这些脚本(同样可以配合邮件通知)。
Python:
| |
Linux 脚本:
| |
具体逻辑与期货合约价格手工检查基本相同;
不同之处在于 FX 数据是一条单一时间序列,因此无需对其他字段做额外调整。
交互式修改资本值
Python:
| |
Linux 脚本:
| |
关于资本机制本身,见 capital。
该交互函数用于在下列场景中控制总资本分配:
- 你希望初始化账户中“可用总资本”:
- 若不手动初始化,则第一次运行
update_total_capital时会用默认值自动初始化:- 券商账户价值 = 可用总资本 = 最大可用资本(即从历史高水位 HWM 开始),累计利润 = 0;
- 如果你不认同这些默认值,可以通过本函数以其他方式初始化。
- 若不手动初始化,则第一次运行
- 你在券商账户中进行了取款或存款:
- 否则这些变动会被视为“资本变化”,导致表面上的可用资本减少;
- 需要通过本函数把它们从“业绩变化”中剔除。
- 券商账户价值发生了较大变化:
- 过滤器将其标记为潜在错误,需要你手工确认是否属实。
- 你希望删除某一段时间内的资本记录(例如漏记了一笔取款,导致资本序列被污染)。
- 你希望删除所有资本记录(然后重新初始化):
- 例如之前一直在跑测试账户,现在准备切换到真实生产。
- 你希望对一条或多条资本记录做其它修改——只有在你完全明白自己在做什么时才这么做。
交互式 roll 调整价格
(按需运行)
该流程允许你修改 roll 状态,并实现从当前 priced 合约到下一合约的 roll。
Python:
| |
Linux 脚本:
| |
该脚本有四种运行模式:
- 手动输入 instrument 代码,并手动决定何时 roll;
- 自动遍历 instrument 代码,但手动决定何时 roll;
- 自动遍历 instrument 代码,自动判断是否 roll,但由用户手动确认 roll;
- 自动遍历 instrument 代码,自动判断是否 roll,并自动执行 roll。
手动输入 instrument 代码,并手动决定何时 roll
你先输入想要考虑 roll 的 instrument code。
流程首先会生成并打印一份 roll 报告。
关于如何理解该报告,见 roll report 部分。
随后,你可以在多种 roll 模式之间切换,
但并非所有模式都一定会被允许,具体取决于:
- 当前持仓情况;
- 当前 roll 状态。
可选的 roll 状态包括:
No roll:不 roll,很直观;Passive:在当前 priced 合约上策略性减仓,并在 forward 合约上开同等方向的新仓;Force:暂停该 instrument 所有正常交易,由 stack handler 创建 calendar spread 交易,把仓位从 priced 合约滚到 forward 合约;Force outright:暂停所有正常交易,创建两笔单边交易:- 平掉 priced 合约仓位;
- 在 forward 合约上开出相应仓位;
Roll adjusted:仅在当前 priced 合约没有持仓时才允许:- 会重新生成 adjusted prices 与 multiple prices;
- 新的 forward 合约会成为新的 priced 合约(其余全部顺延);
- 写入数据库前会让你确认调整后的价格。
No open:由于临近到期,且 forward 合约流动性不足,因此不再开新仓;Close:只平掉近月合约上的仓位。
更新 roll 状态后,你可以选择继续处理下一只 instrument,或者退出。
注意:
如果系统无法找到当前合约与 forward 合约“对齐”的价格点,adjusted price 的 rolling 将失败。
此时你可以选择对 forward 合约价格进行前向填补(forward fill):
- 这会降低 roll 的精确度,但至少可以让该 instrument 继续留在系统中。
自动遍历 instrument 代码,但手动决定何时 roll
该模式会自动挑选一组“临近到期”的 instruments。
脚本会询问你要向前看多少天的到期(days ahead),再根据该窗口挑选合约。
之后的逻辑与“手动输入 instrument 代码”完全一致,只是会自动在这组 instrument 中循环。
自动遍历 instrument 代码,自动判断是否 roll,并手动确认
同样会先选出一组“临近到期”的 instruments。
然后脚本会给出默认的自动 roll 参数,并允许你调整它们。默认配置为:
| |
随后行为取决于你设定的参数:
- 如果 forward 合约的成交量小于所需的相对成交量阈值,则什么也不做;
- 如果相对成交量达标,且在 priced 合约上没有持仓:
- 则自动决定进行 adjusted price 的 rolling;
- 如果相对成交量达标、priced 合约上有持仓,且你选择“每次都手动输入状态”:
- 则对每个 instrument 都会交互式询问 roll 状态;
- 如果相对成交量达标、priced 合约上有持仓,且你没有选择手动输入状态:
- 则会自动将状态设为
Passive、Force或Force Outright中的一种(由配置决定); - 作者强烈建议在这里使用
Passive模式,然后在必要时对个别 instrument 手动调整。
- 则会自动将状态设为
如果决定执行 adjusted price 的 rolling,写入数据库前会要求你确认对价格的修改是否满意。
建议在运行完毕后再生成一份 roll 报告,以检查当前整体状态。
自动遍历 instrument 代码,自动判断是否 roll,并自动执行
这一模式与上一种完全相同,唯一差别是:
- 一旦决定执行 adjusted price rolling,就会自动写入数据库,不再询问用户确认。
菜单驱动的交互式脚本
其余交互脚本允许你查看和控制大量系统状态,因此采用“菜单驱动”的交互界面。
主要有三个脚本:
interactive_controls:交易限额、仓位限额、进程控制与监控;interactive_diagnostics:查看回测对象、生成 ad hoc 报表、查看日志/邮件与错误;查看价格、资本、仓位与订单、配置等;interactive_order_stack:查看订单栈与仓位、创建订单、净额 / 取消订单、锁定/解锁 instruments、删除并清理订单栈。
菜单是嵌套结构,常见模式是:
- 回车(return)返回上一级菜单;
- 或退出到上一层。
Interactive controls
这个工具用于控制系统行为,包括运行控制和操作风险控制。
Python:
| |
Linux 脚本:
| |
Trade limits(交易次数限额)
你可以为“在给定时间段内的最大交易次数”设置限额,限额可以针对:
- 某个 instrument;
- 某个 instrument + 某个 strategy。
这些限额会在 run_stack_handler 中生效:
- 当系统试图从 contract 订单生成 broker 订单时,就会应用 trade limits。
可选操作包括:
- 查看限额(View limits);
- 修改限额(Change limits):按 instrument 或 instrument+strategy;
- 重置限额(Reset limits):按 instrument 或 instrument+strategy;
- 当你已经达到限额但仍想继续交易,又不想简单把上限调高时很有用;
- 自动填充限额(Autopopulate limits)。
自动填充会根据当前风险水平估算合适的 trade limit:
- 风险越高,给出的限额越低,反之亦然。
- 在设定限额时会做很多假设:
- 所有策略有相同的风险目标(可配置);
- 所有策略有相同的 IDM(也可修改);
- 所有 instrument 有相同的 instrument weight(可配置);
- 所有 instrument 交易速度相同(可配置“每日交易量占典型仓位的最大比例”)。
- 它不会使用实际的 instrument weights,只依赖这些统一假设。
Position limits(仓位限额)
你可以为多种不同层级的仓位设置限额,包括:
- 按单个 instrument 的总仓位;
- 按 instrument + strategy;
- 按 instrument + account(未来支持多个账户时有用);
- 等等。
Position limits 在整个交易流程中都会被检查,和平时使用的 risk limits 配合工作。
(详细字段与操作选项与 trade limits 类似,这里不再展开。)
Trade control / override(交易控制 / 覆盖)
该菜单允许你对某些 instrument 或策略施加额外控制,例如:
- 暂停某个 instrument 的新开仓;
- 仅允许减仓;
- 为某个 instrument 或 strategy 设定临时禁止交易等。
这些控制会在订单生成与执行前被检查,用于快速降低风险或执行临时限制。
Broker client IDs
该选项用于查看和管理不同进程或系统实例所使用的 IB client ID 分配情况,避免:
- 多个进程使用同一 client ID;
- 与
ib_idoffset配置不一致。
Process control & monitoring(进程控制与监控)
这一块用于查看和控制各个长期运行进程的状态。主要功能包括:
View processes(查看进程)
- 查看所有受控进程的当前状态:
- 是否标记为 RUNNING / STOP / NO RUN;
- 当前 PID;
- 上次启动时间等。
- 注意:有些进程虽然已经被启动,但仍在“等待真正开始”(例如尚未到预定开始时间、或依赖的上个进程尚未启动),
- 这些进程会显示为“未运行”,且没有 PID;
- 这种情况下可以安全地 kill 掉它们。
Change status of process(修改进程状态)
你可以将任意进程的状态修改为 STOP、GO 或 NO RUN:
- 将进程设为
NO RUN:- 进程当前不会被马上停止;
- 但当它结束后不会再次启动;
- 这是“正确的停止方式”,因为会正确更新进程状态,并在必要时(例如 stack handler)执行“优雅退出”。
- 将进程设为
STOP:- 进程会“在当前方法执行完毕后”才真正停止;
- 对于
run_systems和run_strategy_order_generator来说,这意味着会等到当前策略处理完成(可能需要一段时间)。
如果进程拒绝响应 STOP,可以在命令行使用 kill NNNN 作为“最后手段”(NNNN 为 PID):
- 但这样可能导致数据损坏或奇怪行为(尤其是对 stack handler);
- 且你随后必须把该进程标记为 close(见下文)。
将进程状态设为 START 并不会真正启动进程:
- 需要手工启动或等待 crontab 触发;
- 如果进程的前置条件不满足(时间窗口、前置进程未完成等),即使设为
START也不会运行。
Global status change(全局状态修改)
有时你可能希望将所有进程标记为 STOP(紧急停机),或全部标记为 GO(紧急停机后的统一重启)。
Mark as close(标记为 close)
该选项会手动把某个进程标记为 close。
- 正常情况下,进程在正常结束或被 STOP 时会自动设为 close;
- 但如果进程异常终止,状态可能仍显示为 “running”,导致无法启动新的同名进程;
- 此时需先确认该 PID 确实不再运行(例如用
ps aux | grep NNNN检查),再把它标为 close。
注意:
- 标记为 close 并不会停止仍在运行的进程;
- 如果进程还在运行,应先用“Change status”停止之。
启动脚本(startup script)也会将所有进程标记为 close(因为启动时本不应有任何进程在跑);
另外,下一个选项“Mark all dead processes as close”也会自动完成类似操作。
Mark all dead processes as close(标记所有“死进程”为 close)
该选项会检查每个进程对应的 PID 是否仍然存活:
- 若不存活,则将该进程标记为 close(认为该进程已崩溃);
- 若系统监控或 Dashboard(见
/docs/dashboard_and_monitor.md)在运行,也会定期执行类似检查。
View process configuration(查看进程配置)
该选项用于查看每个进程的配置来源:
- 可以选择查看
control_config.yaml或private_control_config.yaml中的配置; - 相关字段定义见 调度章节。
Update configuration(更新配置)
这里的选项用于更新,或建议如何更新 instrument 与 roll 配置,包括:
- 根据采样数据与实际交易自动更新 spread cost 配置;
- 安全地修改 roll 参数;
- 检查价格乘数(price multipliers)是否与 IB 及配置文件一致。
仓位与订单层级
到这里,我们需要讨论不同层级的仓位和订单。为了抽象和灵活性,系统将仓位与订单划分为两到三个层级:
- Instrument 层级(品种层:仓位与订单)
- Contract 层级(合约层:仓位与订单)
- Broker 层级(券商层:仅订单)
在代码里你会看到“parent / child(父 / 子)”关系:
例如,一张 instrument 订单的子订单就是一组 contract 订单,依次向下。
每个层级都有自己的订单“栈”(严格意义上并不是计算机科学里的 LIFO 栈),其中保存的是处于活动状态的订单。
Instrument 层级
Instrument 层订单是“某个策略 + 某个品种”的订单。这些订单由 run_strategy_order_generator 进程生成。
Instrument 可以是一个一般性的期货市场,如 GOLD 或 DAX。重要的是:此时并不指定具体合约(具体合约取决于当前 roll 状态)。这一抽象层在回测里也同样存在,因此我们才会为每个 instrument 构造一条调整后的价格序列(adjusted prices),作为该 instrument 的“价格”。
Instrument 订单可以是:
- 显式订单:没有价格条件,直接执行
- 条件订单:如“当价格到达某个水平时再执行”
- 限价订单:如“只在某个价格或更好价格成交”
同时还可以附带执行偏好,例如:
- 用市价单(market order)尽快成交
- 使用某个执行算法(algo)尽量在市场上“以最好方式”执行
- 用限价单并指定一个价格(按调整后价格序列进行缩放)
系统会跟踪每个策略在每个 instrument 上的仓位(positions),在 instrument 订单被执行并成交后更新这些仓位。
Contract 层级
Instrument 订单会被分解为 contract 订单:即针对具体合约的订单(或一个市场内的合约价差交易,因为价差也是可交易的“instrument”)。
这一步由 run_stack_handler 进程完成,可能有多种情况:
普通单腿订单:
- 交易当前定价合约(priced contract),或
- 交易 forward 合约,或
- 同时交易二者;
具体取决于你当前是否处于被动换月(passive roll)状态以及仓位是增加还是减少(关于换月细节见这里)。
Force roll(强制换月)订单:
- 在当前 priced 合约与 forward 合约之间创建一个市场内价差(calendar spread);
- 同时会创建一张数量为 0 的 instrument 订单。
Force Outright roll 订单:
- 创建两笔独立交易:一笔平掉 priced 合约,一笔在 forward 合约上开同样的仓位;
- 同样会创建一张数量为 0 的 instrument 订单。
如果 instrument 订单为限价订单,其限价会传递到 contract 订单上;
当交易合约与调整价格所基于的合约不同(例如处于被动 roll 状态,交易的是 forward 合约)时,限价会根据 roll 关系做相应调整。
Contract 订单会根据 instrument 订单类型(限价、市价、best-execution)分配到不同的执行算法。
系统会跟踪每个 instrument / contract 组合的仓位,并在 instrument 订单被执行与成交时更新这些仓位;这些仓位可以直接与券商 API 返回的仓位做对账。
Broker 层级
当 contract 订单提交给券商时,会被转换成 broker 订单,这一步也由 run_stack_handler 完成。
由于流动性限制或 algo 自身的拆分规则,一个 contract 订单往往会被拆成多张 broker 订单。
Broker 订单由执行算法发起(根据 contract 订单所分配的 algo)。它可以是限价单或市价单,这取决于该执行算法的具体逻辑。
在 broker 层没有“仓位”,但我们可以用 broker 层的成交记录与券商 API 提供的真实成交数据进行对账。
当从券商 API 收到 fills(成交)后,会自底向上回传:先更新 broker 订单,再更新 contract 订单,最后更新 instrument 订单。
当某个订单的所有子订单全部完成时,该父订单也可以标记为完成;完成的订单会从活动订单栈中移出,转存到历史订单数据库。
一张订单的旅程
任何交易系统中最复杂的部分都是订单管理流程。
在 pysystemtrade 中,这一部分尤其复杂,因为系统被设计成可以(理论上)支持非常复杂的交易策略,并同时交易多套策略,再加上期货本身的复杂性。
下面我们以几类典型订单为例,跟踪它们在系统中的旅程:
- 一笔普通交易系统中的订单——可能包含被动换月(从一个合约滚到下一个合约);
- 一笔作为日历价差(calendar spread)的换月订单(Force roll);
- 一笔由两笔单独交易构成的换月订单(Force Outright roll)。
最优仓位(Optimal positions)
当回测被运行时(定期由 run_systems 启动,或者通过 update_system_backtests 临时触发),系统会为每个“策略 / instrument”组合生成一组 optimal position rules(最优仓位规则)。
Optimal positions 本身并没有固定的结构,常见例子包括:
- 简单最优仓位:例如“把仓位调到 +5 张合约”。这是动态优化交易系统使用的形式。
- 带缓冲区的最优仓位:例如“目标仓位在 +10.87 到 +13.32 张合约之间”。这是默认提供的“静态”回测系统所采用的形式。
- 条件最优仓位:例如“如果价格跌破某个水平则开多,如果涨破某个水平则平仓”(用于均值回复策略,通常还会配合止损单)。这类更复杂的仓位管理计划在未来系统中实现。
之所以有 optimal positions,是因为即便在参数固定而不重新估计的前提下,回测本身仍然比较耗时(在生产系统中我也推荐固定参数)。
可以通过只用有限数据加快回测,但这种做法并不令人满意,因此我们采用“先跑回测得到最优仓位规则,再在生产中只做订单生成”的方式。
Optimal position 不会指定“具体在哪个合约上持仓”,合约层的决策会在生成 contract 订单时再确定。
为了评估“从回测到实际执行之间时间滞后造成的滑点(slippage)”,optimal positions 会附带一些参考信息,包括:
- 参考价格:回测执行时调整价格序列中的最后一个价格;
- 参考合约:该参考价格对应的合约(当前 priced 合约,其价格等于调整价格序列);
- 这是为了在实际交易的合约与参考价格所基于的合约不同时(例如被动 roll 或 roll 已发生)进行换算;
- 参考日期时间:用于计算“回测认为可以执行的时间点”与“实际执行时间点”之间的分钟级延迟。
如果是限价订单,则还需要记录价格和合约(以防参考价格对应的合约设置有误)。
换月订单的 optimal position
对于换月订单,没有 optimal position 的概念:
因为换月是在 contract 层操作,而 optimal positions 定义在 instrument / strategy 层。
策略订单处理(Strategy order handling)
系统会定期(通过 run_strategy_order_generator)或者按需(通过 update_strategy_orders)为每个策略生成 instrument 订单。
目前默认是每天运行一次,但对于高频策略,也可以考虑一天运行多次。
Optimal positions 与生成出的订单之间的关系,取决于:
- 策略类型;
- 数据库里当前记录的仓位。
例如:
对于简单的最优仓位:
- 取“当前记录仓位”与“目标 optimal position”之差作为交易量,直接生成订单(当前实现中尚未提供)。
对于带缓冲区的最优仓位,例如“目标仓位在 +10.87 到 +13.32 合约之间”:
- 如果当前仓位大于 +13,就卖到 +13(取整);
- 如果当前仓位低于 +11,就买到 +11;
- 这是默认回测系统使用的逻辑。
对于条件最优仓位,则需要考虑价格水平以及当前记录的仓位;此类系统很可能需要一天生成多次策略订单。
生成的 instrument 订单会被放入 instrument 订单栈,行为如下:
如果当前 instrument + strategy 已经存在一张或多张订单:
- 将所有未成交部分(unfilled)相加,得到“未成交量”;
- 再计算为达到 optimal position 还需要多少额外订单;
- 例如:optimal position = +10,当前仓位 = +8,则原始订单应为 +2;
- 但栈上已经有一张 +4 的订单,其中 +3 已成交,剩余 +1 未成交;
- 那么新的订单量会从 +2 调整为 +1。
- 如果调整后的订单量为 0(说明未成交部分已经足够把仓位推到 optimal 区间),则不再新增订单。
如果当前 instrument + strategy 不存在任何订单:
- 若计算出的目标订单量非零,则直接下这张订单;
- 若订单量为 0(即当前仓位已在 optimal 缓冲区内),则不生成任何订单。
注意,上述逻辑要求“记录的仓位数据”和“已有订单的成交数据”必须同时被读取、且尽可能最新(或者至少在它们被读取期间不会产生新的成交)。
另外,调整订单方向也可能改变符号:
这意味着我们有可能先买再卖同一个市场——既愚蠢又可能违法。为了避免这种情况,需要做某种“订单净额化(order netting)”,并在跨策略交易时提高效率(相关内容后文会提到)。
在每天只生成一次订单、且在生成前由“日终流程”清空 instrument 栈(即不存在未决订单)的前提下,上述问题通常不严重;但对日内策略则需要格外小心。
Instrument 订单字段详解
创建一张 instrument 订单时,会记录以下字段:
- instrument / strategy
order_id:由栈处理代码分配desired_trade:期望交易量(标量;spread instrument 在此阶段也被视为普通 instrument)order_type:当前可能值包括:'best':以“尽可能好的价格”执行'market':市价单'limit':限价单(当前未使用)'Zero-roll-order':用于 roll 订单'balance trade':平衡性交易,用于interactive_order_stack,但不会实际在市场中执行
limit_price:限价limit_contract:限价对应的合约reference_price:参考价格reference_datetime:参考时间reference_contract:参考合约generated_datetime:该订单生成时间manual_trade:是否为手工交易(除非由interactive_order_stack生成,否则为 False)roll_order:是否为 roll 订单(不是 roll 则为 False)active:Truelocked:False
暂未使用的字段包括:
fill、filled_price、filled_datetimeparent(instrument 订单本身不使用)children
换月订单的策略层处理
Roll 订单并不是在这里生成的,而是由 stack handler 创建。
Overrides
Overrides(覆盖规则)会在 instrument 订单被放入栈之前应用。
这些规则会综合考虑该笔订单的目标交易量以及当前持仓。
关于 overrides 的更多细节,可以参见 instruments 文档。
Stack handler
在订单生命周期剩余的所有操作都在 stack handler 中完成:
- 自动模式通过
run_stack_handler运行; - 临时模式通过
interactive_order_stack进行手动干预。
Contract 订单创建
普通订单的 contract 订单创建
当 roll 状态为 'not rolling' 时,stack handler 会通过 spawn_children_from_new_instrument_orders 从 instrument 订单生成 contract 订单。
对于不涉及 rolling 的普通策略,每张 instrument 订单只会产生一张子 contract 订单,并在当前 priced 合约上交易。
这符合一个“几乎普适”的真理:contract 订单的目标是完全满足 instrument 订单的需求量(后文会看到,broker 和 contract 订单并不一定完全满足)。
新建 contract 订单并写入数据库时,会记录以下字段:
- instrument / strategy:策略为
_ROLL_PSEUDO_STRATEGY - 合约到期日(contract date):对于不处于 rolling 状态的普通策略,长度为 1
desired_trade:长度为 1locked:Falseactive:Trueorder_id:由 stack handler 分配parent:父 instrument 订单的order_idorder_type:继承自父 instrument 订单,目前可能为:'best'(尽可能好的价格)'market'(市价)'limit'(限价,但当前未用)'balance trade'(平衡交易,不实际执行,由interactive_order_stack生成)
limit_price:若交易的合约与 instrument 订单一致,则与 instrument 订单相同;
否则会根据 roll 调整;reference_price:逻辑与limit_price相同,只是作为参考价格;generated_datetime:该 contract 订单的生成时间(不同于父订单生成时间);roll_order:Falseinter_spread_order:Falsealgo_to_use:执行该订单所用 algo 的路径或标识
当前未使用的字段包括:
fill、filled_price、fill_datetimechildren- 控制该订单的 algo 引用
split_ordersibling_id_for_split_order
被动 roll(PASSIVE)状态下的 contract 订单
当 roll 状态为 PASSIVE 时,我们会:
- 在当前合约上发出平仓订单;
- 在下一只 forward 合约上发出开仓订单。
理论上,对同一张 instrument 订单可以生成 两张 contract 订单:
- 一张在当前合约上平仓;
- 一张在 forward 合约上开仓。
这两张 contract 订单在结构上与普通情况类似,只是当订单对应的是 forward 合约时,其 limit_price 与 reference_price 会根据 roll 做相应调整。
注意:即便在被动 roll 时,这些订单的 roll_order 标志仍为 False,因为即使不 rolling,我们也会做这些交易。
主动 roll(Force / Force Outright)下的 instrument 与 contract 订单创建
当 roll 状态为 Force 或 Force Outright 时,stack handler 会通过 generate_force_roll_orders 同时生成 instrument 和 contract 订单。
生成的 instrument 订单具有如下特征:
- instrument / strategy:策略为
_ROLL_PSEUDO_STRATEGY desired_trade:0(roll 订单在合约空间里是“平”的,不改变 instrument 层的头寸)order_type:'Zero-roll-order'limit_price:不适用limit_contract:不适用reference_price、reference_datetime:为一个 spread 价格,基于最近一次同时有 priced 与 forward 合约价格的时间点reference_contract:未使用(contract 订单本身不需要它来查找参考价格)generated_datetime:该 instrument 订单生成时间manual_trade:Falseroll_order:Trueactive:Truelocked:False
若 roll 状态为 Force Outright,则还会创建两张 contract 订单:
- instrument / strategy:策略为
_ROLL_PSEUDO_STRATEGY - 合约到期日:长度 1
desired_trade:长度 1,一张用于平掉当前合约,另一张用于在下一合约上开同等仓位locked:Falseactive:Trueorder_id:由 stack handler 分配parent:父 instrument 订单的order_idorder_type:'best'limit_price:未使用reference_price:- 对 priced 合约腿,与 instrument 订单参考价相同(假设是同一合约);
- 对 forward 合约腿,则根据 roll 调整;
generated_datetime:该 contract 订单生成时间(通常比 instrument 订单晚几秒);roll_order:Trueinter_spread_order:Falsealgo_to_use:执行该订单所用 algo 的位置
若 roll 状态为 Force,则会创建一张 spread contract 订单:
- instrument / strategy:策略为
_ROLL_PSEUDO_STRATEGY - 合约到期日:长度 2(两条腿)
desired_trade:长度 2 的价差交易,以平掉当前合约、在下一合约上开同等仓位locked:Falseactive:Trueorder_id:由 stack handler 分配parent:父 instrument 订单的order_idorder_type:'best'limit_price:未使用reference_price:为 priced 与 forward 两个合约都可交易时的实际价差generated_datetime:该 spread 订单生成时间(通常比 instrument 订单晚几秒)roll_order:Trueinter_spread_order:Falsealgo_to_use:执行该订单的 algo 位置
手工交易(Manual trades)
通过 interactive_order_stack,我们可以手工创建 instrument 订单,并在需要时手工创建 contract 订单。
手工交易不会带有前面提到的各种“参考数据”。
Broker 订单创建与执行
Broker 订单是实际提交给券商的订单(同时我们也会在本地数据库中保存它们)。
对于一个 contract 订单来说,生成多张 broker 订单是“可能且常见”的——因为默认执行算法会倾向于“滴灌式”下单(一次一手)以避免冲击市场。
Broker 订单由 stack handler 中的 create_broker_order_for_contract_order 方法从 contract 订单生成。
至于该 contract 订单是否源自 roll、spread 策略等,并不会改变 broker 订单自身的处理逻辑。
订单被下到市场前的检查
在创建 broker 订单之前,我们会对从数据库取出的 contract 订单做一系列检查和调整:
- 检查该 contract 订单是否已经完全成交;
- 检查该订单是否已经被某个 algo “控制”(algo controlled),以避免多实例 stack handler 同时执行同一张订单;
- 检查 instrument 是否被锁定(当发现仓位不匹配时会加锁,匹配恢复时自动解锁);
- 检查该合约是否可交易(向券商查询);
- 将订单尺寸调整到不超过预设的交易限额(trade limits);
- 再根据当前流动性调整订单尺寸(参考 order book 顶部的 level 1 volume,通过券商获取;多腿订单会按单腿逐一检查,以最保守的尺寸为准)。
需要注意的是,这些尺寸调整只影响“内存中的派生订单”(即即将变成 broker 订单的那份),不会修改存入数据库的 contract 订单本身。
因此可能出现这样的情形:
- 合约订单为买入 +10 手,但市场流动性只允许买 +3;
- 则会先生成一张 +3 的 broker 订单(之后 algo 还可能继续减少);
- 随后系统会尝试再生成一张 +7 的 broker 订单。
或者:
- 合约订单为 +10 手,但交易限额只允许 +5;
- 则会生成一张 +5 的 broker 订单;
- 一旦这张订单成交完成,将不再为该 contract 订单生成新的 broker 订单;
- 在日终清理时,该 contract 订单会从栈中清除;下一天如果限额是按日限制,则可以执行更多合约。
交易执行算法(Trading algos)
接下来,订单会被分配给一个执行算法(如果 contract 订单尚未绑定 algo,会在此分配)。
当前提供两类算法:
- 市价 algo(market algo);
- “best execution” algo。
两者都不是针对“带限价的 contract 订单”专门设计的,尽管 best execution algo 会战术性地使用限价单。
一般情况下:
- 如果距离收盘时间不足一小时,则订单会交给 market algo;
- 否则交给 best execution algo。
Contract 订单会被标记为“algo controlled”,以防其他线程或进程尝试对同一 contract 订单再次执行(目前只有通过 interactive_order_stack 才可能意外触发)。
Algo 中的下单前准备
在 algo 内部,若有必要会进一步缩小交易量(当前两个 algo 默认单次只交易一手)。
后续步骤取决于所用 algo:
- market algo:直接使用市价单;
- best execution algo:
- 先获取该合约的一个 “ticker 对象”;
- 决定是否适合使用限价单(例如当 order book 不平衡、用市价单更可能得到更好价格时,就不会用限价);
- 然后要么发出市价单,要么发出“尽量被动成交”的限价单。
之后,代码会创建一张 broker 订单,其数量等于“缩减后的 contract 订单数量”(注意这通常会远小于存入数据库的原始 contract 订单量)。
Broker 订单属性
创建 broker 订单时会设置以下属性:
- instrument / strategy:继承自 contract 订单
- 合约到期日(contract date):继承自 contract 订单;对 roll spread 订单来说长度为 2
calendar_spread_order:是否为价差订单locked:Falseparent:父 contract 订单的 idactive:Trueorder_type:market或limitalgo_used:创建该订单的 algo(继承自 contract 订单)limit_price:市价单为空side_price:- 买单:当前卖一价(offer);
- 卖单:当前买一价(bid);
- 即订单创建那一刻的价格(可能比实际提交时略早一点);
- 对于 spread 订单也存为一个浮点数。
mid_price:当前中间价,同样为 float,即便是 spread 订单;offside_price:- 卖单:当前买一价(bid);
- 买单:当前卖一价(offer);
- 同样为 float,即便是 spread 订单。
roll_order:继承自 contract 订单broker:券商标识broker_account:券商账户broker_clientid:客户端 IDmanual_fill:False
注意:此时尚未存储任何参考信息;之后如需参考价,会回溯到父 contract 订单中获取。
暂未设置的字段包括:
fill、filled_price、filled_datetimeorder_id:直到写入数据库后才会被赋值children:未使用(broker 订单处在订单层级的最底部)algo_commentsubmit_datetimebroker_tempidbroker_permidcommissionsplit_ordersibling_id for split_order
随后,broker 订单会通过生产数据 API sysproduction.data.broker 交给 sysbroker 订单代码(这个 API 同时处理之前提到的诸如获取 TICK 数据、开市时间、可用流动性等问题)。
在 sysbroker 代码中的订单执行
以下描述以 IB 为例(也是当前 pysystemtrade 唯一支持的券商)。
Broker 订单会被传递给 sysbrokers.IB.ib_orders.ibExecutionStackData.put_order_on_stack;
在那里,系统会补充足够的信息来唯一标识要交易的合约,然后再将其发送给 sysbrokers.IB.client.ib_orders_client.ibOrdersClient。
这一步会把订单翻译成 IB 能理解的形式并真正下单。
返回的对象是一个 tradeWithContract,其中包含:
ibcontractWithLegs:IB 对合约的表示,外加对 spread 订单各腿的描述;- 从 ib-insync 返回的
order对象。
然后,该对象会被包装成一个 ibOrderWithControls 对象。
这一层抽象同时包含:
- broker 订单本身;
- 以及管理交易所需的控制对象
tradeWithContract。
(这也为支持其他券商预留了空间。)
接下来我们会用券商返回的订单时间覆盖 submit_datetime,以保证其与行情时间戳及成交时间保持一致。
然后把这个 orderWithControls 存入本地缓存中,以便后续查询和处理 fills。
缓存的 key 为 broker_tempid。
之后,algo 会把 ibOrderWithControls 返回给 stack handler,此时 broker 订单上会多出:
submit_datetime:来自券商的实际下单时间broker_tempid:格式为account_id/client_id/ib_order_id,用于后续匹配订单(见“fills”部分)。
将 broker 交易写入数据库
ibOrderWithControls 中的 broker 订单部分会被作为一条新订单写入“broker 订单栈”数据库表(我们并不会保存“control”部分,这对如何获取 fills 有影响)。
在这个时刻:
order_id会被赋值;- 同时,它会被加入父 contract 订单的子订单列表。
一个重要约束是:
- 没有成功到达券商的 broker 订单不会被写入数据库,也不会以任何形式被记录(日志/echo 文件除外)。
- 换句话说,broker 订单数据库只包含已经送到 券商 的订单,即便它们随后被取消。
- 同时,如果 broker 订单没有写入数据库,我们必须确保将 contract 订单从 algo 控制状态中释放出来。
交易管理(Managing the trade)
控制权随后回到 algo 以管理这笔交易。
对于 market algo:
- 它会等待订单完全成交、或被券商取消、或超时(默认 10 分钟);
- 如超时未成交,algo 会尝试向券商发出取消请求。
对于 best execution algo:
- 若发出的是市价单,其行为与 market algo 相同;
- 若发出的是限价单,则会被动等待成交;
- 若满足某些条件,会“转为主动执行”:
- 即将限价改为买价等于卖一(offer)、卖价等于买一(bid),以提高成交概率。
那么,如何轮询取消与成交、调整限价、以及发出取消请求?
这些操作都依赖于 ibOrderWithControls 中的“control 对象”,即 ib-insync 中对订单和合约的抽象。
一个常见模式是:
- 先更新控制对象;
- 再用它来更新 broker 订单的“执行细节”。
通过这种方式会更新以下 broker 订单字段:
fill:对于 spread 订单可能为长度大于 1 的数组;filled_price:成交价格(float),即便 spread 订单也是单个浮点数(由于 IB 对各腿分别返回成交,我们需要计算聚合后的价格);filled_datetime:若有多次成交,则为最后一笔成交的时间;algo_comment:订单执行过程中从券商收到的消息会追加到这里;broker_permid:订单开始执行后,券商赋予的持久 ID;commission:佣金。
注意:此时这些信息仍只存在于内存中的订单对象中,并未写入数据库。
重要的一点是:在订单执行期间,交易管理是“阻塞”的——stack handler 在同一时间只能管理一笔交易。
理论上可以开启多个 stack handler 进程以缓解这个问题,并配合 algo 控制机制避免极端情况;不过不建议在家自行尝试。
执行结束后的 fills 与完成(After execution: fills and completions)
当 algo 完成交易管理后,控制权回到 stack handler。
此时我们应该拥有一张“完全成交”的订单,或者至少是已经被取消且不再期待有后续成交的订单。
当然也存在一些边界情况会打破这一假设。
stack handler 现在会:
- 根据 broker 订单的实际成交规模更新交易限额(trade limits);
- 将 broker 订单的成交信息应用到数据库(见下一节);
- 将父 contract 订单从 algo 控制状态中释放。
Fills 与订单完成(Fills and completions)
当订单沿着层级自上而下(instrument → contract → broker)传播时,fills 则会沿着相反方向自下而上回传。
- 在回传过程中,我们会不断更新各级的持仓表;
- 当订单完成后,它会从活动订单栈中移除,并被转存到历史订单数据库。
在正常订单流中,这一过程会自动完成;
此外还会有一个“兜底”机制:定期调度的进程会轮询各订单栈,检查是否有带新成交的订单需要处理。
Broker 订单的 fills
当 broker 订单执行结束后,其成交(fills)会被应用到保存在数据库中的 broker 订单记录上。这一步通过“把内存中的 broker 订单对象写回栈中”来实现(此时对象中已经包含执行细节)。
代码随后会调用相应逻辑,将这些 fills 继续传递到父 contract 订单。
一个旁支问题:如果成交在“很久之后”才到达怎么办?
考虑这样一个边界情况:
- 订单在真正成交前被取消,但之后券商又把这笔订单成交了;
- 此时 stack handler 已经“忘记”这张 broker 订单,并删除了我们用于查询成交的关键 control 对象;
- 因此本地记录的仓位与成交情况会与现实不一致。
那我们如何发现这笔“迟到成交”?
stack handler 会定期运行 sysexecution/stack_handler/fills.stackHandlerForFills.pass_fills_from_broker_to_broker_stack 方法。
对每一张保存在栈中的 broker 订单(即数据库中仍有记录,但已不再有 control 对象):
- 先在本地缓存的“broker 订单 + control 对象”集合中查找匹配订单——这通常包含当前会话中发出的订单(但如果订单由
interactive_order_stack在别处发出,则不会在此集合中)。这一集合以broker_tempid为索引。 - 若找不到,则从券商查询订单和 control 对象:
- 首先仍按
broker_tempid匹配(同一账户、同一 clientid、同一 IB orderId); - 若仍失败,则按
permid匹配,只要原始 broker 订单在栈中时已经拿到permid,这就可以工作。
- 首先仍按
- 若上述步骤都失败,那我们只能手工处理(“we’re buggered”):
- 需要通过
interactive_order_stack手工录入成交信息。 - 通常只有在订单执行后超过 24 小时时才会发生这种情况,因为 IB 的 API 只返回最近的订单。
- 需要通过
一旦我们从券商拿回了订单以及对应的 control 对象,就可以更新 broker 订单中的以下字段并写回数据库:
fill、filled_price、filled_datetimealgo_comment:执行过程中收到的券商消息broker_permidcommission
然后再调用代码,将这些成交传递到父 contract 订单。
Contract 订单的 fills 与仓位更新
在 stack handler 中,方法 apply_broker_fills_to_contract_order 会把 fills 向上汇总到 contract 层。
它可以通过以下几种方式被调用:
- (a)当一笔订单完成后,从 algo 传回时;
- (b)定期调度的
pass_fills_from_broker_to_broker_stack,如果订单尚未完全成交; - (c)定期调度的
pass_fills_from_broker_up_to_contract; - (d)通过
interactive_order_stack手工触发。
由于一个 contract 订单可以对应多张 broker 订单,我们会采取如下方式:
- 收集该 contract 订单所有子 broker 订单的成交;
- 注意此时可交易对象(instrument / strategy + 一组合约日期)是一致的;
- 然后计算:
- 最后一笔成交时间;
- 总成交量(对于多腿订单,每一腿都有相应数量);
- 平均成交价格(浮点数)。
随后,我们会更新保存在栈中的 contract 订单记录,以反映这些新成交信息,包括:
fill、filled_price、fill_datetime。
然后比较:
- 内存中 contract 订单的表示(尚未带 fill 信息);
- 以及我们刚刚计算出的总成交(带 fill 信息);
这样可以判断成交量是否发生变化(注意成交量是“累积”的)。
如果数量发生变化,就更新 instrument / contract 级别的仓位表,使之反映新的成交量;
但此时 instrument / strategy 级别的仓位表尚未更新,因此两者会暂时不一致。
在这一刻,对 IB 与本地仓位的对账会“表现正常”,因为 contract 级仓位已经正确。
最后,我们会调用代码,将 contract 层的成交进一步传递到 instrument 订单。
Instrument 订单的 fills 与仓位更新
Stack handler 使用 apply_contract_fills_for_instrument_order 将 fills 从 contract 层继续上传到 instrument 层。
该方法可以通过以下途径被调用:
- (a)由
apply_broker_fills_to_contract_order调用(正常情况下,在 broker 订单完全成交后触发); - (b)定期调度的
pass_fills_from_contract_up_to_instrument; - (c)通过
interactive_order_stack手工调用。
Instrument 订单与 contract 订单之间可以是一对多关系,因此会有多种情况。
单一 contract 订单 / 单腿(Single contract order / single leg)
这一情形最为简单。我们从 contract 订单获取:
- 成交时间(filled_datetime);
- 成交量(filled quantity);
- 成交价格(filled price)。
然后,将成交量与原始 instrument 订单的目标交易量比较,判断是否发生变化。
如果发生变化,就将仓位变化应用到 instrument / strategy 级别的仓位表。
此时两张仓位表(instrument/contract 与 instrument/strategy)都已正确且一致。
接着,我们更新数据库中 instrument 订单的记录,使其反映最新成交情况,并检查该订单是否可以标记为“完成”。
单一 contract 订单 / 多腿(例如 spread 交易)
这里有两种情况:
平价差(flat spread),instrument 订单对应的净 instrument 交易量为 0(例如 FORCE 状态下的 roll):
- 情况简单:因为 instrument 层净交易量为 0,instrument 仓位不会受到 contract 交易的影响;
- 接下来只需判断该订单是否可以标记为“完成”。
多腿订单,但 instrument 层净交易量非 0:
- 这里逻辑与上文“多合约订单”一节类似,会跨多腿计算总成交量与平均价格,并据此更新 instrument 仓位与订单状态。
多个 contract 订单
这种情况有三种子场景:
分布式订单(distributed orders):
- 典型于 PASSIVE roll,即在 priced 合约和 forward 合约上都存在交易;
- 定义:所有 contract 订单交易方向相同、instrument 代码相同、且所有合约层交易总和等于 instrument 层总交易量。
- 在这种情况下,我们会跨 contract 订单计算:
- 最后一笔成交时间;
- 总成交量;
- 平均成交价格。
- 然后将总成交量与原始 instrument 订单比较,如有变化则更新 instrument/strategy 仓位表;
- 接着更新数据库中的 instrument 订单记录,并检查是否可以标记为完成。
净平订单(flat orders),即 instrument 订单净交易量为 0(例如 Force Outright roll 时):
- 同样简单:由于 instrument 层净交易为 0,instrument 仓位不受 contract 交易影响;
- 接下来只需检查是否可以将订单标为完成。
订单完成(Order completions)
Stack handler 中的 handle_completed_instrument_order 方法负责处理“订单完成”。
它可以通过以下方式被调用:
- (a)由
apply_contract_fills_for_instrument_order调用(这是常见路径:在填充完 instrument 订单的成交后); - (b)通过定期调度的
handle_completed_orders; - (c)在栈关闭时由“日终流程(end of day process)”调用;
- (d)通过
interactive_order_stack手工触发。
一笔订单在以下条件下被视为“完成”:
- 整个“订单家族”(instrument 订单、其子 contract 订单、以及孙级 broker 订单)全部成交;
- 或者在“日终流程”调用时,也会把未成交或部分成交的订单视为“完成”(因为后续要清空订单栈)。
完成后的订单会经历两步处理:
- 取消激活(deactivation);
- 复制到历史订单数据库。
订单取消激活(Order deactivation)
对整个订单家族中的所有订单,将:
active字段设为 False。
被取消激活的订单在逻辑上是“不可见”的:
- 它们不会出现在“获取订单 ID 列表”的查询结果中;
- 因此也不会被执行或获得新的成交;
- 但它们仍然保留在订单栈中(直到日终流程删除)。
写入历史订单数据库
为了做分析,我们需要保留所有下达过的订单记录。
- 会将整个订单家族保存到历史订单数据库中;
- 然后可以安全地从订单栈对应的数据库表中删除它们(由日终流程完成)。
需要注意的是,不能保证“仓位数据库”和“订单数据库”永远完全一致,虽然系统已经尽最大努力保证一致性。
实践中,作者通常假定仓位数据是正确的,同时也会使用成交价格进行市值标记(mark-to-market)。
日终订单栈关闭流程(End of day stack shut down process)
每当 stack handler 关闭时(通常是在交易日结束),都会运行一个“安全关闭流程”。
该流程也可以通过 interactive_order_stack 手工触发。
其目标是:
- 确保订单栈处于干净、无残留状态;
- 所有订单都被迁移至历史存储;
- 所有未决订单都已被取消;
- 仓位表已更新。
这样一来,在栈关闭并清空之后,就可以重新生成新的 instrument 订单——目前默认是在每天栈关闭后生成一次。
具体步骤包括:
- 尝试取消所有 broker 订单;
- 处理所有成交;
- 处理所有完成的订单,包括未成交或部分成交的订单(此时会将它们标记为完成并迁移至历史表);
- 删除所有已取消激活的订单。
历史订单表与交易报告
为了做交易成本分析(TCA),我们需要比较以下价格以衡量滑点(slippage):
- 参考价格(reference price):在 instrument 订单中设置,在 contract 订单中可能被调整;
- 中间价(mid price):来自 broker 订单;
- 买卖方向价格(side price):来自 broker 订单;
- 对侧价格(offside price):来自 broker 订单;
- 限价(limit price):来自 broker 订单(注意这里是初始限价);
- 父限价(parent limit price):来自 contract 订单(主要用于那些试图改善限价的执行算法);
- 成交价格(fill price):来自 broker 订单。
为了衡量时间延迟,我们会比较:
- 参考时间(reference datetime):来自父 instrument 订单;
- broker 订单的提交时间(submitted datetime);
- broker 订单的成交时间(filled datetime)。
不会使用的字段包括:
- instrument 订单生成时间;
- contract 订单生成时间;
- instrument 和 contract 订单上的成交价格(否则会重复计数)。
上述信息会通过特殊的方法从历史订单表中恢复:
该方法会用 instrument 和 contract 订单中的信息来增强 broker 订单记录,从而得到完整的分析视图。
交互式诊断(Interactive diagnostics)
用于查看系统内部诊断信息的工具。
Python:
| |
Linux 脚本:
| |
回测对象(Backtest objects)
通常,检查 run_systems 的回测输出,有助于理解某些具体交易是如何产生的(相比单纯查看策略报告能看到更多细节)。
这些回测结果会以“pickled 缓存 + 配置 YAML 文件”的组合形式保存下来,你可以据此复现系统在当时运行时所做的全部计算。
输出方式选择(Output choice)
首先,你可以选择希望以何种形式查看结果:
- 交互式 Python:加载回测对象,并打开一个简单的 Python 解释器环境(实际上是对你输入的内容执行
eval)。 - 绘图(Plot):加载一个菜单,让你选择回测中的某个数据元素并绘制成图(在没有图形界面的服务器上会失败)。
- 打印(Print):加载一个菜单,让你选择回测中的某个数据元素,并将其打印到屏幕。
- HTML:加载一个菜单,让你选择回测中的某个数据元素,并将其输出到一个 HTML 文件(保存为
~/temp.html),之后可以用浏览器打开。
策略与回测选择(Choice of strategy and backtest)
接下来你可以选择策略,以及想要查看的那次回测 —— 所有回测都会带有时间戳保存(通常只保留几天)。
默认会选用最近一次回测。
选择阶段 / 方法 / 参数(Choose stage / method / arguments)
除非你处于“interactive python”模式,否则接下来可以选择希望查看的阶段(stage)和方法(method)。
根据你的选择,系统还会再询问一些附加参数,例如品种代码(instrument code)以及可能的交易规则名称(trading rule name)。
随后会用你选定的输出方式展示该方法在时间序列上的调用结果。
在自有 Python 环境中使用(Alternative Python code)
如果你更希望在自己的 Python 环境中完成上述操作,可以使用下面的代码:
它会以交互方式让你选择一个系统及其带日期戳的回测,并返回相应的 system 对象供你自由操作。
| |
报告(Reports)
允许你按需临时运行任意一个报告。
日志、错误与邮件(Logs, errors, emails)
用于查看各种系统诊断信息。
查看已存储的邮件(View stored emails)
系统在多种情况下会发送邮件:发生严重错误、发送报告,以及价格出现剧烈跳动等。
为了避免频繁骚扰用户,系统不会在 24 小时内就同一主题反复发邮件;对于主题与过去 24 小时已发送邮件相同的消息,将改为只存储但不发送。
在这里查看这些“存储邮件”时,它们会被打印出来,然后从存储中删除。
最常见的场景是:某个品种出现大幅价格波动,影响到该品种的多个合约;第一封“价格跳变”邮件会被发送,其余同主题邮件则被存储。
查看价格(View prices)
查看历史价格的 dataframe。可选内容包括:
- 单个期货合约价格;
- 多品种或多合约价格;
- 调整后的价格(adjusted prices);
- 外汇(FX)价格。
查看资金(View capital)
查看资金(capital)的历史时间序列。关于资金如何运作的更多细节见这里。你可以查看:
- 单个策略的资金;
- 总资金(跨所有策略):当前资金(current capital);
- 总资金:按券商估值(broker valuation);
- 总资金:历史最高资金(maximum capital);
- 总资金:累计收益(accumulated returns)。
仓位与订单(Positions and orders)
查看仓位与订单的历史序列。可选内容包括:
- 最优仓位历史(按策略、按 instrument);
- 实际仓位历史(按策略、按 instrument);
- 实际仓位历史(按 instrument、按 contract);
- 历史 instrument 级订单列表(按策略);
- 历史 contract 级订单列表(按策略、按 instrument);
- 历史 broker 级订单列表(按策略、按 instrument);
- 查看任意单个订单(任意层级)的完整细节。
Instrument 配置(Instrument configuration)
查看 instrument 配置数据(View instrument configuration data)
查看某个特定 instrument 的配置数据,例如 DAX:
| |
请注意,还有可能在其他位置(例如特定 broker 的配置中)存有更多的配置信息。
查看合约配置数据(View contract configuration data)
查看某个特定合约的配置,例如:
| |
关于 roll 参数的含义,见这里。
交互式订单栈(Interactive order stack)
用于检查和控制各级订单栈。
Python:
| |
Linux 脚本:
| |
查看(View)
可选操作包括:
- 查看某个具体订单(任意订单栈);
- 查看 instrument 订单栈;
- 查看 contract 订单栈;
- 查看 broker 订单栈(本地数据库中的记录);
- 查看 broker 订单栈(直接通过 broker API 获取所有活动订单及已完成交易);
- 查看仓位(数据库中的最优仓位、instrument 层和 contract 层仓位,以及来自 broker API 的 contract 层仓位)。
创建订单(Create orders)
正常情况下,订单由 run_strategy_order_generator 或 run_stack_handler 自动创建,但有时你可能希望手工创建订单。
从 instrument 订单生成 contract 订单(Spawn contract orders from instrument orders)
当 Stack handler 运行时,它会定期检查新的 instrument 订单,并为其创建子 contract 订单。
不过,你也可以在这里手动触发这一操作。典型用途包括:调试、暂时不信任 Stack handler 想要逐步执行每一步,或者当前在手工交易(这时 Stack handler 通常不会运行)。
创建强制滚动 contract 订单(Create force roll contract orders)
如果某个 instrument 处于 Force 或 Force Outright 的 roll 状态(参见滚动),则 Stack handler 会在每天开始时自动创建新的 roll 订单。
这些订单由一笔父级 instrument 订单(同品种内的价差单 intramarket spread,分配到虚拟策略 rolling)以及一个子 contract 订单组成。
同样,你也可以在这里手工完成这一流程,用途包括:调试、对 Stack handler 不放心而想逐步执行,或者处于手工交易模式(此时 Stack handler 不会运行)。
创建(并尝试执行)IB broker 订单(Create (and try to execute…) IB broker orders)
当 Stack handler 运行时,它会定期检查尚未完全成交的 contract 订单,并为其生成 broker 订单,提交给券商,然后交由执行算法(algo)管理执行。
在这里,你可以手工完成这一步。用途同样包括:调试,或当你暂时不信任 Stack handler、希望逐步执行每一步操作。
平衡交易:创建一系列交易并立即填充(不会真实执行)(Balance trade: Create a series of trades and immediately fill them (not actually executed))
正常情况下,Stack handler 会捕捉到所有成交(fills),并据此更新状态。
但有时这一步不会发生:例如,IB 因临近到期而强制平掉某个仓位;或者你在其他平台手工下了一个交易;又或者 Stack handler 在提交订单后、记录成交前崩溃……情况非常多。
这会带来严重问题:券商记录中的真实仓位无法在系统的仓位数据库中得到反映,这会在对账报告中被发现;一旦发现此类不一致,对应 instrument 会被自动锁定,在问题解决之前无法再交易。
从较次要的角度看,你也会在历史交易数据库中缺失这笔交易记录。
为了解决这个问题,你应该提交一笔平衡交易(balance trade):
它会像一笔正常交易那样在所有相关数据库中产生连锁更新,但不会真正发送给券商执行,从而“补上”那笔缺失的交易。
平衡 instrument 交易:只在策略层创建并填充(不真实执行)(Balance instrument trade: Create a trade just at the strategy level and fill (not actually executed))
在正常情况下,策略层仓位(按策略、按 instrument 聚合,跨所有合约求和)应该与合约层仓位(按 instrument 与 contract 聚合,跨所有策略求和)一致。
但如果某笔订单“丢失”或出现意外,两者就会不匹配(同样会在对账报告中被发现)。
为解决这个问题,你可以提交一笔仅存在于策略层的订单(不分配到具体 contract),用于调整逻辑仓位;这笔订单同样不会真实执行。
手工交易:创建一系列将被执行的交易(Manual trade: Create a series of trades to be executed)
通常,run_strategy_order_generator 会生成你所需的全部交易,但有时你可能希望生成一笔手工交易。
用途包括:用于测试;在你需要紧急平掉某个仓位时(严格来说更推荐通过override 来实现);或者当滚动过程出现问题、系统无法自动平掉某个合约时。
请注意,手工交易与平衡交易不同:手工交易会真的提交给券商执行!
另外,你也可以创建手工价差交易(spread trade):
先将 instrument 层目标仓位设为 0,然后选择“创建 contract 订单”,再输入你想要的腿数(legs)即可。
现金外汇交易(Cash FX trade)
现金外汇(cash FX)并不是 pysystemtrade 主要交易的资产类别,但除非你只在账户货币中交易,否则你实际上一直在交易外汇。
当你买卖海外期货合约时,会占用相应货币的保证金;如果你的该币种保证金不足,IB 会借给你这笔钱。
以外币形式借款通常要付利差,因此更好的做法是做一笔即期外汇(spot FX)交易:把你的本币(通常存着也拿不到任何利息)兑换成所需的保证金货币。
一般来说,我会定期优化自己的货币持仓,让持仓构成成为一个分散化的货币组合。
有的人则偏好定期把所有“多余的”外币头寸“扫回”(sweep)本币,以减少不必要的货币 Beta;
也有人会更激进,尝试在利率较高、存款利率为正的货币中保持更大余额(本质上是一种 carry trade)。
首先,你会看到各个货币的余额。注意,这里显示的不是“可用余额”或“未清算余额”,因此你仍需要跑一份 IB 报表,才能知道自己每种货币真正的多头或空头规模。
之后,你可以创建一笔 FX 交易。
在指定货币对时,不要忘记市场上有约定俗成的顺序;如果你把货币对写反,订单会被拒绝。
净额、取消与锁(Netting, cancellation and locks)
取消 broker 订单(Cancel broker order)
如果你已经提交了一笔 broker 订单,并希望取消它,可以在这里完成操作。
对 instrument 订单进行净额处理(Net instrument orders)
订单栈之所以设计得较为复杂,是为了允许不同类型的策略同时提交交易。
这种设计的一个好处是可以在不同订单之间做净额处理(netting)。
通常,这一步由 Stack handler 自动完成,但你也可以在这里手动触发。
锁定 / 解锁订单(Lock/unlock order)
在订单数据库中存在一个“锁(lock)”标记,本质上是一个显式的标志位,用来阻止订单在某些操作期间被修改。
对于跨多张数据表的操作,会先加锁以避免出现“部分成功、部分失败”的提交(这里使用的是 noSQL 数据库,没有原生的跨表事务提交功能)。
如果操作在加锁期间失败,代码通常会尝试回滚并移除锁,但这并不总是成功。
因此,有时需要手工解锁订单;出于对称性,也提供了手工加锁的功能。
锁定 / 解锁 instrument 代码(Lock/unlock instrument code)
如果券商记录的仓位与我们系统记录的仓位不一致,对应的 instrument 会被加锁,并禁止对其发出新的 broker 交易。
这一机制由 Stack handler 自动完成。
一旦不一致消除,Stack handler 会自动解除锁定。
你也可以在这里手工执行锁定或解锁操作。
注意:如果你是出于其他原因想暂时停止交易某个 instrument,请使用override,而不是锁(lock)。
锁会在条件恢复正常后被系统自动清除,而 override 则不会。
解锁所有 instruments(Unlock all instruments)
如果 broker API 出现故障或崩溃,所有仍有持仓的 instruments 都可能被锁定。这里提供了一个快速解锁全部 instruments 的方法。
移除合约订单上的 Algo 锁(Remove Algo lock on contract order)
当某个执行算法(algo)开始执行一笔 contract 订单(无论是全部还是部分)时,会先对该订单加锁。
订单执行完成后,这个锁会被释放。
如果 Stack handler 在释放锁之前崩溃,那么其他任何 algo 都无法继续执行这笔订单。
虽然在正常的日终栈清理流程中,这笔订单最终会被删除,但如果你无法等待那么久,可以在这里手工移除该锁。
删除与清理(Delete and clean)
删除整个订单栈(小心!)(Delete entire stack (CAREFUL!))
你可以删除三个订单栈中任意一个上的所有订单。
我很难充分强调这是一个多么糟糕的主意。
如果你需要紧急停止交易,我强烈建议针对 run_stack_handler 使用STOP 命令,或者手工调用下面描述的日终流程(end of day process),这样可以在保留 Stack handler 运行的前提下安全地停下交易。
只有在调试或测试时,并且非常清楚自己在做什么时,才考虑使用本操作。
删除指定订单 ID(小心!)(Delete specific order ID (CAREFUL!))
你可以从数据库中删除某个具体的“活动订单”。
同样,这极有可能导致各种奇怪的副作用。
而且,这个操作并不会取消该订单:券商端仍会继续尝试执行它。
因此,只有在调试或测试时,并且非常清楚自己在做什么的前提下,才应该使用本操作。
日终流程:取消订单、标记完成并删除(End of day process (cancel orders, mark all orders as complete, delete orders))
当 run_stack_handler 完成当天的工作时(要么是到达预设结束时间,要么是收到了 STOP 命令),会运行一个清理流程。
首先,它会尝试取消所有仍然“激活”的订单;
然后,它会将所有订单标记为“完成”,从而更新仓位数据库,并将订单迁移到历史数据表中;
最后,它会从所有订单栈中删除全部订单,确保不会有任何状态留到下一交易日(否则可能引发各种怪异行为)。
相较于直接删除订单栈,我强烈建议使用这个日终流程;除非你非常清楚自己在做什么,并且有充分合理的理由。
报告、日常维护与备份脚本(Reporting, housekeeping and backup scripts)
运行全部报告(Run all reports)
Python:
| |
Linux 脚本:
| |
单个报告的详细说明,见前文的报告部分。
删除旧的回测状态 pickle 对象(Delete old pickled backtest state objects)
Python:
| |
Linux 脚本:
| |
由 run_cleaners 调用。
每次 run_systems 运行时,都会创建一个回测对象并以 pickle 形式保存,同时也会保存一份对应的配置文件。
这样可以在之后用于诊断。
但这些文件非常大,因此会删除所有超过 5 天的旧文件。
清理旧日志(Clean up old logs)
Python:
| |
Linux 命令行:
| |
由 run_cleaners 调用。
作者非常喜欢打日志,这意味着日志条目会非常多。
本脚本会删除所有超过 1 个月的日志文件。
截断 echo 文件(Truncate echo files)
Python:
| |
Linux 命令行:
| |
由 run_cleaners 调用。
每天系统都会生成扩展名为 .txt 的 echo 文件。
本过程会将“昨天及更早”的 echo 文件重命名,追加日期后缀,然后删除所有超过 30 天的文件。
将数据库备份为 CSV 文件(Backup DB to CSV files)
Python:
| |
Linux 脚本:
| |
由 run_backups 调用。
更多说明见备份部分。
- 首先,从 MongoDB 和 Parquet 中读取数据,并写入临时 CSV 目录;
- 然后,将这些 CSV 文件复制到配置参数
offsystem_backup_directory所指定的备份目录下的/csv子目录。
备份状态文件(Backup state files)
Python:
| |
Linux 脚本:
| |
由 run_backups 调用。
它会将回测的 pickle 文件和配置文件复制到 offsystem_backup_directory 指定的备份目录下的 /statefile 子目录。
重要提示:
这些备份文件会包含你在私有配置(private config)中加入的所有数据,其中可能有敏感信息(例如 IB 账户号、邮箱地址、邮箱密码等)。
如果你选择把这些文件存放在云存储或备份服务上,你应当考虑先对它们加密(部分服务会自动加密,但也有很多不会)。
备份 MongoDB dump(Backup MongoDB dump)
Python:
| |
Linux 脚本:
| |
由 run_backups 调用。
- 首先,它会将 MongoDB 数据库导出(dump)到配置参数
mongo_dump_directory指定的本地目录(该参数可在defaults.yaml或私有配置 YAML 文件中设置); - 然后,它会把这些 dump 文件复制到
offsystem_backup_directory指定的备份目录下的/mongo子目录。
备份 Parquet(Backup Parquet)
Python:
| |
Linux 脚本:
| |
由 run_backups 调用。
- 将所有 Parquet 文件复制到配置参数
offsystem_backup_directory指定的备份目录下的/parquet子目录。
启动脚本(Start up script)
Python:
| |
Linux 脚本:
| |
当一台机器启动时,需要做一些日常“善后”工作,尤其是在机器在此前崩溃、未能正常关闭所有进程时:
- 清理 IB client ID:在机器重启且 IB 确认未运行时执行(否则最终会“用完”可用 ID);
- 将所有“运行中”的进程标记为已关闭(close)。
非 Linux 系统下的脚本(Scripts under other (non-linux) operating systems)
Python 有一个内建机制可以创建命令行可执行程序;
对于希望在 macOS 或 Windows 上部署生产环境 pysystemtrade 的用户,这可能是比当前 Linux 脚本更合适的方式。
对于用 Linux 但更偏好“标准方法”而非随附脚本的用户,也同样适用。
这一机制由打包工具提供,并在 pyproject.toml 中配置。
相关文档见这里:https://setuptools.pypa.io/en/latest/userguide/entry_point.html。
你可以在 pyproject.toml 中增加一个新的 entry_points 配置段,例如:
| |
项目安装后,上述配置会在当前 Python 路径中生成一组可执行“shim”(小包装程序);
执行这些命令时,会调用对应的函数。
这种方式的优点包括:
- 不需要额外的代码或脚本;
- 跨平台;
- 完全基于标准 Python;
- 不需要手工调整
PATH或其他环境变量; - 支持命令补全;
- 可以搭配任意类型的
virtualenv使用。
调度(Scheduling)
要运行一个完全或部分自动化的交易系统,必须使用某种调度软件,在固定时间启动脚本,例如:
- 机器一开机就要启动的进程;
- 每天固定时间运行的进程;
- 每天运行多次的进程(例如定期报告或对账)。
设计调度计划时需要考虑的问题(Issues to consider when constructing the schedule)
在设计调度计划时,你需要考虑:
- 机器负载:例如,应避免在交易时段(对延迟较敏感)运行特别耗 CPU 的任务。若使用多台机器,这一点会稍微不那么重要;
- 数据库“抖动”(database thrashing):例如,应避免在重要的实时交易进程进行读写时,同时在相同表上运行 I/O 密集的报表任务;
- 文件锁 / 完整性:例如,应避免在仍有活跃写入时运行备份;
- 鲁棒性:例如,比起让进程“永远运行不重启”,更可靠的方式通常是在每天夜里主动关闭交易相关进程,第二天再重新启动。
调度系统的选择(Choice of scheduling systems)
你需要某种调度系统,来触发各类顶层进程(所有以 run_ 开头的脚本)。
理想情况下,它应当支持:监控进程、记录其活动、远程控制、限制运行次数、在其他进程完成后再启动(依赖 / 条件)、等等。
Linux cron
Linux 的 crontab 非常优雅,但它无法(或很难)处理“条件化”的进程,也无法提供监控功能。
第三方调度器(Third party scheduler)
市面上有很多第三方调度器,尤其是在你使用 Docker / Puppet 等工具时。
Windows 任务计划程序(Windows task scheduler)
作者本人没有使用过这个产品(他既不用 Windows,也不用 Mac —— “出于意识形态原因”,并认为前者糟糕、后者价格过高),但理论上它应该可以完成任务。
使用 Python 调度(Python)
你也可以直接用 Python 充当调度器,利用类似 这个项目 的工具;
这样可以获得平台无关性。
但你仍然需要保证有一个 Python 进程始终在运行。
同时,还要小心区分“新线程”和“新进程”:在同一个进程中只能有一个 IB Gateway 或 TWS 连接。
手动系统(Manual system)
也可以完全不使用调度器,而是在需要时手动启动相关进程。
对于不打算完全自动化交易的用户,这种选择可能是合理的(不过,你可能仍然希望让大部分调度保持自动化)。
Python 与 cron 的混合方案(Hybrid of Python and cron)
这是 pysystemtrade 采用的方案,下面会更详细地介绍。
理论上,可以用其他调度器替代 cron 部分。
Pysystemtrade 内建调度(Pysystemtrade scheduling)
pysystemtrade 内置的调度器本身并不负责“启动进程”(这部分仍交由 cron 等调度器每天触发),但它会处理其他所有事情:
- 记录进程何时启动与停止、当前是否仍在运行、以及进程 ID;
- 只在指定的时间窗口内运行(
start_time与end_time); - 只在某个前置进程已在过去 24 小时内成功运行的前提下再启动(例如只有在价格更新完成后才运行
run_systems); - 允许通过
interactive_controls将进程状态置为STOP,或阻止其启动; - 在单个进程内部,多次调用“方法”(methods,实质上是子进程),可以设置上限次数(
max_executions)以及调用间隔(frequency); - 提供一个监控工具,也可以从远程机器上使用。
调度配置(Configuring the scheduling)
crontab
由于 pysystemtrade 的调度器不会主动“启动”进程,因此仍需要每天借助 cron(或其他调度器)去启动它们。
不过,具体的启动时间不那么关键,因为每个进程的运行时间窗口会在 YAML 配置中单独设置(见下文)。
作者本人使用 cron,因此在项目中提供了一个示例 crontab:
https://github.com/robcarver17/pysystemtrade/blob/master/sysproduction/linux/crontab。
关于该 crontab,有几点需要注意:
- 我们会启动
run_stack_handler和run_capital_update进程。这些进程会“全天”运行(你也可以想象某些日内系统会让更多进程全天运行)。
它们实际上会在启动后按照进程配置(YAML 中的参数)自行决定何时真正开始与停止。 - 接着,我们启动一批“每日一次”的进程:
run_daily_price_updates、run_systems、run_strategy_order_generator、run_cleaners、run_backups、run_reports。
启动顺序与它们实际的执行顺序一致,但实际行为仍由 YAML 中的进程配置控制。 - 在系统启动时,我们会启动一个 MongoDB 实例,并运行启动脚本。
进程配置(Process configuration)
进程配置由如下配置参数控制(位于 /syscontrol/control_config.yaml,也可以在 /private/private_control_config.yaml 中被覆盖):
process_configuration_start_time:进程允许开始运行的时间(默认00:01);process_configuration_stop_time:进程结束时间 —— 无论方法配置如何,过了这个时间进程就会结束(默认23:50);process_configuration_previous_process:前置进程名称;只有在这个前置进程在过去 24 小时内运行过,当前进程才会启动(默认无)。
以上每个配置都是一个 dict,以进程名为 key。
所有 value 均为字符串:开始与结束时间采用 24 小时制,例如 '23:05'。
如果某个进程没有单独配置,则使用 default 值。
下面是默认的 YAML 配置,并附有注释:
| |
进程内部各“方法”(methods)的配置由 process_configuration_methods 控制。
该参数本身是一个 dict,每个进程对应一个子 dict,子 dict 中每个 key 是一个方法名,而方法对应的配置可以包含下列字段:
frequency:两次运行该方法之间间隔多少分钟(默认 0,即无等待);max_executions:最多运行多少次(默认 -1,表示没有上限);run_on_completion_only:若为 True,则只在进程即将结束时运行一次。
(为什么没有“只在启动时运行一次”的选项?
其实只要设置 max_executions 为 1 就可以;
如果你还希望该方法在其他方法之前执行,只需要在配置中把它放在最前面即可。)
需要注意的是:
对于 run_systems 和 run_strategy_order_generator,这里的“方法名”实际上是策略名称(strategy name),并且针对这些进程还存在一些额外参数。
下面给出了带注释的完整默认控制配置:
| |
你可以在 /private/private_control_config.yaml 中覆盖以上任意配置项。
但你必须在私有控制配置中包含以下这些小节(如果你有更多策略,可以再增加),否则一些 run_ 进程将无法正常工作:
| |
最后,你还可以选择性地增加 arguments 配置块,用来为某些方法(或某个进程中“仅在结束时运行”的所有方法)传递参数。
例如:
| |
系统监控与看板(System monitor and dashboard)
系统提供了一个相对简单的监控工具,以及一个更复杂、功能更丰富的“看板(dashboard)”,用于监控系统当前状态。
详见这里的文档:/docs/dashboard_and_monitor.md。
故障排查(Troubleshooting)
在排查问题时,可以结合使用 status report 和 interactive_controls。
为什么我的进程没有运行?
- 它是否已由 cron 或其他调度器真正启动?
- 它当前是否处于
STOP或NO-RUN状态?(可以用interactive_controls修改); - 当前时间是否早于该进程的
start_time?(若是,可调整 start time,或等待); - 当前时间是否晚于该进程的
end_time?(若是,可调整 end time,或等到下一天); - 前置进程是否尚未“关闭”(close)?(若是,可以等待,或移除依赖关系);
- 它是否已经在运行,或者“误以为自己还在运行”(例如上一次迭代异常退出)?(可用
interactive_controls将其标记为 close)。
为什么我的进程停止了?
- 它是否被设置为
STOP? - 当前时间是否已经晚于该进程的
end_time? - 是否所有方法都已经停止运行,因为它们都达到了各自的
max_executions?
为什么我的某个方法没有运行?
- 是否已经用完它的
max_executions? - 它是否被设置为
run_on_completion_only?(如果是,则只有在进程结束时才会运行)。
生产系统核心概念(Production system concepts)
配置文件(Configuration files)
系统的配置分布在多个位置:
- 系统默认配置(system defaults);
- 私有配置(private config,覆盖系统默认配置);
- 回测配置(backtest config,覆盖系统默认配置和私有配置);
- 控制配置:包括默认和私有控制配置;
- 券商与数据源的特定配置;
- Instrument 与滚动(roll)相关配置。
系统默认配置与私有配置(System defaults & Private config)
大部分配置保存在 /sysdata/config/defaults.yaml 中;
你可以在私有配置文件 /private/private_config.yaml 中覆盖这些配置,示例见 /examples/production/private_config_example.yaml。
凡是在私有配置中出现的键值,都会覆盖 defaults.yaml 中的对应设置。
有少数配置项不在 defaults.yaml 中,必须写在 private_config.yaml 里:
broker_account:IB 账户 ID(字符串);offsystem_backup_directory:若使用异地备份,需要指定该路径(如果不使用,可以设为本地磁盘路径,或修改备份脚本)。
与策略相关的配置(见策略章节):
strategy_list(dict,key 为策略名称);strategy_name:load_backtests:object:用于创建 system 实例的类,例如sysproduction.strategy_code.run_system_classic.runSystemClassic;function:该类中用于创建 system 实例的方法,例如system_method;
reporting_code:function:用于产出策略报告的函数,例如sysproduction.strategy_code.report_system_classic.report_system_classic;
strategy_capital_allocation,见资金章节:function:用于分配资金的函数,例如sysproduction.strategy_code.strategy_allocation.weighted_strategy_allocation;strategy_weights:策略名称到权重的映射;strategy_name:对应策略的权重(浮点数)。
以下配置项不在 defaults.yaml 中,但可以选择性写入 private_config.yaml:
quandl_key:若使用 Quandl 数据;barchart_key:若使用 Barchart 数据;email_address:若希望通过邮件接收错误与报告;email_pwd;email_server:发件服务器地址。
以下配置项存在于 defaults.yaml 中,并可在 private_config.yaml 中覆盖:
与备份路径相关:
backtest_store_directory:父目录;各策略的回测结果会保存在以策略名为子目录的路径下;csv_backup_directory;mongo_dump_directory;echo_directory。
与券商连接相关:
ib_ipaddress:默认127.0.0.1;ib_port:默认4001;ib_idoffset:默认100。
与数据库相关:
mongo_host:默认127.0.0.1;mongo_db:默认'production';parquet_store:例如'/home/me/data/parquet',即 Parquet 文件所在路径。
与价格采集相关:
max_price_spike:默认 8;intraday_frequency:默认H。
与资金计算相关:
production_capital_method:默认'full';base_currency:账户基准货币(回测时也会用到,但在生产环境中更为重要)。
系统回测 YAML 配置文件(System backtest YAML config file(s))
参见回测用户指南。
系统默认配置、私有配置与回测配置之间的交互略显复杂:
在“回测内部”(无论是在生产模式还是仿真模式),配置项按以下优先级读取:
- 当前回测的 YAML 配置文件;
private_config.yaml;defaults.yaml。
在“回测代码之外”的生产环境中,配置项则按以下优先级读取:
private_config.yaml;defaults.yaml。
生产代码看不到你的回测配置文件:
它本身不隶属于某个具体策略,因此不知道应该去读取哪份策略配置。
控制配置文件(Control config files)
如前文所述,控制配置只用于“控制与监控”,保存在 /syscontrol/control_config.yaml 中,亦可由 /private/private_control_config.yaml 覆盖。
券商与数据源特定配置文件(Broker and data source specific configuration files)
以下配置主要用于将系统内部代码与券商代码之间进行映射:
/sysbrokers/IB/config/ib_config_futures.csv;/sysbrokers/IB/config/ib_config_spot_FX.csv。
Instrument 与滚动配置(Instrument and roll configuration)
以下 CSV 配置同时用于生产与仿真环境:
/data/futures/csvconfig/instrumentconfig.csv;/data/futures/csvconfig/rollconfig.csv。
初始化配置(Set up configuration)
以下文件用于在初始化数据库时写入初始配置,同时也会在仿真环境中使用:
/data/futures/csvconfig/spreadcosts.csv。
资金(Capital)
“资金”(Capital)指我们在交易账户中“处于风险之中”的那部分资金。
这部分总资金会在策略层之间进行分配,分配频率为每日。
最简单的情况是:你的风险资金就等于交易账户中的资金。
如果你完全不做任何设置,系统就会采用这一行为。
在其他所有情形中,资金的行为取决于“已存储的资金数值”与参数 production_capital_method(若未在私有配置中设置,则默认值为 full)之间的交互。
如果你希望采用不同的方法,应考虑修改该参数,并 / 或使用交互工具来修改或初始化资金。
在初始化资金时,你可以选择以下数值:
- 券商账户估值(brokerage account value,默认从券商 API 获取)。
如果你将其他系统中的资金“拼接”进本账户,可以修改该值;否则通常保持默认即可; - 当前分配资金(current capital allocated,默认等于券商账户估值);
- 最大分配资金(maximum capital allocated,默认等于当前资金)。
仅当production_capital_method='half'时才会用到这一数值,它相当于策略的“高水位线(high-water mark)”。
如果你此前在其他地方已经运行过该策略并产生亏损,你可能希望把它设得高于当前资金。
虽然技术上可以设得低于当前资金,但从逻辑上讲没有太大意义; - 累计盈亏(accumulated profits and losses,默认 0)。
这一值不会直接影响资金计算,但有助于你了解整体表现。
如果你已经在其他地方运行过该策略,可以在此填入对应的历史累计盈亏。
如果你没有主动初始化资金,那么在资金逻辑第一次运行时,会自动填入各项默认值(等价于“资金 = 当前交易账户估值”)。
在初始化之后,资金会在每天进行更新:
- 首先记录券商账户的最新估值,并与前一次的估值进行比较;
- 两次估值的差额即为自上次检查以来的盈亏,这一数值会写入 P&L 累计账户。
接下来如何根据盈亏更新资金,将取决于 production_capital_method。
建议先阅读这篇文章:https://qoppac.blogspot.com/2016/06/capital-correction-pysystemtrade.html。
如果为
full:- 盈亏会全部计入“在用资金”;
- 出于整洁,最大资金会被设置为当前在用资金;
- 这会导致收益被“复利化”(compounded);
- 这是默认设置。
如果为
half:- 盈亏同样会计入在用资金,但仅限于当前资金尚未达到“最大资金”的阶段;
- 一旦当前资金达到最大资金,后续盈利将不再增加资金;
- 因为亏损会减少资金,所以这一方法在 Kelly 意义上是“兼容”的,但收益不会复利;
- 作者个人采用的就是这一方法。
如果为
fixed:- 盈亏不会改变资金;
- 出于整洁,最大资金被设置为当前在用资金;
- 这一方法并不推荐,因为它不“Kelly 兼容”:一旦亏损,你将在账户价值的意义上遭受指数级递增的百分比亏损;
- 在小规模测试账户中,为了维持一个最低持仓规模,这种方法在某些情境下“勉强说得过去”。
总体而言,资金逻辑可以基本视作“设置好就不用管”(fire and forget)。
只有在少数情形下,才需要使用交互工具进行干预。
资金的巨大变动(Large changes in capital)
如果券商账户估值较上一次变动超过 10%,系统不会立刻做进一步操作,因为这很可能是一个错误值。
此时会发送一封邮件,提示你使用交互工具,选择“从 IB 账户估值更新资金(Update capital from IB account value)”。
系统会再次获取估值;如果变动仍超过 10%,你将可以选择是否接受这一变化(前提是你已经自己核对过数据)。
现金 / 股票的存取(Withdrawals and deposits of cash or stock)
上述资金更新方法的优点之一,是具有一定的“自我恢复”能力:
如果你一段时间没有更新资金,重新启动时它会自动补账。
然而,这也意味着:
如果你从券商账户中取出现金或证券,这在系统看来会像是一笔亏损,从而导致资金减少;
反之,如果你向账户中存入资金,看起来就会像获利,资金增加。
这也许并不会困扰你(你确实希望资金随账户总值变动,或者根本不关心账户级别的 P&L),
但如果你不希望这样,可以在交互工具中选择“调整账户估值以反映存取(Adjust account value for withdrawal or deposit)”。
注意要使用账户基准货币来表达这笔变动。
如果你事后才想起自己忘记做上述操作,应先在交互工具中选择“Delete values of capital since time T”:
- 删除某一时间点以来的“错误资金记录”;
- 记录这笔存取动作;
- 再选择“从 IB 账户估值更新资金(Update capital from IB account value)”,确认资金更新正确。
如果你希望 P&L 是正确的,但又希望资金本身随着存取而变动,则在记录存取之后,应使用“Modify any/all values”选项:
- 相应调低(或调高)总资金(total capital);
- 若使用半复利(half compounding),且最大资金(maximum capital)低于新的总资金,也需要一并调高最大资金。
资金方法或资金基准的变化(Change in capital methodology or capital base)
在交互工具中使用“Modify any/all values”选项。
即便已经开始实盘交易,你仍可以改变资金计算方法、最大资金,或其他任意值 —— 在很多情形下,这样做还有合理的动机。
如果你希望持续计算“账户级别”的百分比盈亏,建议不要删除历史资金记录;
不过,交互工具中也提供了“Delete everything and start again”这一选项。
- 更改资金方法(capital method):可以,这一点作者本人也做过。
系统不会记录这一参数的历史值,但你通常可以从历史资金变化推断出过去采用的方法; - 更改总资金(total capital):也完全可以、而且经常合理。
例如,你可能希望在全额资金已经到位的情况下,仅以较小资金启动新系统,然后逐步增加;
或者在自己“怂了”的时候临时减小资金。
如果使用半复利,记得同时考虑最大资金的设定; - 更改最大资金(maximum capital,仅影响 half 模式):
- 若将其设为低于当前总资金,则总资金会立刻降至该新值;
- 若将其设为高于当前总资金,则之后你可以累积利润,直到达到新的最大资金。
你也可以在交互工具中修改其他字段,但请务必谨慎,并确保自己清楚“在改什么、为何要改”。
策略(Strategies)
每个策略通过配置参数 strategy_list 定义,配置位置在 defaults.yaml 中,亦可在私有 YAML 配置中覆盖。
下面是一个名为 example 的示例策略的配置:
| |
策略资金(Strategy capital)
策略资金来自总资金,由脚本函数 update strategy capital 进行分配。
相关配置在 defaults.yaml 中(也可在 private_config.yaml 中覆盖):
| |
资金分配逻辑会调用上述 function,并将其他参数以关键字形式传入。
默认函数非常简单:按 strategy_weights 中给出的权重在所有策略之间按比例划分资金。
如果你使用默认函数,只需调整 strategy_weights 即可;
当然,你也可以自行编写更复杂的资金分配函数。
风险目标(Risk target)
策略实际承担的风险取决于“策略资金”和“风险目标”两方面。
风险目标通过回测配置 YAML 文件中的 percentage_vol_target 配置项设置(若未指定,则使用 defaults.yaml 中的值;这一值不会被 private_config.yaml 覆盖)。
不同策略可以拥有不同的风险目标。
调整风险目标与 / 或资金(Changing risk targets and/or capital)
策略资金可以在任意时间调整,并且实际上通常会每日变化,因为它依赖于分配给策略的总资金。
你也可以调整某个策略在整体中的权重。
系统会记录每个策略资金的历史数据,因此所有变动在事后都可以追踪。
权重本身不会单独存储,但可以从总资金与各策略资金反推出来。
系统不会记录策略风险目标的历史值;
因此,如果你频繁更改风险目标,会给跨时间比较带来困难。
不太建议这么做。
系统运行器(System runner)
系统运行器会在夜间为每个策略跑一次回测(更多细节见这里)。
下面是在 /syscontrol/control_config.yaml 中针对示例策略 example 的配置(记得在 private_control_config.yaml 中为自己的策略写入对应配置):
| |
这里的通用进程参数 max_executions 与 frequency 都是可选的。
但强烈建议将 max_executions 设为 1,除非你确实希望在白天多次运行回测(这时也应该设置 frequency,表示两次运行之间的时间间隔,单位为分钟)。
一个系统通常会执行以下步骤:
- 获取当前交易账户中的资金量(见策略资金);
- 使用该资金规模跑一次回测;
- 获取仓位缓冲区(position buffer)上下限并写入存储(对“经典系统”而言如此;其他系统可能会写入不同的内容);
- 将回测状态(pickled 缓存)存入
csv_backup_directory对应目录下(该目录在私有配置或系统默认配置中设定),
再加一个以策略名称为子目录、以回测生成时间为文件名的一套文件; - 同时,将用于生成该回测的配置文件复制一份,采用类似的命名规则。
作为示例,“经典”系统运行函数见:/sysproduction/strategy_code/run_system_classic.py。
策略订单生成器(Strategy order generator)
回测完成后,会生成一组目标最优仓位(对于经典的“缓冲仓位”系统,其中会包含 buffer)。
接下来,需要基于这些目标仓位和我们当前的实际仓位,计算出需要由 run_stack_handler 执行的交易。
下面是在 /syscontrol/control_config.yaml 中针对示例策略 example 的配置(同样需要在 private_control_config.yaml 中为自己的策略添加对应配置):
| |
订单生成器示例见:/sysexecution/strategies/classic_buffered_positions.py。
对于使用限价单、条件单或不使用缓冲的策略,则需要实现不同的订单生成器。
重新加载回测(Load backtests)
为了诊断等用途,经常需要重新加载某次回测(例如生成策略报告)。
该配置用于指定使用的类与方法(本例中与 run_systems 使用的是同一个类,只是方法不同):
| |
报告代码(Reporting code)
每个策略都有自己对应的一组报告;
要实现这一点,需要在配置中指定用于生成报告的函数:
| |
对应示例见:/sysproduction/strategy_code/report_system_classic.py。
崩溃后的恢复:哪些能救、怎么救,哪些救不了(Recovering from a crash - what you can save and how, and what you can’t)
一般建议(General advice)
关于系统崩溃后的恢复,这里有一些通用建议:
- 如果你没有使用 IBC,先重启 IB Gateway;如果用了 IBC,则先确认 Gateway 已正常启动;
- 临时关掉 crontab,避免在你还没准备好之前自动拉起各个进程;
- 检查 MongoDB 实例是否运行正常;
- 跑一遍完整的报告集,并仔细检查,尤其是
status报告和reconcile报告,确认一切正常; - 如有必要,按下一节描述的方式恢复数据;
- 如果一切顺利,你会得到一个“空订单栈”;此时运行
update_strategy_orders重新填充订单栈; - 当确认一切恢复正常后,再重新启用 crontab;
- 各个进程由调度器(如 cron)启动;如果在崩溃当天,它们的正常启动时间已经过去,你需要手动启动这些进程(作者在无图形界面的服务器上习惯使用
linux screen来做这件事)。
到第二天,一切应恢复到正常自动模式。
数据恢复(Data recovery)
先考虑最糟糕的情况:MongoDB 已损坏,且备份也损坏。
在这种情况下,你可以通过备份的 CSV 数据库导出文件恢复以下内容:
- 外汇(FX);
- 单个期货合约价格;
- 多品种或多合约价格;
- 调整后的价格;
- 仓位数据;
- 历史成交;
- 资金数据;
- 合约元数据;
- instrument 数据;
- 最优仓位。
需要注意的是,目前尚未为所有这些恢复步骤都提供现成脚本(原文在此处标注 FIX ME TO DO)。
此外,还有一部分“与交易和进程控制相关”的状态信息同样保存在数据库中,这部分会丢失,但通过一定操作可以恢复:
- 滚动状态(roll status);
- 交易限额(trade limits);
- 仓位限额(position limits);
- Overrides。
日志数据也会丢失;但你仍可通过归档的 echo 文件 搜索相关信息。
更理想的情况是:MongoDB 本身完好无损。
在这种情况下,一旦你完成恢复,只会损失最近一次“夜间备份”之后的增量数据。
接下来,如果希望尽可能恢复这些数据,可以按照以下步骤操作:
- 由于数据库不是 SQL 类型,存在出现不一致的可能;
因此,即便部分数据看似“最新”,通常也最好全部回滚到最后一次完整备份; - 所有对交易限额、仓位限额和 overrides 的修改都会丢失,需要重新设置;
- 你可能还希望将回测状态文件(backtest state files)从备份中复制回来;
- 重要:任何对滚动状态的修改都会丢失;任何基于回溯调整价(back-adjusted price)的滚动也会回到旧状态。
在进行任何交易之前务必先完成这一步,否则系统可能被搞糊涂; - 重要:Stack handler 中可能存在“未完成的订单”。
需要运行interactive_order_stack,并执行日终流程(end of day process)。
同样,这一步必须在任何新交易之前完成,否则会让系统状态混乱; - 重要:即便完成了 Stack handler 的清理,仓位数据与历史数据仍将缺失某些交易的影响——尤其是那些已经在券商端成交,但对应成交记录在我们这边丢失的情形。
使用interactive_order_stack查看仓位;若出现任何“断点”(break),需要通过interactive_order_stack创建对应的“平衡交易(balance trade)”(针对 broker 与数据库之间在合约层的差异),
或“平衡 instrument 交易(balance instrument trade)”(针对策略层与合约层之间在 instrument 层的差异)。
成交价格应从券商网站获取。
同样,这一步必须在任何新交易之前完成,否则系统会将这些断点视为严重错误,并锁定对应 instruments,导致无法交易; - 外汇、单个期货合约价格、多品种 / 多合约价格、调整后的价格:
一旦run_daily_price_updates运行,这些数据会被自动补回; - 资金:任何“日内 P&L”信息会丢失,但当
run_capital_update运行后,当前资金值会恢复为正确值; - 最优仓位:一旦
run_systems运行,便会重新计算并恢复正确; - 你可以使用各类
update_*进程来手动补跑那些因为崩溃而跳过的流程(在调度器自动运行之前)。
注意要按正确顺序执行:update_fx_prices(必须在run_systems之前)、update_sampled_contracts、update_historical_prices、update_multiple_adjusted_prices、update_strategy_backtests; - 重要:关于“进程状态”的信息可能已经不正确;
你可能需要使用interactive_controls手工将某些进程标记为 FINISH,否则系统会因为“担心冲突”而拒绝再运行它们(不过启动脚本通常会替你做一部分这类清理工作)。
报告(Reporting)
看板(Dashboard)
下文要介绍的许多报告信息,其实也可以在网页看板中查看。
滚动报告(Roll report,日报)
滚动报告可以针对所有市场运行(这是邮件报告的默认方式),也可以针对单一市场按需运行。
当你针对某个市场运行交互式的 update roll status 流程时,也会自动生成该报告。
下面是一个滚动报告示例,原文中用引号(")标出了作者的说明:
| |
简要说明:
- 表格上半部分给出了各个品种的 roll 状态,以及距离“建议滚动时间”、当前 price 合约到期、carry 合约到期的天数;
contract_priced/contract_fwd列显示“当前主要交易的合约”和“下一步要交易的合约”;position_priced显示在当前 price 合约上的仓位;volume_priced/volume_fwd显示最近几天各合约的成交量(按最大成交量归一化到 1.0);- 报告中也给出了“建议在到期前多少天滚动”的参考值,这些值也被用来在回测中生成历史滚动日期,但实盘中无需完全拘泥于此。
P&L 报告(P&L report)
P&L 报告展示你的盈亏情况。
日常运行时,它会覆盖过去 24 小时;按需运行时,则可以指定任意时间区间(无论是最近还是历史)。
下面是一个带注释的示例(中文解释在报告之后):
| |
简要说明:
- “Total p&l” 来源于比较两个时间点的券商账户估值;
- “P&L by instrument” 以“占总资金百分比”的形式给出每个 instrument 的盈亏,使用数据库中的价格与成交记录计算;
- “Total futures p&l” 为期货部分的总盈亏,将其与总 P&L 相比会得到一个“Residual p&l”,这部分可能来自:
- 价格或数据 bug;
- 手续费与利息;
- 非期货类资产的盈亏(不在 instrument P&L 中体现);
- 券商估值时间与价格时间不一致等;
- “P&L by strategy” 中的 P&L 也是相对“总资金”的百分比,而不是相对于单个策略资金,因此各策略之和应等于总 P&L;
- “P&L by asset class” 则按资产类别聚合。
状态报告(Status report)
状态报告用于监控各个进程、数据采集以及各种控制要素的状态。
它会在每日定时运行,也可以按需运行。下面是一个带注释的示例:
| |
该部分报告展示了:
- 各个
run_...进程是否正在运行; - 它们最近一次的启动与结束时间;
- 以及最近一次运行的对应日期时间等信息。
报告接下来还会列出:
- 各
run_进程中每个方法最近一次运行时间(“Status of methods within processes”); - 所有“每日价格更新相关”方法、资本更新、策略回测、订单生成、清理与备份脚本等的最新运行时间;
- 各种“状态类”报告(roll、P&L、status 本身等)的最新生成时间。
之后是价格与 FX 状态部分:
| |
这里列出了所有生成过的调整价格与 FX 汇率,并按“更新时间从早到晚”排序:
- 若某个市场已停牌或数据采集出了问题,其价格会“陈旧”;
- 可以注意到亚洲市场在列表顶部,早晨时段就已更新完毕;
- FX 汇率通常在 23:00 这一时间戳更新(表示使用的是“每日价格”;文中作者并不采集日内 FX 价格)。
接下来是最优仓位状态:
| |
这里显示了各品种的最优仓位上次生成的时间戳(由每日回测生成)。
在示例中,当日报告生成时,当天的回测尚未完全运行结束,因此这里仍是前一个交易日(周五)的结果。
然后是交易限额状态:
| |
这一部分按策略与 instrument 维度展示:
- 每个 instrument 在某个周期(
period_days)内允许的最大交易次数(trade_limit); - 自上次重置以来已发生的交易次数(
trades_since_last_reset); - 剩余可用交易额度(
trade_capacity_remaining); - 距离上次重置过去了多长时间。
紧接着是仓位限额状态:
| |
这一部分展示了:
- 在哪些策略 / instrument 上当前有仓位;
- 每个位置当前的头寸大小(
position); - 对应的仓位限额(
pos_limit),若为no limit表示未单独设限。
随后是 Overrides 的状态:
| |
如果你在某些 instrument 上设置了临时减持 / 禁止交易的 override,这里会列出相应记录。
示例中作者当前并未使用 override,因此表为空。
最后是锁定 instrument 的列表:
| |
若因仓位不一致(position mismatch)而触发了锁定,这里会列出被锁的 instrument 列表,并提示你参考对账报告(reconcile report)以了解细节。
交易报告(Trade report)
交易报告列出数据库中记录的所有交易,并允许你以非常细致的方式分析滑点。
日常运行时覆盖过去 24 小时;按需运行时可以指定任意时间段。
下面是一个带注释的示例:
| |
这里首先给出了每笔 broker 订单的基础信息:
- instrument 代码、策略名称、合约 ID;
- 成交时间、成交数量(
fill)、成交价格(filled_price); - 对于跨合约的 roll 交易,由于展示方式的限制,此处
fill显示为 0。
接下来是“延迟(Delays)”部分:
| |
这里比较了:
- 父级 instrument 订单生成时间(
parent_generated_datetime)与提交给券商的时间(submit_datetime),得到submit_minus_generated; - 提交时间与成交时间(
fill_datetime)之间的间隔,得到filled_minus_submit。
在仿真中,作者假设订单会在生成后一整“工作日”才提交;
实盘中通常会比这更快。
由于时间戳原因,有些记录会出现“先成交后提交”的异常情况,这类记录会在统计中被忽略。
随后报告依次给出了多种维度的滑点分析:
- 以“tick”为单位的滑点(例如相对于中间价、买卖价、限价、父限价的偏差);
- 经过波动率调整的滑点;
- 换算为账户基准货币(例如 GBP)的现金滑点。
现金滑点部分示例:
| |
这里通过“每个价格点在基准货币中的价值(value_of_price_point)”将 tick 数转换为具体金额;
并将延迟、买卖价、执行、相对于限价与父限价的各类滑点合并到 total_trading_cash 中。
最后,报告会给出一段较长的统计汇总:
| |
这一部分主要用于年度或长周期的交易成本分析:
针对每一种滑点维度(延迟、买卖价、执行、相对限价、相对父限价以及总交易滑点 = 执行 + 买卖价),
会对每个 instrument 和策略给出:总量、次数、均值、区间上下限(±2 标准差),
并在 tick、波动率调整值和基准货币现金三个尺度下分别统计。
对账报告(Reconcile report)
对账报告用于检查数据库中仓位与成交记录的一致性,以及它们与券商记录的一致性。
该报告会每天运行一次,也可以按需运行。下面是一个带注释的示例:
| |
简要说明:
- 第一部分比较“最优仓位(含上下 buffer 区间)”与当前实际仓位;若实际仓位超出 buffer 区间,则
breaks列为 True;
例如 BTP 当前多 3,而上界 buffer 为约 2.4(四舍五入为 2),因此被标记为 break; - 接着,在合约层面对比数据库与券商记录的仓位;
- 然后汇总三类“断点(break)”:
- instrument 层 vs 最优仓位;
- instrument 层 vs 合约层聚合;
- 券商 vs 数据库的合约层仓位;
- 最后给出“数据库中的成交记录”和“券商记录中的成交”,供人工视觉对比 —— 系统不会自动判定是否一致。
当出现“Instrument vs Contract”或“Broker vs Contract”的 break 时,通常需要用 interactive_order_stack 创建平衡交易来修复问题。
策略报告(Strategy report)
策略报告是针对单一策略定制的诊断报告:
它会加载该策略最近一次生成的回测文件,并从中输出各种诊断信息。
日常运行时会对所有策略执行;按需运行时可以指定全部或单个策略。
具体采用哪一份“策略报告函数”,由 defaults.yaml(或私有配置 YAML)中的 strategy_list/strategy_name/reporting_code/function 参数决定。
“经典”系统的默认报告函数为:sysproduction.strategy_code.report_system_classic.report_system_classic。
下面是一个示例(medium_speed_TF_carry 策略),原文中注释使用双引号:
| |
简要说明:
- 报告前半部分给出未加权预测、预测权重、加权预测的“矩阵形式”视图(示例中为节省空间省略);
- 接着展示基于
percentage_vol_target、基准货币与名义资金计算出来的“目标波动率”(年度与每日现金波动目标); - “Vol calculation” 表按 instrument 给出:日收益率标准差、价格、日/年波动率百分比;
- “Subsystem position” 表展示“如果整个系统资金都投在某个 instrument 上时,该 instrument 的位置”以及对应的波动缩放(Vol Scalar)、合成预测(Combined forecast)等;
- “Portfolio positions” 则将 subsystem position 与 instrument 权重(instr weight)、IDM(instrument diversification multiplier)结合,得到最终名义仓位;
- 最后,“Positions vs buffers” 展示了每个 instrument 的名义仓位、上下 buffer 以及“回测生成时的仓位”与“当前仓位”的对比。
风险报告(Risk report)
风险报告(Risk report)用于检查整体及分策略的风险暴露,并给出更细的 instrument 级风险分解。
报告每天运行一次,也可按需运行。示例如下:
| |
简要说明:
- 报告顶部给出“全策略组合”的年化波动率(标准差);
- 然后按策略分解风险,考虑到各策略获得的资金分配;
其中_ROLL_PSEUDO_STRATEGY仅用于生成滚动交易,因此不应承担任何风险;ETFHedge在此示例中也没有实际风险敞口; - “Instrument risk” 部分对每个 instrument 给出详细的风险计算(价格单位、百分比、基准货币现金、持仓相对资金的风险比例等);
- 最后,相关矩阵部分展示了各 instrument 收益率之间的相关性结构,用于判断分散化情况。
流动性报告(Liquidity report)
流动性报告用于检查各市场是否具备足够流动性以供交易。
更多背景讨论见这篇博客:https://qoppac.blogspot.com/2021/05/adding-new-instruments-or-how-i-learned.html。
作者设定的最低要求是:
- 每日“风险单位”成交额不少于 125 万美元;
- 每日合约成交量不少于 100 手。
报告使用最近 2 周的交易数据来估算相关指标(该窗口不可配置——在元旦后流动性较低的时期运行该报告时应格外小心)。
不满足上述阈值的 instruments,原则上应该考虑从组合中移除。
示例:
| |
第一部分按“合约成交量”排序,第二部分按“每日风险额”排序:
- 在风险维度排序中,风险低于 1.5M 美元的品种被视为“可能过于不够流动”;
- 示例中作者也用 “milk isn’t liquid :-)” 等注释提醒哪些市场看上去明显不够流动。
成本报告(Costs report)
成本报告用于检查配置文件中设置的买卖价差(bid/ask spread)成本是否足够保守。
报告会基于过去 250 个交易日的数据(可配置),并使用三种来源来估算实际成本:
- 下单前捕获的“半个买卖价差”(half spread,报告中的第一列);
- 从初始中间价(mid price)到实际成交价之间的实际价差(第二列);
- 调整后报价(adjusted quotes)所隐含的价差。
这些不同来源都会以“标准化 tick”的形式进行比较。
报告节选如下(仅展示尾部几行):
| |
阅读该报告时可以关注:
- 实际滑点是否显著低于配置中的成本(如 GOLD_micro 的注释);
- 是否存在“配置中有 instrument,但实际没有成本数据”的情况(ASX);
- 是否存在“配置了滑点,但没有采样或真实交易”的 instrument(KOSPI)等。
TODO:新增报告(TODO add new reports)
原文在此处留有 TODO,表示未来可能会新增更多类型的报告。
自定义定时报告生成(Customize scheduled report generation)
你可以自定义报告生成的配置。
例如,如果你希望将报告推送到一个 git 仓库(类似这个示例:https://github.com/robcarver17/reports),
就需要把默认的“通过邮件发送报告”改为“将报告保存为文件”。
这些文件会保存在 private_config.yaml 中 reporting_directory 所定义的目录下。
下面是一个自定义报告配置示例:
| |
可用报告的完整列表可以通过 dataReports 对象查询,例如:
| |