回测与用户指南(Backtesting)

回测流程、配置与诊断。

译文说明

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

这是将 pysystemtrade 用作回测平台的用户指南。在阅读本指南之前,你应该已经看过《介绍》

相关文档:

本指南分为四个部分。第一部分 How do I? 说明如何完成许多常见任务。第二部分 Guide 详细介绍代码中相关的组成部分,并解释如何修改或创建新的组件。第三部分 Processes 讨论那些跨越代码多个部分的流程。最后一部分 Reference 给出了方法和参数的列表。

目录

如何操作?

如何……在单个交易规则和单个品种上做试验

虽然这个项目主要是为了处理完整的交易系统,但你也可以在不构建系统的情况下做一些有限的试验。示例见《介绍》

如何……创建一个标准的期货回测

下面的代码会创建我书中第 15 章定义的 “staunch systems trader” 示例系统,使用提供的 csv 数据,并给出 DAX 市场上的持仓:

1
2
3
from systems.provided.futures_chapter15.basesystem import futures_system
system=futures_system()
system.portfolio.get_notional_position("DAX")

更多信息见标准期货系统

如何……创建一个带参数估计的期货回测

下面的代码会创建我书中第 15 章定义的 “staunch systems trader” 示例系统,使用提供的 csv 数据,并估计预测缩放因子(forecast scalar)、标的权重(instrument weights)、预测权重(forecast weights),以及标的分散化乘数和预测分散化乘数:

1
2
3
from systems.provided.futures_chapter15.estimatedsystem import futures_system
system=futures_system()
system.portfolio.get_notional_position("SOFR")

更多信息见带参数估计的期货系统

如何……查看回测的中间结果

下面的代码会给出标准期货回测中,DAX 期货的一条 EWMAC 规则的原始预测(缩放和截断之前的预测):

1
2
3
from systems.provided.futures_chapter15.basesystem import futures_system
system=futures_system()
system.rules.get_raw_forecast("DAX", "ewmac64_256")

要查看所有可能的中间结果,可以先用 print(system) 查看每个阶段的名称,然后对某个阶段调用 stage_name.methods()。或者参见这张表,查找标记为 D(诊断)的行。另一种方式是直接输入 system 查看所有阶段列表,然后用 system.stagename.methods() 查看某个阶段的所有方法(把 stagename 换成真实的阶段名称)。

如何……查看回测有多赚钱

1
2
3
4
5
6
7
from systems.provided.futures_chapter15.basesystem import futures_system
system=futures_system()
system.accounts.portfolio().stats() ## 查看一些统计量
system.accounts.portfolio().curve().plot() ## 画出账户净值曲线
system.accounts.portfolio().percent.curve().plot() ## 画出百分比账户净值曲线
system.accounts.pandl_for_instrument("US10").percent.stats() ## 输出 10 年期国债的百分比统计
system.accounts.pandl_for_instrument_forecast("SOFR", "carry").sharpe() ## 某条交易规则变体的夏普比率(Sharpe ratio)

关于还能得到哪些统计量,见相关指南部分

如何……修改回测参数

回测会按以下顺序查找配置信息:

  1. 配置对象中的元素
  2. 如果找不到,则在私有 YAML 配置中查找:/private/private_config.yaml(如果存在)
  3. 如果仍然找不到,则使用项目默认值

配置对象可以从 YAML 文件加载,也可以通过字典创建。这意味着你可以通过以下任一方式修改系统行为:

  1. 修改或新建配置 YAML 文件,把它读入,然后创建一个新的系统
  2. 在内存中修改配置对象,然后用它创建一个新的系统
  3. 在已有系统内部修改配置对象(高级用法)
  4. 创建一个私有配置 YAML:/private/private_config.yaml(如果你希望对所有回测做一个全局性的参数修改,这会很有用)
  5. 修改项目默认值(强烈不推荐)

所有可用配置选项的完整列表参见这张表

如果你使用方法 2 或 3,可以把配置保存为一个 YAML 文件。

选项 1:修改配置文件

本项目中的配置文件以 YAML 格式存储。如果你不熟悉 YAML 也不用担心;它本质上就是一种用纯文本构造嵌套 dictlist 和其他 Python 对象的方式。和 Python 一样,要注意缩进,它决定了嵌套层级。

你可以复制这个配置文件并进行修改,作为新的配置文件。比较好的做法是将其保存为 pysystemtrade/private/this_system_name/config.yaml(你需要先创建几个目录)。

然后,你需要创建一个新的系统,并让它指向这个新的配置文件:

1
2
3
4
5
from sysdata.config.configdata import Config
from systems.provided.futures_chapter15.basesystem import futures_system

my_config=Config("private.this_system_name.config.yaml")
system=futures_system(config=my_config)

关于在 pysystemtrade 中如何指定文件名,见这里

选项 2:修改配置对象并创建新系统

我们也可以直接从已加载的系统中获取配置对象,对其进行修改,然后用它创建一个新系统:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
from systems.provided.futures_chapter15.basesystem import futures_system
system=futures_system()
new_config=system.config

new_idm=1.1 ## 新的 IDM

new_config.instrument_div_multiplier=new_idm

## 下面是如何修改一个嵌套参数的示例
## 如果该元素在你的配置中还不存在:

system.config.volatility_calculation=dict(days=20)

## 如果它已经存在:
system.config.volatility_calculation['days']=20


system=futures_system(config=new_config)

如果你是交互式地“一边试一边改”,这种方法会很有用。

选项 3:在已有系统内修改配置对象(不推荐——高级)

如果你选择方法(3),就需要理解系统缓存以及默认值是如何处理的。要在系统内部直接修改配置对象,可以这样做:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
from systems.provided.futures_chapter15.basesystem import futures_system
system=futures_system()

## 我们对 system 做的任何操作都可能被缓存;
## 在系统看到新数值之前,缓存需要被清理……


new_idm=1.1 ## 新的 IDM
system.config.instrument_div_multiplier=new_idm

## 如果我们修改的是嵌套结构,为了避免清空默认值,
## 只修改其中的单个元素:
# 正确做法:
system.config.volatility_calculation['days']=20

# 不要这样做——它会抹掉 volatility_calculation 字典中其他所有元素:
# system.config.volatility_calculation=dict(days=20)


## 配置已经更新,但再次强调:任何用到它的东西
## 都需要从缓存中清除后再重新计算

由于我们没有创建一个新系统、也不需要从头重新计算所有东西,如果你知道自己在做什么,这种方法在测试系统的局部改动时会很有帮助。

选项 4:创建私有配置文件

你可以在 /private/private_config.yaml 中放置配置项来覆盖默认配置。这在你希望对某个参数做全局修改,而不是在每个配置文件里都重复写一遍时特别有用。这个文件中的内容会覆盖系统默认值,但会被回测的配置 YAML 文件再覆盖。当你把 pysystemtrade 用作实盘交易环境时,这个文件也会非常有用。

选项 5:修改项目默认值(强烈不推荐)

我并不建议修改默认值——首先,很多测试都会因此失败——但如果你确实想这样做,可以在这里找到更多信息。

如何……在不同的品种集合上运行回测

固定标的权重:你需要在配置中修改标的权重。只有被赋予权重的品种才会产生持仓。
估计标的权重:你需要修改配置中的 instruments 部分。

有两种简单的方式可以做到这一点——要么修改配置文件,要么修改系统中已经加载好的配置对象(关于如何修改配置参数,参见“修改回测参数”)。你还需要确保已经为任何新增的品种准备好了所需数据。详见下文“使用我自己的数据”

更换品种:修改配置文件

你可以通过复制这个配置文件来创建一个新的配置文件。比较好的做法是将其保存为 pysystemtrade/private/this_system_name/config.yaml(你需要先创建这个目录)。

对于固定标的权重,你可以修改配置中的这一部分:

1
2
3
4
5
6
7
8
instrument_weights:
    SOFR: 0.117
    US10: 0.117
    EUROSTX: 0.20
    V2X: 0.098
    MXP: 0.233
    CORN: 0.233
instrument_div_multiplier: 1.89

如果预测权重是按品种单独设定的,你也可能需要修改 forecast_weights

1
2
3
4
5
6
forecast_weights:
   SOFR:
     ewmac16_64: 0.21
     ewmac32_128: 0.08
     ewmac64_256: 0.21
     carry: 0.50

在这个阶段,你还需要重新计算分散化乘数(详见我书中的第 11 章)。见估计预测分散化乘数

对于估计标的权重,你需要修改以下配置:

1
instruments: ["SOFR", "US10", "EUROSTX", "V2X", "MXP", "CORN"]

注意,如果你从固定标的权重切换到估计标的权重(通过把 system.config.use_instrument_weight_estimates 设为 True),那么 system.config.instrument_weights 中选择的品种集合会被忽略;如果你仍然想使用相同的一组品种,就需要显式指定:

1
system.config.instruments = list(system.config.instrument_weights.keys())

(IDM 会自动重新估计。)

如果你为每个品种指定了不同的规则列表,也可能需要修改这一部分:

1
2
rule_variations:
     SOFR: ['ewmac16_64','ewmac32_128', 'ewmac64_256', 'carry']

然后,你需要创建一个新的系统,并让它指向新的配置文件:

1
2
3
4
5
6
from sysdata.config.configdata import Config

my_config=Config("private.this_system_name.config.yaml")

from systems.provided.futures_chapter15.basesystem import futures_system
system=futures_system(config=my_config)

关于在 pysystemtrade 中如何指定文件名,见这里

更换品种:修改配置对象

我们也可以直接修改系统中的配置对象:

对于固定标的权重:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
from systems.provided.futures_chapter15.basesystem import futures_system
system=futures_system()
new_config=system.config

new_weights=dict(SP500=0.5, KR10=0.5) ## 新的标的权重
new_idm=1.1 ## 新的 IDM

new_config.instrument_weights=new_weights
new_config.instrument_div_multiplier=new_idm

system=futures_system(config=new_config)

对于估计标的权重:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
from systems.provided.futures_chapter15.estimatedsystem import futures_system
system=futures_system()
new_config=system.config

new_config.instruments=["SP500", "KR10"]

del(new_config.rule_variations) ## 表示所有品种都会使用所有交易规则

# 如果你希望给不同品种指定不同的规则集合,可以加上这一步(可选)
new_config.rule_variations=dict(SP500=['ewmac16_64','carry'], KR10=['ewmac32_128', 'ewmac64_256', 'carry'])

system=futures_system(config=new_config)

如何……只在最近一段数据上运行回测

你需要在 YAML 回测配置文件中设置 start_date

1
2
## 注意必须使用这种格式
start_date: '2000-01-19'

如何……在所有可用品种上运行回测

如果配置中没有 instrument_weightsinstruments 这两个元素,那么回测会在数据里所有可用品种上运行。

如何……把部分品种从回测中排除

参见品种文档

如何……禁止某些品种具有正的标的权重

参见品种文档

如何……创建我自己的交易规则

你最终应该阅读一下相关的指南部分 ‘rules’,其中包含的内容远比这里简短说明的要多。

编写函数

一个交易规则由以下部分组成:

  • 一个函数
  • 一些数据(以位置参数形式传入)
  • 一些可选的控制参数(以关键字参数形式传入)

因此函数应该类似这样:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
def trading_rule_function(data1):
   ## 对 data1 做一些处理

def trading_rule_function(data1, arg1=default_value):
   ## 对 data1 做一些处理
   ## 由 arg1 的取值来控制

def trading_rule_function(data1, data2):
   ## 对 data1 和 data2 做一些处理

def trading_rule_function(data1, data2, arg1=default_value, arg2=default_value):
   ## 对 data1 做一些处理
   ## 由 arg1 和 arg2 的取值来控制

……等等。

函数必须返回一个 Tx1 的 pandas DataFrame

把交易规则加入配置

我们既可以修改 YAML 配置文件,也可以修改已经加载到内存中的配置对象。更多细节见“修改回测参数”。如果你想通过 YAML 文件使用新规则,需要先把函数写入某个 .py 模块,这样就可以通过字符串引用它(对于内存中的配置对象也可以采用这种方式)。

例如,下面这样导入的规则:

1
from systems.futures.rules import ewmac

也可以通过字符串 systems.futures.rules.ewmac 来引用。

此外,规则所需的数据列表也会是一些字符串,这些字符串指向系统对象中的方法。比如获取日价格可以使用 system.rawdata.daily_prices(instrument_code)(系统中所有数据方法的完整列表见阶段方法表,或者直接输入 system.rawdata.methods()system.rawdata.methods())。在交易规则的配置里,这个方法会写成 "rawdata.daily_prices"

如果没有在配置中指定数据,系统会默认传入单一数据项——该品种的价格。最后,如果某些 other_arg 关键字参数缺失,则函数会使用它们在定义时的默认值。

在这个阶段,我们也可以移除不想使用的交易规则。我们还应该修改预测缩放因子(见预测缩放估计)、预测权重,以及通常还要修改预测分散化乘数(见估计预测分散化乘数)。如果你在使用带有估计权重和缩放因子的预制期货系统,这些步骤会自动完成。

如果你使用的是固定数值(默认情况),那么如果没有为某条规则配置预测缩放因子,该规则会使用 1.0 作为缩放因子。如果在配置中不包含预测权重,系统会默认使用等权重。但如果你在配置中给出了预测权重,却漏掉了新规则,那么这条新规则就不会参与合成预测。

下面是一个 EWMAC 规则新变体的示例。该规则使用两类数据——价格(对期货做过拼接处理)和预先计算好的波动率估计。

YAML 示例:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
trading_rules:
  .... existing rules ...
  new_rule:
     function: systems.futures.rules.ewmac
     data:
         - "rawdata.daily_prices"
         - "rawdata.daily_returns_volatility"
     other_args:
         Lfast: 10
         Lslow: 40
#
#
## 以下部分用于固定的缩放因子、权重和分散化乘数:
#
forecast_scalars:
  ..... existing rules ....
  new_rule=10.6
#
forecast_weights:
  .... existing rules ...
  new_rule=0.10
#
forecast_div_multiplier=1.5
#
#
## 如果你要估计这些量,则使用下面这些配置:
#
use_forecast_weight_estimates: True
use_forecast_scale_estimates: True
use_forecast_div_mult_estimates: True

rule_variations:
     SOFR: ['ewmac16_64','ewmac32_128', 'ewmac64_256', 'new_rule']
#
# 或者如果所有品种使用相同的规则集合:
#
rule_variations: ['ewmac16_64','ewmac32_128', 'ewmac64_256', 'new_rule']
#

Python 示例(假设我们已经有一个待修改的配置对象 config):

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32

from systems.trading_rules import TradingRule

# 方法一
new_rule = TradingRule(
   dict(function="systems.futures.rules.ewmac", data=["rawdata.daily_prices", "rawdata.daily_returns_volatility"],
        other_args=dict(Lfast=10, Lslow=40)))

# 方法二——适合于“现写”的函数
from systems.futures.rules import ewmac

new_rule = TradingRule(dict(function=ewmac, data=["rawdata.daily_prices", "rawdata.daily_returns_volatility"],
                            other_args=dict(Lfast=10, Lslow=40)))

## 两种方法都一样——接下来要修改配置:
config.trading_rules['new_rule'] = new_rule

## 如果你使用的是固定权重和缩放因子:

config.forecast_scalars['new_rule'] = 7.0
config.forecast_weights = dict(...., new_rule=0.10)  ## 所有已有的预测权重也需要重新调整
config.forecast_div_multiplier = 1.5

## 如果你使用的是估计值:

config.use_forecast_scale_estimates = True
config.use_forecast_weight_estimates = True
use_forecast_div_mult_estimates: True

config.rule_variations = ['ewmac16_64', 'ewmac32_128', 'ewmac64_256', 'new_rule']
# 或者为不同品种指定不同的规则集合:
config.rule_variations = dict(SP500=['ewmac16_64', 'ewmac32_128', 'ewmac64_256', 'new_rule'], US10=['new_rule', ....)

无论你采用哪种方式得到新的配置对象,接下来只需在系统中使用它,例如:

1
2
3
4
## 用新配置创建一个系统

from systems.provided.futures_chapter15.basesystem import futures_system
system=futures_system(config=config)

如何……使用不同的数据或品种

回测默认使用的,是来自 CSV 文件的期货拼接价格、外汇价格以及合约相关数据。我打算会定期更新这些数据,并尽量在每次发布时保持它们大致“不过时”。数据存放在 data/futures 目录。另见“2025 年 9 月更新说明”

如果你愿意,可以自己更新这些数据。注意保存为格式正确的 CSV,否则 pandas 会报错。可以这样检查文件格式是否正确:

1
2
3
import pandas as pd
test=pd.read_csv("filename.csv")
test

你也可以为新增的品种添加新的文件。务必保持文件格式和表头名称一致。

你也可以为 CSV 文件创建你自己的目录。例如,假设你希望从 pysystemtrade/private/system_name/adjusted_price_data 这个目录读取经调整后的价格数据,可以这样使用:

1
2
3
4
5
from sysdata.sim.csv_futures_sim_data import csvFuturesSimData
from systems.provided.futures_chapter15.basesystem import futures_system

data=csvFuturesSimData(csv_data_paths=dict(csvFuturesAdjustedPricesData = "private.system_name.adjusted_price_data"))
system=futures_system(data=data)

注意,这里我们在项目内部使用的是 Python 风格的 “点号” 引用,而不是直接给出实际的文件路径。关于在 pysystemtrade 中如何指定文件名,见这里

csv_data_paths 中可以使用的 key 完整列表如下:

  • csvFuturesInstrumentData(品种配置与成本)
  • csvFuturesMultiplePricesData(当前合约、下一合约以及展期合约的价格)
  • csvFuturesAdjustedPricesData(拼接回溯的调整价格)
  • csvFxPricesData(外汇价格)
  • csvRollParametersData(展期配置)

注意,调整价格和 carry 数据不能放在同一个目录下,因为它们使用的是相同的文件格式。

关于使用 CSV 文件的更多细节,见这里

如果你想把数据存储在 MongoDB 数据库中,需要使用不同的数据对象

如果你希望从其他地方获取数据(例如数据库、Yahoo Finance、券商、Quandl……),你需要创建你自己的 Data 对象

如果你希望使用不同类型的数据(例如股票 EP 比率、利率……),你也需要创建你自己的 Data 对象

如果你想更深入地了解数据存储问题,参见《处理期货数据》

如何……保存我的工作

为了保持条理,有一个好习惯是把你的所有工作保存在类似 pysystemtrade/private/this_system_name/ 这样的目录下(你需要先创建该目录)。如果你计划向 GitHub 提交代码,请务必小心,不要把 private 目录一起提交(可以参考这篇文章)。

你可以把系统缓存的内容保存下来,这样下次再继续研究这个系统时,就不必重新做所有计算了(在重新加载缓存之前,你可能想先阅读系统缓存与 pickling)。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
from systems.provided.futures_chapter15.basesystem import futures_system

system = futures_system()
system.accounts.portfolio().sharpe() ## 会执行一大堆计算,并把结果保存到缓存中

system.cache.pickle("private.this_system_name.system.pck") ## 文件扩展名可以随意

## 在新的会话中
from systems.provided.futures_chapter15.basesystem import futures_system

system = futures_system()
system.cache.unpickle("private.this_system_name.system.pck")

## 下面这步会快很多,并复用之前的计算结果
# 只有比较复杂的记账 P&L 对象不会被存入缓存
system.accounts.portfolio().sharpe()

你也可以把配置对象保存为 YAML 文件,见“保存配置”

Guide

本“指南”部分会更详细地说明系统各组成部分是如何工作的:

  1. Data 对象
  2. Config 对象和 YAML 文件
  3. System 对象
  4. 系统内部的 Stages

每一部分都分成若干层次,难度逐步增加:从使用已经提供的标准对象,一直到编写你自己的实现。

Data

Data 对象用于向系统提供数据。每个数据对象负责某一种特定“类别”的数据(通常按资产类别划分,例如期货),并来自某个特定“来源”(例如 CSV 文件、数据库等)。

Using the standard data objects

当前版本的系统中提供了两类具体的数据对象——csvFuturesSimData(使用 CSV 文件)和 dbFuturesSimData(使用数据库存储)。

详见《处理期货数据》

Generic data objects

你可以直接导入并使用数据对象:

下面这些命令对所有数据对象都适用——这里只是用 csvFuturesSimData 作为示例。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
from sysdata.sim.csv_futures_sim_data import csvFuturesSimData

data=csvFuturesSimData()

## 取出数据
data.methods() ## 列出所有方法

data.get_raw_price(instrument_code)
data[instrument_code] ## 等价于 get_raw_price

data.get_instrument_list()
data.keys() ## 同样可以得到品种列表

data.get_value_of_block_price_move(instrument_code)
data.get_instrument_currency(instrument_code)
data.get_fx_for_instrument(instrument_code, base_currency) # 获取品种货币与基准货币之间的汇率

或者在系统中使用:

1
2
3
4
5
## 与 system 配合使用
from systems.provided.futures_chapter15.basesystem import futures_system
system=futures_system(data=data)

system.data.get_instrument_currency(instrument_code) # 以及其他类似方法

(注意:在交易规则中指定数据项时,应省略 system,例如使用 data.get_raw_price。)

如果你在配置中设置了 start_date,那么只会展示该日期之后的数据子集:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
## 与 system 配合使用
from systems.provided.futures_chapter15.basesystem import futures_system
system=futures_system(data=data)

# 也可以在 YAML 文件中做同样的设置。注意格式必须一致
system.config.start_date = '2000-01-19'

## 或者使用 datetime(显然不能在 YAML 中这么写)
import datetime
system.config.start_date = datetime.datetime(2000,1,19)

The csvFuturesSimData object

csvFuturesSimData 对象的使用方式如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
from sysdata.sim.csv_futures_sim_data import csvFuturesSimData

## 使用默认目录
data=csvFuturesSimData()

## 或者通过传入一个字典来指定不同的目录
data=csvFuturesSimData(csv_data_paths = dict(key_name = "pathtodata.with.dots"))

# 允许使用的 key 名包括:
# 'csvFxPricesData'(外汇价格)、
# 'csvFuturesMultiplePricesData'(carry 与远期价格)、
# 'csvFuturesAdjustedPricesData' 和 'csvFuturesInstrumentData'(配置与成本)。
# 如果未提供某个 key,则会使用系统默认路径。

# 示例:覆盖默认设置,改用 /psystemtrade/private/data/fxdata/ 中的 FX 数据:

data=csvFuturesSimData(csv_data_paths = dict(csvFxPricesData="private.data.fxdata"))

# 警告:不要把 multiple_price_data 和 adjusted_price_data 放在同一目录下,
#       因为它们使用相同的文件名!

## 取出数据
data.methods() ## 会列出任何额外的方法
data.get_instrument_raw_carry_data(instrument_code) ## 期货特有的数据

## 与 system 配合使用
from systems.provided.futures_chapter15.basesystem import futures_system
system=futures_system(data=data)
system.data.get_instrument_raw_carry_data(instrument_code)

每个相关路径下都必须包含以下几类 CSV 文件(其中 code 表示 instrument_code):

  1. 静态配置与成本数据——instrument_config.csv。字段:Instrument, Pointsize, AssetClass, Currency。关于成本,额外字段包括:Slippage, PerBlock, Percentage, PerTrade。更多细节见成本
  2. 展期参数数据。细节见《存储期货与即期外汇数据》
  3. 调整价格数据——code.csv(例如 SP500.csv),字段:DATETIME, PRICE
  4. Carry 与远期数据——code.csv(例如 AEX.csv),字段:DATETIME, PRICE,CARRY,FORWARD,CARRY_CONTRACT PRICE_CONTRACT, FORWARD_CONTRACT
  5. 货币数据——ccy1ccy2fx.csv(例如 AUDUSDfx.csv),字段:DATETIME, FXRATE

DATETIME 必须是 pandas.to_datetime 能够解析的格式。注意,(2)中的价格是连续拼接后的价格(见波动率计算),而(3)中的价格列则是当前正在交易的那张合约的价格。

至少,你需要为每个品种的货币相对于默认货币(定义为 "USD")准备一份货币文件;还需要为你进行交易的账户货币准备一份货币文件(例如,对英国投资者来说,需要一个 GBPUSDfx.csv 文件)。如果存在交叉汇率文件,系统会优先使用;否则会用相应的 USD 汇率推导隐含交叉汇率。

可以在子目录 pysystemtrade/data/futures 中查看可供修改的示例数据:

更多信息见《期货数据》文档

The dbFuturesSimData object

dbFuturesSimData 是一个 simData 对象,它从 MongoDB(静态数据)和 Parquet(时间序列)中获取数据。对于实盘交易,这种方式更合适。对于生产代码以及存储大量数据(例如逐合约的期货数据),我们通常需要比 CSV 文件更健壮的方案。

Setting up MongoDB and Parquet

显然,你需要先确保有一个正在运行的 MongoDB 实例。你可能会发现机器上已经有一个在跑了;在 Linux 下可以用 ps wuax | grep mongo 找到相关进程,然后将其结束。

由于 MongoDB 数据并未随 GitHub 仓库一起提供,在使用本方案之前,你需要先把所需数据写入 Mongo 和 Parquet。你可以按“期货数据工作流” 从零开始构建;或者运行下面这些脚本,将现有 GitHub CSV 文件中的数据拷贝过去:

当然,你也可以混合使用这两种方式。

Using dbFuturesSimData

准备好数据之后,只需要把默认的 CSV 数据对象替换掉即可:

1
2
3
4
5
6
7
8
9
from systems.provided.futures_chapter15.basesystem import futures_system
from sysdata.sim.db_futures_sim_data import dbFuturesSimData

# 使用默认数据库
data = dbFuturesSimData()

# 与 system 配合使用
system = futures_system()
print(system.accounts.portfolio().sharpe())

Arctic

本项目的早期版本使用 Arctic 来存储时间序列数据。自 2023 年 11 月起,Parquet 成为默认方案。参见更换原因说明切换指引以及调度配置所需的变更

原始的 Arctic 项目 已不再维护——开发工作已迁移至 ArcticDB

Parquet 和 Arctic 是完全不同的东西:Parquet 是一种高效的列式数据文件格式,而 Arctic 是一个薄封装,用于在 MongoDB 数据库中存储时间序列数据。

尽管 Parquet 本身不是数据库,在本项目中,数据被封装在一个抽象层后面,对上层来说就像在使用数据库一样。

Backtesting with Arctic instead of Parquet

仍然可以使用旧的方案。对于回测,只需在 sysdata.sim.db_futures_sim_data.py 中把所用的类改为 Arctic 版本,而不是 Parquet 版本:

1
2
3
4
5
6
7
8
use_sim_classes = {
    FX_DATA: arcticFxPricesData,
    ROLL_PARAMETERS_DATA: csvRollParametersData,
    FUTURES_INSTRUMENT_DATA: csvFuturesInstrumentData,
    FUTURES_MULTIPLE_PRICE_DATA: arcticFuturesMultiplePricesData,
    FUTURES_ADJUSTED_PRICE_DATA: arcticFuturesAdjustedPricesData,
    STORED_SPREAD_DATA: mongoSpreadCostData,
}

同时,你需要把与 Arctic 相关的 import 语句从注释中恢复出来。

Creating your own data objects

在阅读本节之前,你应该已经对 Python 面向对象编程风格比较熟悉。

simData() 对象是用于回测的所有数据对象的基类。在此之上,我们派生出按数据类型区分的子类,例如期货数据对象。然后再从这些类型特定的类继承,构造具体的数据来源实现,例如基于 CSV 文件的 csvFuturesSimData()

建议遵循一种命名约定:sourceTypeSimData。例如,如果我们有单只股票的数据存储在数据库中,可以定义 class EquitiesSimData(simData),再定义 class dbEquitiesSimData(EquitiesSimData)

因此,你需要考虑自己究竟是要一种新的“数据类型”、一种新的“数据来源”,还是两者都要。同时你也可以选择扩展已有的类。例如,如果你想为期货添加一些基本面数据,可以定义 class fundamentalFuturesSimData(futuresSimData),然后再从它继承,针对具体的数据来源实现。

这看上去有点麻烦,很容易让人想直接从 simData() 继承了事。但一旦你的系统运行起来,能方便地切换多个数据源会非常有价值,而按这种方式拆分可以保证同一类数据在不同来源下仍然保持一致的 API。

在修改或编写自己的数据对象之前,建议先阅读期货数据文档,了解 csvFuturesSimData() 是如何构建的。

The Data() class

下面是你很可能需要重写的一些方法:

  • get_raw_price:返回 Tx1 的 pandas DataFrame
  • get_instrument_list:返回 list[str]
  • get_value_of_block_price_move:返回 float
  • get_raw_cost_data:返回一个包含成本数据的 dict
  • get_instrument_currency:返回 str
  • _get_fx_data(currency1, currency2):返回一个 Tx1 的 pandas DataFrame,表示汇率时间序列
  • get_rolls_per_year:返回 int

你不应该重写 get_fx_for_instrument,也不应该重写任何其他与 FX 相关的私有方法。一旦你实现了 _get_fx_data 方法,Data 基类中的方法就会协同工作,当外部对象调用 get_fx_for_instrument() 时,能够返回正确的汇率,包括处理和推导交叉汇率。

同样地,你也不应该重写 daily_prices

最后,数据方法本身不应该做任何缓存。缓存 是在 system 类内部完成的。

Configuration

配置(config)对象决定了系统的行为。配置对象本身很简单:它包含若干属性,这些属性要么是单个参数,要么是嵌套的一组参数。

创建配置对象

创建配置对象主要有几种方式:

  1. 通过字典在交互环境中创建
  2. 从 YAML 文件加载
  3. 从“预制”(pre‑baked)系统中获取
  4. 通过一个列表把多个配置拼在一起

1)通过字典创建配置对象

1
2
3
4
from sysdata.config.configdata import Config

my_config_dict=dict(optionone=1, optiontwo=dict(a=3.0, b="beta", c=["a", "b"]), optionthree=[1.0, 2.0])
my_config=Config(my_config_dict)

理论上字典中可以嵌套任何东西,但如果像上面那样随便放点乱七八糟的内容,其实没什么用!配置选项那一节解释了系统会用到哪些配置项。

2)从文件创建配置对象

下面这个简单的文件可以复现上面字典示例中“没什么用”的配置:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
optionone: 1
optiontwo:
  a: 3.0
  b: "beta"
  c:
    - "a"
    - "b"
optionthree:
  - 1.0
  - 2.0

注意,与 Python 类似,YAML 文件中的缩进表示嵌套层级。如果你想更深入了解 YAML,可以参考这里

1
2
from sysdata.config.configdata import Config
my_config=Config("private.filename.yaml") ## 假设文件位于 "pysystemtrade/private/filename.yaml"

关于在 pysystemtrade 中如何指定文件名,见这里

从理论上讲,配置顶层必须是一个字典,但其内部可以嵌套任意内容;不过实际使用中只需要 strfloatintlistdict 这几种类型即可(如果你是 PyYAML 高手,也可以放 tuple 等其他 Python 对象,但 YAML 看起来会比较难看)。

你应该在嵌套层级上遵循默认配置的结构,否则默认值无法正确填充。

关于可用的配置选项,见配置选项一节。

3)从预制系统创建配置对象

1
2
3
from systems.provided.futures_chapter15.basesystem import futures_system
system=futures_system()
new_config=system.config

在底层,这实质上等价于从 /systems/provided/futures_chapter15/futuresconfig.yaml 获取配置。

用这种方式创建的配置,会自动填充所有默认值

4)通过列表创建配置对象

我们也可以把一个列表传给 Config(),其中每个元素要么是一个字典,要么是一个文件名。例如,可以用前面简单文件名的例子来这么做:

1
2
3
4
from sysdata.config.configdata import Config

my_config_dict=dict(optionfour=1, optionfive=dict(one=1, two=2.0))
my_config=Config(["filename.yaml", my_config_dict])

注意,如果多个配置中存在同名 key,那么列表中靠后的配置会覆盖前面的配置。

这在某些场景下很有用,比如你只想“临时调整”标的权重,而其它配置保持不变。

5)从 CSV 文件生成配置文件

有时用 CSV 文件来指定某些参数,然后再把它们导入 YAML 会更方便。如果你想这么做,可以使用如下函数:

1
2
3
from sysinit.configtools.csvweights_to_yaml import instr_weights_csv_to_yaml  # 标的权重
from sysinit.configtools.csvweights_to_yaml import forecast_weights_by_instrument_csv_to_yaml  # 各品种的预测权重
from sysinit.configtools.csvweights_to_yaml import forecast_mapping_csv_to_yaml # 各品种的预测映射

这些函数会生成对应的 YAML 文件,你可以把这些内容拷贝到已有的配置文件中。

项目默认值与私有配置

很多(但不是全部)配置参数都有默认值:在配置对象中缺少某个参数时,系统会使用默认值。这些默认值保存在 defaults.yaml 文件 中。配置选项一节会说明默认值是什么,以及它们被用在什么地方。

我不建议你修改这些默认值。更好的做法是:在每个系统的配置文件中写明你想要的设置;如果你希望把某些设置应用于所有回测,可以使用单独的私有配置文件。

如果存在 /private/private_config.yaml 文件,它就会被当作私有配置文件。另一种做法是把私有配置放在项目外部的目录中

大致流程是:每当某个配置对象被传入系统时,如果存在私有配置文件,就先把其中的元素添加进配置对象;然后再把 defaults.yaml 中的剩余缺失元素补齐。

当你修改某些函数时如何处理默认值

在某些地方,你可以替换用来执行某个计算的函数,比如波动率估计函数(不包括交易规则——交易规则的函数更换方式完全不同)。如果你仍然使用原来相同的函数参数,这会非常简单;但如果你修改了参数,就需要同时修改项目的 defaults.yaml。建议保留原有参数,再额外添加新的不同名字的参数,避免不小心把系统搞挂。

默认值与私有配置的工作方式

当配置对象被加入系统时,config 类会自动填补原始配置中缺失但存在于 (i) 私有 YAML 文件 和 (ii) 默认 YAML 文件中的参数。例如,如果配置中没有 forecast_scalar,系统会使用默认值 1.0。对于顶层是 liststrintfloat 的配置项,这一逻辑也是类似的。

如果配置中某个字段本身是 dict,而默认 YAML 中对应字段也是 dict,那么默认文件里任何缺失的 key 都会被补上(例如 config.forecast_div_mult_estimate 是一个字典时,默认 YAML 中存在但配置中缺失的 key 会被加入)。对于嵌套字典也同样适用:如果 config.instrument_weight_estimate['correlation_estimate'] 中缺少某些 key,这些 key 会从默认文件中补齐。

如果某个字段在配置中是 dict 或嵌套 dict,而在默认文件中不是(或者反过来),则不会替换已有值,并且可能导致各种问题。因此最好让你的配置文件和默认文件在结构上保持一致(至少对那些你打算修改的项目要一致)。这也是“增加新参数而保留旧参数”的另一个理由。

需要注意的是,配置对象在传入系统之前和之后通常会不同;传入系统后,它会被填上默认值。

1
2
3
from sysdata.config.configdata import Config
my_config=Config()
print(my_config) ## 空配置

输出类似:

1
 Config with elements:

现在,如果把它放入一个系统中:

1
2
3
4
5
from systems.provided.futures_chapter15.basesystem import futures_system
system=futures_system(config=my_config)

print(system.config) ## 充满默认值
print(my_config) ## 同一个对象

可能会看到类似输出:

1
 Config with elements: average_absolute_forecast, base_currency, buffer_method, buffer_size, buffer_trade_to_edge, forecast_cap, forecast_correlation_estimate, forecast_div_mult_estimate, forecast_div_multiplier, forecast_scalar, forecast_scalar_estimate, forecast_weight_estimate, instrument_correlation_estimate, instrument_div_mult_estimate, instrument_div_multiplier, instrument_weight_estimate, notional_trading_capital, percentage_vol_target, use_SR_costs, use_forecast_scale_estimates, use_forecast_weight_estimates, use_instrument_weight_estimates, volatility_calculation

需要注意:仅有这些默认项还不足以构建一个可运行的交易系统,因为默认配置不会自动填充交易规则:

1
system.accounts.portfolio()

可能会报错:

1
2
# 省略完整错误堆栈
Exception: A system config needs to include trading_rules, unless rules are passed when object created

查看配置参数

无论是通过 YAML 文件还是在交互环境中构建字典,最终我们都会得到一个字典。顶层字典中的 key 会变成 config 对象的属性,我们可以通过字典 key 或列表下标来访问嵌套的内容。例如,使用前面那个简单配置:

1
2
3
my_config.optionone
my_config.optiontwo['a']
my_config.optionthree[0]

修改配置参数

同样地,修改配置也很直接。仍以前面的简单配置为例:

1
2
3
my_config.optionone=1.0
my_config.optiontwo['d']=5.0
my_config.optionthree.append(6.3)

你也可以新增顶层配置项:

1
2
my_config.optionfour=20.0
setattr(my_config, "optionfour", 20.0) ## 如果你更喜欢这种写法

或者删除它们:

1
del(my_config.optionone)

在真实配置中,修改嵌套参数时需要格外小心:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
config.instrument_div_multiplier=1.1 ## 非嵌套,没问题

## 以下示例展示如何修改一个嵌套参数
## 如果该元素在你的配置中尚不存在,
## 这么做没问题;但如果它已经存在,
## 这样写会覆盖掉该字段下所有其他参数——千万不要!

config.volatility_calculation=dict(days=20)

## 如果它已经存在,可以改为只更新某个 key:
config.volatility_calculation['days']=20

尤其是当你修改的配置已经被包含在系统中时要特别注意,因为这时候它已经填充了所有默认值:

1
2
3
4
5
6
7
8
system.config.instrument_div_multiplier=1.1 ## 非嵌套,没问题

## 如果我们要修改嵌套项,应该只改其中一个元素,避免清空默认值:
# 正确方式:
system.config.volatility_calculation['days']=20

# 不要这样做:
# system.config.volatility_calculation=dict(days=20)

在系统中使用配置

当你对配置内容满意后,可以把它用于系统:

1
2
from systems.provided.futures_chapter15.basesystem import futures_system
system=futures_system(config=my_config)

要注意:只有当配置被包含进系统时,私有配置和默认值才会被真正填充。

引入你自己的配置选项

如果你开发了自己的阶段(stage)或修改了现有阶段,可能需要增加新的配置选项。你的代码里大致需要像这样使用它们:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13

## 假设你的配置项叫 my_config_item,在相关方法中:

 parameter=system.config.my_config_item

 ## 也可以使用嵌套配置项,例如按 instrument_code 作为 key 的字典(或者嵌套列表)
 parameter=system.config.my_config_dict[instrument_code]

 ## 列表同样适用:

 parameter=system.config.my_config_list[1]

 ## (注意:也可以使用 tuple,但 YAML 会比较难看,所以不建议这么做。)

然后,你需要在配置文件中加入类似下面的内容:

1
2
3
4
5
6
7
my_config_item: "ni"
my_config_dict:
   US10: 45.0
   US5: 0.10
my_config_list:
   - "first item"
   - "second item"

类似地,如果你希望为新参数使用项目默认值,也需要把它们加入 defaults.yaml 文件。在此之前请确保你理解默认值是如何工作的

保存配置

你也可以把配置对象保存为 YAML 文件:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
from systems.provided.futures_chapter15.basesystem import futures_system
import yaml
from syscore.fileutils import resolve_path_and_filename_for_package

system = futures_system()
my_config = system.config

## 在这里对 my_config 做一些修改

filename = resolve_path_and_filename_for_package("private.this_system_name.config.yaml")

with open(filename, 'w') as outfile:
   outfile.write(yaml.dump(my_config, default_flow_style=True))

这在你“折腾”回测配置、并希望记录已经做过的修改时非常有用。需要注意的是,这样保存时会把交易规则的函数以函数对象形式写入 YAML;这可能无法正常工作,而且会让 YAML 非常难看。所以更好的做法是使用字符串来定义规则函数(详见规则一节)。

你也可以把最终优化得到的参数保存为固定权重,用于实盘交易:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
# 假设 system 已经包含了带有估计值的系统
from systems.diagoutput import systemDiag

sysdiag = systemDiag(system)
sysdiag.yaml_config_with_estimated_parameters('someyamlfile.yaml',
                                              attr_names=['forecast_scalars',
                                                                  'forecast_weights',
                                                                  'forecast_div_multiplier',
                                                                  'forecast_mapping',
                                                                  'instrument_weights',
                                                                  'instrument_div_multiplier'])

根据你想导出的内容调整 attr_names 列表。随后你可以把生成的 YAML 文件合并到用于回测的 YAML 文件中。别忘了把 use_forecast_div_mult_estimatesuse_forecast_scale_estimatesuse_forecast_weight_estimatesuse_instrument_div_mult_estimatesuse_instrument_weight_estimates 这些标志关闭。预测映射(forecast mapping)的标志不用改,因为默认情况下不会对其做估计。

修改配置类

一般不需要修改配置类本身,因为它被设计得非常轻量而且足够灵活。

System

一个 system 对象实例由若干阶段(stages)、一些数据(data),以及通常还会有一个**配置(config)**对象组成。

预制系统(Pre‑baked systems)

我们可以基于现有的“预制系统”(pre‑baked system)来创建一个系统。这类系统已经包含了一套准备好的数据、阶段列表以及配置。

1
2
from systems.provided.futures_chapter15.basesystem import futures_system
system=futures_system()

我们可以覆盖这些默认设置,在这样的系统中使用我们自己的数据和/或配置:

1
2
3
system=futures_system(data=my_data)
system=futures_system(config=my_config)
system=futures_system(data=my_data, config=my_config)

最后,我们还可以创建自己的交易规则对象,并将其传入。这在交互式模型开发中很有用。例如,假设我们刚刚“现写”了一条新规则:

1
2
my_rules=dict(rule=a_new_rule)
system=futures_system(trading_rules=my_rules) ## 如果使用固定预测权重,这里很可能也需要一个新的配置

第 15 章的期货系统

这个系统实现了我书中第 15 章的框架。

1
2
from systems.provided.futures_chapter15.basesystem import futures_system
system=futures_system()

本质上,它等价于如下代码:

1
2
3
4
5
6
7
8
data=csvFuturesSimData() ## 或者用户传入的 data 对象
config=Config("systems.provided.futures_chapter15.futuresconfig.yaml") ## 或者用户传入的 config 对象

## 用户可以选择性地传入 trading_rules(能被解析为一组交易规则的对象);
## 如果留空(默认为 None),则使用配置中的规则。

system=System([Account(), Portfolios(), PositionSizing(), RawData(), ForecastCombine(),
                   ForecastScaleCap(), Rules(trading_rules)], data, config)

第 15 章的估计系统

这个系统同样实现了我书中第 15 章的框架,但会额外估计预测缩放因子、标的和预测的分散化乘数,以及标的权重与预测权重。

1
2
from systems.provided.futures_chapter15.estimatedsystem import futures_system
system=futures_system()

本质上,它等价于如下代码:

1
2
3
4
5
6
7
8
data=csvFuturesSimData() ## 或者用户传入的 data 对象
config=Config("systems.provided.futures_chapter15.futuresestimateconfig.yaml") ## 或者用户传入的 config 对象

## 用户可以选择性地传入 trading_rules(能被解析为一组交易规则的对象);
## 如果留空(默认为 None),则使用配置中的规则。

system=System([Account(), Portfolios(), PositionSizing(), RawData(), ForecastCombine(),
                   ForecastScaleCap(), Rules(trading_rules)], data, config)

与标准系统相比,关键的配置差异在于以下这些“估计参数”:

  • use_forecast_scale_estimates
  • use_forecast_weight_estimates
  • use_instrument_weight_estimates
  • use_forecast_div_mult_estimates
  • use_instrument_div_mult_estimates

……全部被设为 True

警告:通过修改这些以 use_*_estimates 命名的估计参数,在运行过程中“动态切换”系统是否使用估计值时要格外小心。

使用 system 对象

system 对象本身并不做太多事情,它主要提供:

  • 对子阶段(child stages)的访问;
  • 对缓存(cache)的访问;
  • 若干少量自身的方法。

所有子阶段都以属性的形式挂在父级 system 上。

在系统中访问子阶段、数据和配置

例如,要获取投资组合层面的“名义”持仓(notional position),它位于名为 portfolio 的子阶段中:

1
system.portfolio.get_notional_position("SOFR")

我们也可以访问系统中数据对象上的方法:

1
system.data.get_raw_price("SOFR")

要查看系统及其各阶段上的所有方法列表,参见阶段方法表。或者直接:

1
2
3
system ## 列出所有阶段
system.accounts.methods() ## 列出某个阶段中的所有方法
system.data.methods() ## 对数据对象同样有效

我们还可以访问或修改配置对象中的元素:

1
2
system.config.trading_rules
system.config.instrument_div_multiplier=1.2

System 方法

基础的 System 类除了缓存相关的方法(见下文)外,只对外暴露了少量公共方法:

  • system.get_instrument_list():返回系统中的品种列表;如果配置对象里包含标的权重,则从配置中取;否则从数据对象中取。

以下方法同样会返回品种列表,详细说明见品种文档

1
2
3
4
5
6
get_list_of_bad_markets
get_list_of_markets_not_trading_but_with_data
get_list_of_duplicate_instruments_to_remove
get_list_of_ignored_instruments_to_remove
get_list_of_instruments_to_remove
get_list_of_markets_with_trading_restrictions'

system.log 提供对系统日志的访问,详见日志一节。

系统缓存与 pickling

加载数据并计算系统中的各个阶段可能非常耗时,因此代码支持缓存。当我们第一次调用某个阶段的方法请求数据(比如 system.portfolio.get_notional_position("DAX"))时,系统会先检查该值是否已在缓存中。如果尚未缓存,就会从头算一遍。这个过程通常需要先计算一些前置结果(除非这些结果也已经被预先计算过)。例如,要得到合成预测(combined forecast),我们需要先得到某个品种上所有不同交易规则变体的单独预测。

一旦我们算出了某个数据点(可能耗时很长),它就会被存储在 system 对象的缓存中(以及所有过程中顺便算出来的中间结果)。下一次请求相同数据时就能立即返回。

大部分时间里你不需要关心缓存。如果你在测试不同配置,或者更新 / 更换了数据,只要每次变更之后都重新创建一个新的 system 对象即可;新的 system 对象自带空缓存。

缓存标签示例

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
from copy import copy
from systems.provided.futures_chapter15.basesystem import futures_system

system=futures_system()
system.combForecast.get_combined_forecast("SOFR")

## 查看缓存中有什么?
system.cache.get_cache_refs_for_instrument("SOFR")

   [_get_forecast_scalar_fixed in forecastScaleCap for instrument SOFR [carry] , get_raw_forecast in rules for instrument SOFR [ewmac32_128]  ...


## 修改配置:
system.config.forecast_div_multiplier=0.1

## 由于结果已被缓存,这里得到的还是旧结果
system.combForecast.get_combined_forecast("SOFR")

## 但如果我们用新的配置重新创建系统……
system=futures_system(config=system.config)

## 检查缓存是否为空:
system.cache.get_cache_refs_for_instrument("SOFR")

## ……现在就会得到不同的结果
system.combForecast.get_combined_forecast("SOFR")

## 我们也可以关闭缓存
## 先清空缓存
system.cache.clear()

## ……此时缓存应该是空的
system.cache.get_cache_refs_for_instrument("SOFR")

## 现在关闭缓存
system.cache.set_caching_off()

## 再次获取数据:
system.combForecast.get_combined_forecast("SOFR")

## ……缓存仍然是空的
system.cache.get_cache_refs_for_instrument("SOFR")

我们也可以通过 copy() 来复制系统对象。由于缓存是系统对象的一部分,复制时缓存也会被一起复制。

关于缓存 API 的更多信息见缓存类文档

Pickling 与反 pickling 已保存的缓存数据

我们还可以将整个缓存对象保存到磁盘上,从而在以后重用:

1
2
3
4
5
6
from systems.provided.futures_chapter15.basesystem import futures_system

system = futures_system()
system.accounts.portfolio() ## 这里会运行整个回测,并把所有中间结果存入缓存

system.cache.pickle("private.this_system_name.system.pck") ## 文件扩展名可以任意

(关于文件名的写法,见这里。)

下次你重启 Python 解释器时,可以从磁盘恢复这些缓存:

1
2
3
4
5
6
from systems.provided.futures_chapter15.basesystem import futures_system

system = futures_system()
system.cache.unpickle("private.this_system_name.system.pck")

system.accounts.portfolio() ## 这一步需要重新计算的内容会少很多

这在你试验不同配置、但不希望每次都完整重跑回测时尤其有用。

注意,只有较为简单的对象才会被存入缓存;例如完整的账户 P&L 对象太复杂,不会被 pickling。

system 对象还提供了一个方法 pickle_withcaching,它会将 system 对象自身以及缓存一起保存下来。下一节会说明这个方法的用途。

高级缓存:回测时的缓存策略

在上一节中,我们看到 system 对象如何在同一次进程中帮助避免重复计算。缓存还可以配合 pickle 一起使用,以减少在不同 Python 会话之间重复计算。

下面是一种利用缓存进行系统开发的可能流程:

  1. 决定你要使用的交易规则列表、参数和阶段;以及它们的配置和数据。
  2. 运行一次完整回测,包括账户统计等所有内容。
  3. 修改系统配置中的一部分内容(例如某个参数)。
  4. 清空所有未受保护的缓存项。
  5. 对系统重新进行完整回测。
  6. 重复步骤 3–5,直到你对系统满意。如果修改的是会生成受保护缓存项的东西,要注意它们是否真的需要重新估计。如果需要,你也必须清空这些内容。
  7. 将所有品种补全到最终数据集。
  8. 再运行一次完整回测。此时会从头重新估计系统最终版本所需的所有东西,包括扩展后的标的权重。

下面是一个在系统开发中使用缓存的简单示例:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
from systems.provided.futures_chapter15.basesystem import futures_system
system=futures_system()

# 第 2 步
system.accounts.portfolio().curve() ## 实际上相当于运行了一次完整回测

# 第 3 步
new_idm=1.1 ## 新的 IDM
system.config.instrument_div_multiplier=new_idm

# 第 4 步
system.cache.delete_all_items() ## 受保护的项目不会被删除
system.accounts.portfolio().curve() ## 重新运行回测

# 假设我们很满意——进入第 6 步
system.cache.delete_all_items(delete_protected=True)

## 或者,用修改后的配置重建系统:
new_config=system.config
system=futures_system(config=new_config)

## 第 7 步
system.accounts.portfolio().curve() ## 重新运行最终回测

实盘系统中的高级缓存行为

虽然项目目前还没有包含完整的实盘交易系统,但 system 对象的缓存行为可以让它更适配实盘场景。如果我们的交易节奏足够慢,例如每天一次,我们可以在夜间做以下事情:

  1. 拉取所有品种的新价格;
  2. 将这些价格保存到数据对象所期望的位置;
  3. 从头创建一个新的 system 对象;
  4. 运行系统,对所有品种求出最优持仓。
    • 第 4 步可能非常复杂且耗时,但市场已收盘,所以没关系。
    • 接下来,在白天的交易时段我们会循环执行:
  5. 等待某个新价格到来(例如通过消息总线);
  6. 为避免使用过期价格,调用 system.delete_items_for_instrument(instrument_code) 删除该品种对应的所有缓存;
  7. 重新计算该品种的最优持仓;
  8. 将结果传递给交易算法。

因为我们只删除了与单个品种相关的所有缓存项,对该品种的持仓及其所有中间阶段会在使用新价格时重算。但我们无需重复跨品种的耗时计算,例如相关性估计、风险 overlay、横截面数据或权重估计——这些可以留到下一次夜间批处理。

更高级:在新代码或修改过的代码中使用缓存

如果你要为某个阶段编写新的方法(或新增完整的阶段),需要遵循一些规则,以保持缓存行为的一致性。

黄金法则:某一个具体的数值只应该在一个地方被缓存一次。

因此,数据对象的方法不应该做任何缓存;它们应该像“管道”一样,仅在需要时将数据传递给系统阶段。这可以避免在修改缓存时既要操作数据对象缓存、又要操作系统缓存的麻烦。

同样,大多数阶段都会包含一些“输入方法”(input methods),它们本身不做计算,而是从早期阶段获取“输出”再提供给本阶段使用。这些方法的存在是为了简化阶段内部的“接线”(wiring),并减少不同阶段方法之间的耦合。它们同样不应该做缓存,否则会导致同一份数据被缓存多次(详见阶段接线)。

你应该尽可能早地对数据做缓存,这样依赖这些数据的后续阶段就都能直接使用缓存结果。尽量避免“环形依赖”:即某个阶段使用来自更晚阶段的数据,这样可能导致无限递归。

缓存“存活”在父 system 对象中,即 system.cache 属性中,而不是具体阶段本身。项目中提供了一些标准函数,它们会先检查某个条目是否已被缓存,如果没有,就调用相应函数来计算(如下文所述)。为了让这件事更简单,当阶段对象加入某个系统时会获得一个 self.parent 属性,指向“父级” system。

还要仔细考虑某个方法产生的数据是否应当被标记为“受保护”,从而避免被缓存清理操作随意删除。一般来说,只要是跨品种、或者变化很慢的内容,就应该被保护。当前被标记为受保护的缓存项包括:

  • 估计的预测缩放因子(Forecast scalars)
  • 估计的预测权重(Forecast weights)
  • 估计的预测分散化乘数(Forecast diversification multiplier)
  • 估计的预测相关性矩阵
  • 估计的标的分散化乘数(Instrument diversification multiplier)
  • 估计的标的权重(Instrument weights)
  • 估计的标的相关性矩阵

同时还要考虑:你是否会缓存某些 pickle 难以处理的复杂对象(如类实例)。如果是,需要显式把这些结果标记为“不可 pickling”。

从本项目 0.14.0 版本开始,缓存是通过附加在阶段方法上的 Python 装饰器来实现的。代码中使用了 4 个装饰器:

  • @input:不做缓存,用于输入方法,详见阶段接线
  • @dont_cache:不做缓存,用于“太简单、不值得缓存”的计算;
  • @diagnostic():在阶段内部进行缓存,主要用于诊断型方法;
  • @output():在产生输出时进行缓存。

注意后两个装饰器总是以带括号的形式使用,而前两个不带括号。给方法加上 @input@dont_cache 装饰器不会带来任何行为差异,它们只是标记(便于阅读和维护)。

同样地,@diagnostic@output 的行为完全一样;仅仅是为了让阶段接线更一目了然,并突出哪些函数是输出函数。

后两个装饰器带有两个可选关键字参数,默认值均为 False@diagnostic(protected=False, not_pickable=False)
如果你想防止某个方法的结果被“随手删除”,可以设置 protected=True;如果某个方法返回的是 pickle 难以处理的复杂嵌套对象,可以设置 not_pickable=True

下面是 forecast_combine.py 中一些展示缓存装饰器用法的代码片段:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29

 @dont_cache
 def _use_estimated_weights(self):
     return str2Bool(self.parent.config.use_forecast_weight_estimates)

 @dont_cache
 def _use_estimated_div_mult(self):
     # very simple
     return str2Bool(self.parent.config.use_forecast_div_mult_estimates)

 @input
 def get_forecast_cap(self):
     """
     Get the forecast cap from the previous module

     :returns: float

     KEY INPUT
     """

     return self.parent.forecastScaleCap.get_forecast_cap()

 @diagnostic()
 def get_trading_rule_list_estimated_weights(self, instrument_code):
     # snip

 @diagnostic(protected=True, not_pickable=True)
 def get_forecast_correlation_matrices_from_code_list(self, codes_to_use):
     # snip

创建新的“预制系统”

如果你希望重复某个回测,或者已经确定了一套想要模拟或实盘交易的系统,那么把它做成一个新的预制系统会非常划算。

一个新的预制系统通常包含以下要素:

  1. 一组新的阶段,或现有阶段的不同组合;
  2. 一套数据(可以是新的,也可以是已有的);
  3. 一个配置文件;
  4. 一个 Python 函数,用于加载上述元素并返回一个 system 对象。

为了保持条理清晰,比较好的做法是把配置文件以及你编写的 Python 函数保存到类似 pysystemtrade/private/this_system_name/ 这样的目录下(你需要先创建这些目录)。如果你计划向 GitHub 提交代码,请注意不要把 private 目录提交进去(可以参考这篇文章)。如果这个系统用到了你自己的数据,也可以放在同一目录下。

接下来就需要编写 Python 函数。下面是第 15 章系统中的一个片段:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
## 为了获取数据,我们大概率需要这些

from sysdata.sim.csv_futures_sim_data import csvFuturesSimData
from sysdata.config.configdata import Config

## 导入所需的阶段
from systems.forecasting import Rules
from systems.basesystem import System
from systems.forecast_combine import ForecastCombine
from systems.forecast_scale_cap import ForecastScaleCap
from systems.rawdata import RawData
from systems.positionsizing import PositionSizing
from systems.portfolio import Portfolios
from systems.accounts.accounts_stage import Account


def futures_system(data=None, config=None, trading_rules=None):
    if data is None:
        data = csvFuturesSimData()

    if config is None:
        config = Config("systems.provided.futures_chapter15.futuresconfig.yaml")

    ## 保留动态加载交易规则的能力通常是件好事;
    ## 如果你不需要,也可以去掉 trading_rules 参数,直接写 rules=Rules()。
    rules = Rules(trading_rules)

    ## 构建 system
    system = System([Account(), Portfolios(), PositionSizing(), RawData(), ForecastCombine(),
                     ForecastScaleCap(), rules], data, config)

    return system

修改或创建新的 System 类

一般没有必要修改 System() 类或创建新的 System 子类。

Stages

在一个系统中,“阶段”(stage)负责完成从数据到最终最优持仓(以及账户净值曲线)这一长链条中的一部分计算。因此,无论是回测还是实盘,真正的计算过程实际上都是在各个 stage 对象中发生的。

我们在创建系统时,通过把一个 stage 对象列表作为第一个参数传给 System 来定义系统包含哪些阶段:

1
2
3
4
5
from systems.forecasting import Rules
from systems.basesystem import System
data=None ## 这样不会产生任何有用的结果

my_system=System([Rules()], data)

(在使用预制系统时,这一步通常被封装起来,不会直接看到。)

我们可以通过打印 system 来查看它拥有哪些阶段:

1
2
3
from systems.provided.futures_chapter15.basesystem import futures_system
system=futures_system()
system

输出类似:

1
System with stages: accounts, portfolio, positionSize, rawdata, combForecast, forecastScaleCap, rules

各个阶段都是主 system 的属性:

1
2
3
from systems.provided.futures_chapter15.basesystem import futures_system
system=futures_system()
system.rawdata

输出类似:

1
SystemStage 'rawdata'

因此我们可以直接访问每个阶段的数据方法:

1
system.rawdata.get_raw_price("SOFR").tail(5)

例如:

1
2
3
4
5
6
              price
2015-04-16  97.9350
2015-04-17  97.9400
2015-04-20  97.9250
2015-04-21  97.9050
2015-04-22  97.8325

system.rawdata.log 提供对原始数据阶段日志的访问,其他阶段类似。详见日志

阶段“接线”(Stage ‘wiring’)

理解系统内部各个阶段是如何“接线”的很有必要。如果你要修改或扩展代码,或者使用高级系统缓存,就更需要把这一点弄清楚。

在预制期货系统中,当我们调用 system.combForecast.get_combined_forecast("DAX") 时到底发生了什么?这个调用会进一步调用本阶段中的其他方法,而这些方法又会调用前面阶段的方法……一直追溯到底层数据。我们可以用一棵“调用树”来表示这个过程:

  • system.combForecast.get_combined_forecast("DAX")
    • system.combForecast.get_forecast_diversification_multiplier("DAX")
    • system.combForecast.get_forecast_weights("DAX")
    • system.combForecast.get_capped_forecast("DAX", "ewmac2_8"))
      • system.forecastScaleCap.get_capped_forecast("DAX", "ewmac2_8"))
        • system.forecastScaleCap.get_forecast_cap("DAX", "ewmac2_8")
        • system.forecastScaleCap.get_scaled_forecast("DAX", "ewmac2_8")
          • system.forecastScaleCap.get_forecast_scalar("DAX", "ewmac2_8")
          • system.forecastScaleCap.get_raw_forecast("DAX", "ewmac2_8")
            • system.rules.get_raw_forecast("DAX", "ewmac2_8")
              • system.data.get_raw_price("DAX")
              • system.rawdata.get_daily_returns_volatility("DAX")
                • (计算波动率的后续步骤略)

从本质上看,一个系统就是一棵树,上面只展示了其中的一小部分。当我们请求树上的某片“叶子”(最终输出)时,数据沿着树枝向上流动,并在路径中的各节点被缓存。

“阶段接线”描述的,就是各个阶段之间如何互相调用。一般来说,一个阶段包含三类方法:

  1. Input 方法:从其他阶段获取数据,不做任何进一步计算;
  2. 内部诊断(diagnostic)方法:在本阶段内部进行中间计算(有些可能是私有方法,但通常会保持公开,以便用于诊断);
  3. Output 方法:供其他阶段作为输入调用的输出方法。

以上面的调用链为例,我们可以给前几层调用加上角色标签:

  • 输出(combForecast)
    system.combForecast.get_combined_forecast("DAX")
    • 内部(combForecast)
      system.combForecast.get_forecast_diversification_multiplier("DAX")
    • 内部(combForecast)
      system.combForecast.get_forecast_weights("DAX")
    • 输入(combForecast)
      system.combForecast.get_capped_forecast("DAX", "ewmac2_8"))
      • 输出(forecastScaleCap)
        system.forecastScaleCap.get_capped_forecast("DAX", "ewmac2_8"))

这种设计(也可以理解为每个阶段的“API”)的目的是让代码更易于修改——只要保持某个阶段的输出方法签名不变,就可以在内部随意调整实现,甚至用新的阶段替换原阶段,而不必修改其他阶段中的代码。

编写新的阶段

如果你打算编写一个新的阶段(完全新写或替换已有阶段),需要注意以下几点:

  1. 新阶段应该继承自 SystemStage
  2. 如果是修改已有阶段,应当从原始阶段类继承。例如,你准备实现一种新的预测权重计算方式,就应该继承自 ForecastCombine,并重写其 get_forecast_weights 方法,同时保持其他方法不变。
  3. 完全新写的阶段需要一个唯一的名称,通过对象方法 _name() 指定。之后可以通过 system.stage_name 访问它。
  4. 修改已有阶段时,应保持与父类相同的名称,否则整个接线关系会乱套。
  5. 思考是否要把本阶段某些输出对应的缓存标记为“受保护”,以避免被随意删除(详见系统缓存)。
  6. 如果你要缓存某些难以被 pickle 的复杂对象(例如 accountCurve 对象),需要在装饰器调用中设置 not_pickable=True
  7. 通过“非缓存”的 input 方法从其他阶段获取数据。尽量避免直接访问其他阶段的内部方法,尽可能只使用其输出方法。
  8. 通过“带缓存”的 input 方法从系统数据对象获取数据(这是第一次缓存这些数据)。同样只访问数据对象的公共方法。
  9. 内部诊断方法和输出方法建议使用带缓存的方式(详见系统缓存)。
  10. 如果要在阶段内存储属性,建议用 _ 前缀,并提供单独的方法进行访问和修改。否则 methods() 方法会把属性也一并列出来。
  11. 如果内部方法可能对诊断有帮助,建议保持为公开方法;否则用 _ 前缀使之成为私有方法。
  12. Input 和 Output 方法的 docstring 中要清楚地标明它们是输入还是输出,以便查看“接线”结构时更清晰。
  13. 阶段顶部的 docstring 应说明:本阶段有哪些 input 方法(以及它们从哪里获取输入)、有哪些输出方法。
  14. docstring 还应该说明该阶段的职责以及阶段名称。
  15. 体量很大的阶段应该拆分成多个类(甚至多个文件),通过多重继承“拼接”在一起。可以参考accounts 阶段作为示例。

新写的阶段代码应放在 systems 包中(例如期货原始数据),或者放在你的私有目录 中。

具体阶段

系统中标准的阶段列表如下。下面给出默认类以及在 system 中访问该阶段所使用的属性名:

  1. 原始数据: RawDatasystem.rawdata
  2. 预测(Forecasting): Rulessystem.rules(对应我书中第 7 章)
  3. 预测缩放与截断: ForecastScaleCapsystem.forecastScaleCap(第 7 章)
  4. 预测合成: ForecastCombinesystem.combForecast(第 8 章)
  5. 计算子系统头寸: PositionSizingsystem.positionSize(第 9 与第 10 章)
  6. 在多品种间构建投资组合: Portfoliossystem.portfolio(第 11 章)
  7. 计算绩效: Accountsystem.accounts

下面将分别介绍每个阶段。

阶段:原始数据(Stage: Raw data)

原始数据阶段用于对数据做预处理,以支持计算交易规则、缩放头寸以及其他需要的内容。典型地,适合放在 RawData 阶段的内容包括:

  1. 会被多次使用的东西,例如价格波动率;
  2. 为了诊断和提高系统可见性而需要暴露的中间量,例如计算期货 carry 规则时的中间步骤。

使用标准 RawData

基础 RawData 类提供了获取品种价格、日收益、波动率以及“标准化收益”(收益除以波动率)的相关方法。

由于我们在交易期货,原始数据类还额外提供了一些专用于计算期货 carry 规则的方法,并把这些中间计算结果暴露出来。

(在 1.06 版本之前,还有一个单独的 FuturesRawData 类。)

波动率计算

在我的交易系统中,有两种类型的波动率:

  1. 价格差波动率,例如 sigma(Pt - Pt-1)
  2. 百分比收益波动率,例如 sigma((Pt - Pt-1) / Pt-1)

第一种波动率用于在交易规则中把预测标准化为与夏普比率(Sharpe ratio)成比例的量;第二种波动率用于缩放头寸。在这两种情况下,我们都使用“拼接”后的价格来计算价格差。对于期货,这意味着在展期时把合约拼接起来,并按 Panama 方法做平移调整。类似地,如果系统处理的是股票现货,也会用类似方式处理除权日。如果不这样做,而是直接用“天然价格”(正在交易的合约的原始价格)来计算收益,那么在展期日就会出现异常剧烈的收益跳变。

实际上系统默认使用的是拼接价格;因为对于交易规则来说,通常更需要平滑、不带怪异跳变的价格序列。RawData 中大多数带有“price”字样的方法指的都是拼接价格。

但是在计算百分比收益时,我们绝对不希望把拼接价格作为分母。对于正 carry 的资产,拼接价格会随着时间上升;这会导致历史上早期的价格偏小甚至为负,从而使得百分比收益被放大或符号反转。

为此,数据类中提供了一个特殊方法 daily_denominator_price,用来告诉代码在上述公式中的 P* 应该使用哪种价格。由于我们在交易期货,这里使用的是当前合约的原始价格。

另一个需要注意的点是:价格差波动率的计算通过 config.volatility_calculation 来配置。

默认使用的是一个鲁棒的 EWMA 波动率计算器,具有以下可配置属性:

  • 35 日时间跨度;
  • 至少需要 10 个样本才能产生有效值;
  • 会把任何小于 0.0000000001 的值设为该下限;
  • 还会应用一个进一步的波动率下限(vol floor),具体为:
    • 用 500 日窗口(至少 100 个样本)计算波动率的 5% 分位数;
    • 将所有波动率低于该水平的值抬高到该阈值。

YAML 示例:

1
2
3
4
5
6
7
8
9
volatility_calculation:
  func: "sysquant.estimators.vol.robust_vol_calc"
  days: 35
  min_periods: 10
  vol_abs_min: 0.0000000001
  vol_floor: True
  floor_min_quant: 0.05
  floor_min_periods: 100
  floor_days: 500

如果你考虑使用自己的波动率函数,请参考“当你修改某些函数时如何处理默认值”

新建或修改原始数据类

对于新的资产类型,或者希望对交易规则内部计算过程有更多可见性时,创建新的 RawData 类是很有意义的。

例如:

  1. 在股票价值体系中,根据原始会计比率计算“质量因子”(quality factor);
  2. 在 EWMAC 交易规则中,预先计算所需的移动平均线并放入 RawData,以便进行诊断查看。

对于新的资产类别,你尤其需要认真思考是否需要重写 daily_denominator_price(见上文关于波动率计算的讨论)。

阶段:规则(Stage: Rules)

在一个完全系统化的交易系统中,交易规则是核心所在。本阶段的说明和其他阶段略有不同,将以“如何创建交易规则”的小教程形式展开。

基础类 Rules()这里,一般不需要修改该类本身。一个交易规则由下列部分组成:

  • 一个函数;
  • 一些数据(以位置参数形式传入);
  • 一些可选的控制参数(以关键字参数形式传入)。

因此,规则函数的结构应该大致如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
def trading_rule_function(data1):
   ## 对 data1 做一些处理

def trading_rule_function(data1, arg1=default_value):
   ## 对 data1 做一些处理
   ## 行为由 arg1 控制

def trading_rule_function(data1, data2):
   ## 对 data1 和 data2 做一些处理

def trading_rule_function(data1, data2, arg1=default_value, arg2=default_value):
   ## 对 data1 做一些处理
   ## 行为由 arg1 和 arg2 控制

……等等。

在最简单的情况下,我们只需要知道函数本身即可,因为其他参数都是可选的;如果未指定任何数据,则默认使用该品种的价格。只在配置中写函数而不显式指定数据和参数的规则被称为“裸规则”(bare rule)。这种规则只接受一个数据参数(价格),并且不需要新的参数取值。

在本项目中有一个专门的 TradingRule。一个 TradingRule 实例包含 3 个要素:函数、数据列表以及其他参数的字典。

函数既可以是实际的 Python 函数,也可以是对函数的相对字符串引用,例如 systems.provided.futures_chapter15.rules.ewmac(当配置从文件中读取时这种方式很有用)。数据必须始终以字符串形式引用 system 对象的属性或方法,例如 data.daily_pricesrawdata.get_daily_prices。可以传入单个数据项,也可以是列表;其他参数则总是以字典形式给出。

我们可以通过多种方式创建交易规则。不同的人在定义规则时习惯不同,因此刻意提供了较大的灵活性。

只包含函数的“裸规则”可以这样定义:

1
2
3
4
5

from systems.trading_rules import TradingRule

TradingRule(ewmac)  ## 直接传函数对象
TradingRule("systems.provided.futures_chapter15.rules.ewmac")  ## 通过字符串引用函数

也可以同时添加数据和其他参数。数据字段始终是 strstr 列表,其他参数始终是 dict

1
TradingRule(ewmac, data='rawdata.get_daily_prices', other_args=dict(Lfast=2, Lslow=8))

支持多条数据,也可以省略数据或 other_args

1
TradingRule(some_rule, data=['rawdata.get_daily_prices','data.get_raw_price'])

有时以“整体(en bloc)”方式指定规则会更方便,可以使用一个三元组来指定。注意这里我们用字符串指定函数,并列出多个数据项:

1
TradingRule(("systems.provided.futures_chapter15.rules.ewmac", ['rawdata.get_daily_prices','data.get_raw_price'], dict(Lfast=3, Lslow=12)))

你也可以用字典描述规则;在使用字典时,关键字可以省略(但 function 不能省略):

1
TradingRule(dict(function="systems.provided.futures_chapter15.rules.ewmac", data=['rawdata.get_daily_prices','data.get_raw_price']))

需要注意的是,如果你使用“整体指定”的方式,同时在 TradingRule(...) 的调用中又单独传入了 dataother_args,会得到一个警告。

当配置从 YAML 文件读取时,会使用字典方式;YAML 中包含的交易规则是一个嵌套字典。

YAML 示例:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
trading_rules:
  ewmac2_8:
     function: systems.futures.rules.ewmac
     data:
         - "data.daily_prices"
         - "rawdata.daily_returns_volatility"
     other_args:
         Lfast: 2
         Lslow: 8
     forecast_scalar: 10.6

注意,forecast_scalar 严格来说并不算交易规则定义的一部分;但如果把它写在这里,就会覆盖单独的 config.forecast_scalar 参数(详见下一个阶段)。

数据与数据参数(Data and data arguments)

传给交易规则的 data 列表中的每一项,都是对 system 对象某个方法的字符串引用,该方法(通常)只接受一个参数——品种代码。理论上这些方法可以位于 system 对象中的任意位置,但按照惯例,应只放在 system.rawdatasystem.data 上(如果位于调用 rules 阶段的其他阶段里,会导致无限递归并使系统崩溃),极少数情况下可以引用 system.get_instrument_list()。使用 rawdata 中的方法有两个好处:这类方法会被缓存,且可以反复重用。强烈建议你在交易规则中优先使用 rawdata 中的方法。

如果你想给数据方法传参数怎么办?例如,你可能希望在 rawdata 中预先计算不同长度的移动平均线,以便在不同规则中复用;或者希望基于给定时间窗口为所有市场计算偏度,再形成某种横截面的相对价值规则。

我们可以通过在 other_args 中使用特殊的“带下划线参数名”来实现这一点。如果 other_args 字典中的某个 key 没有前导下划线,它会作为关键字参数传给交易规则函数本身;如果只有一个前导下划线(例如 _argname),它会作为关键字参数传给 data 列表中的第一个方法;如果有两个前导下划线(例如 __argname),则会传给第二个方法,以此类推。

下面来看一个移动平均线的示例:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
from systems.provided.futures_chapter15.basesystem import *
from systems.trading_rules import TradingRule

data = csvFuturesSimData()
config = Config(
   "systems.provided.futures_chapter15.futuresconfig.yaml")


# 首先,给 rawdata 增加一个新方法
# 除了常规的 instrument_code 外,它还接受一个关键字参数 span,我们将通过交易规则来控制它
class newRawData(RawData):
   def moving_average(self, instrument_code, span=8):
      price = self.get_daily_prices(instrument_code)
      return price.ewm(span=span).mean()


# 然后定义一个新的交易规则。multiplier 只是一个演示用的变量,说明你可以同时混合“普通参数”和数据参数
def new_ewma(fast_ewma, slow_ewma, vol, multiplier=1):
   raw_ewmac = fast_ewma - slow_ewma

   raw_ewmac = raw_ewmac * multiplier

   return raw_ewmac / vol.ffill()


# 下面定义第一条规则。注意 data 中用了两种移动平均线;
# 第一条的 span=2,第二条的 span=8
trading_rule1 = TradingRule(dict(function=new_ewma, data=['rawdata.moving_average', 'rawdata.moving_average',
                                                          'rawdata.daily_returns_volatility'],
                                 other_args=dict(_span=2, __span=8, multiplier=1)))

# 第二条规则重用了其中一条 ewma,但对 multiplier 和第一条 ewma 的 span 使用默认值
# (不算最佳实践,只是为了演示可能性)
trading_rule2 = TradingRule(dict(function=new_ewma, data=['rawdata.moving_average', 'rawdata.moving_average',
                                                          'rawdata.daily_returns_volatility'],
                                 other_args=dict(__span=32))

rules = Rules(dict(ewmac2_8=trading_rule1, ewmac8_32=trading_rule2))

system = System([
   Account(), Portfolios(), PositionSizing(), newRawData(),
   ForecastCombine(), ForecastScaleCap(), rules
], data, config)

# 现在可以像平常一样工作
system.rules.get_raw_forecast("DAX", "ewmac8_32")
system.rules.get_raw_forecast("DAX", "ewmac2_8")

注意:通过 data 传入的那些方法,除了 instrument_code 之外,只能带关键字参数;如果再加上额外的位置参数就会出问题。同时在 other_args 中,你并不需要为每个数据元素或每个交易规则参数都提供关键字参数——它们都是可选的。

Rules 类与交易规则列表的指定方式

我们可以用多种方式把单条或多条交易规则传给 Rules() 类。

从配置对象中构建规则列表

通常,我们会通过配置对象中的规则列表来构建 Rules。下面看一个简化版的“第 15 章期货系统”示例:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
## 为了获取数据,我们大概率需要这些

from sysdata.sim.csv_futures_sim_data import csvFuturesSimData
from sysdata.config.configdata import Config
from systems.basesystem import System

## 导入所需阶段
from systems.forecasting import Rules
from systems.rawdata import RawData

data=csvFuturesSimData()
config=Config("systems.provided.futures_chapter15.futuresconfig.yaml")

rules=Rules()

## 构建系统
system=System([rules, RawData()], data, config)

rules

输出可能类似:

1
2
<snip>
Exception: A Rules stage needs to be part of a System to identify trading rules, unless rules are passed when object created
1
2
3
## 一旦 Rules 成为 system 的一部分,我们就能看到规则了
forecast=system.rules.get_raw_forecast('DAX','ewmac2_8')
rules

输出类似:

1
Rules object with rules ewmac32_128, ewmac64_256, ewmac16_64, ewmac8_32, ewmac4_16, ewmac2_8, carry
1
2
##
rules.trading_rules()

输出类似:

1
2
3
4
5
6
7
{'carry': TradingRule; function: <function carry at 0xb2e0f26c>, data: rawdata.daily_annualised_roll, rawdata.daily_returns_volatility and other_args: smooth_days,
 'ewmac16_64': TradingRule; function: <function ewmac at 0xb2e0f224>, data: rawdata.daily_prices, rawdata.daily_returns_volatility and other_args: Lfast, Lslow,
 'ewmac2_8': TradingRule; function: <function ewmac at 0xb2e0f224>, data: rawdata.daily_prices, rawdata.daily_returns_volatility and other_args: Lfast, Lslow,
 'ewmac32_128': TradingRule; function: <function ewmac at 0xb2e0f224>, data: rawdata.daily_prices, rawdata.daily_returns_volatility and other_args: Lfast, Lslow,
 'ewmac4_16': TradingRule; function: <function ewmac at 0xb2e0f224>, data: rawdata.daily_prices, rawdata.daily_returns_volatility and other_args: Lfast, Lslow,
 'ewmac64_256': TradingRule; function: <function ewmac at 0xb2e0f224>, data: rawdata.daily_prices, rawdata.daily_returns_volatility and other_args: Lfast, Lslow,
 'ewmac8_32': TradingRule; function: <function ewmac at 0xb2e0f224>, data: rawdata.daily_prices, rawdata.daily_returns_volatility and other_args: Lfast, Lslow}

运行上述代码时,内部到底发生了什么?(稍微有点复杂,但值得理解):

  1. 使用无参数初始化 Rules 类;
  2. 初始状态下,Rules 对象是“空的”——还没有一份有效的、经过处理的交易规则列表;
  3. 创建 system 对象后,所有阶段都能“看到” system,尤其可以访问配置;
  4. 调用 get_raw_forecast 时,它会查找名为 "ewmac2_8" 的交易规则,并通过调用 get_trading_rules 方法获得;
  5. get_trading_rules 方法会先检查自己是否已经有一份“经过处理的”规则字典;
  6. 如果是第一次调用,则不会有处理后的列表,于是就去找“原始规则定义”;
  7. 它首先检查在构造 Rules() 实例时是否传入了任何规则;
  8. 由于此处未传入规则,它会改为处理 system.config.trading_rules 中的嵌套字典(以规则变体名为 key);
  9. 处理完成后,Rules 实例就拥有了一份“已处理”的规则字典,以规则变体名为 key,每个元素是一个有效的 TradingRule 对象。

交互式传入一组交易规则

在开发过程中,我们往往还没有准备好完整的配置。为了解决这个问题,我们可以在创建 Rules() 实例时传入单条规则或一组规则。如果传入的是字典,会使用字典的 key 作为规则名;如果传入的是单条规则或规则列表,则会使用 "rule0""rule1" 等自动命名。

另外,我们不一定要传入 TradingRule 对象;任何能被解析成交易规则的结构都可以。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
## 导入需要的阶段
from systems.forecasting import Rules

## 传入单条规则。以下几种写法都可以,详见“Trading rules”一节
trading_rule=TradingRule(ewmac)
trading_rule=(ewmac, 'rawdata.get_daily_prices', dict(Lfast=2, Lslow=8))
trading_rule=dict(function=ewmac, data='rawdata.get_daily_prices', other_args=dict(Lfast=2, Lslow=8))

rules=Rules(trading_rule)
## 规则会被自动赋予一个名字

## 传入规则列表。每条规则都可以用任意一种方式定义
trading_rule1=(ewmac, 'rawdata.get_daily_prices', dict(Lfast=2, Lslow=8))
trading_rule2=dict(function=ewmac, other_args=dict(Lfast=4, Lslow=16))

rules=Rules([trading_rule1, tradingrule2])
## 各规则会被自动赋予名字

## 传入规则字典。每条规则的定义方式同样可以不同
trading_rule1=(ewmac, 'rawdata.get_daily_prices', dict(Lfast=2, Lslow=8))
trading_rule2=dict(function=ewmac, other_args=dict(Lfast=4, Lslow=16))

rules=Rules(dict(ewmac2_8=trading_rule1, ewmac4_16=tradingrule2))

为单条交易规则创建多个变体

一个非常常见的开发模式是:先定义一条带可调参数的交易规则,然后基于不同参数值生成多个变体。项目中提供了两个函数来简化这一过程:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10

from systems.trading_rules import TradingRule, create_variations_oneparameter, create_variations

## 先为 ewmac 创建 3 个变体
## 默认 ewmac 的 Lslow=128
## 这里固定 Lslow 只调整 Lfast
rule = TradingRule("systems.provided.rules.ewmac.ewmac_forecast_with_defaults")
variations = create_variations_oneparameter(rule, [4, 10, 100], "ewmac_Lfast")

variations.keys()

输出类似:

1
dict_keys(['ewmac_Lfast_4', 'ewmac_Lfast_10', 'ewmac_Lfast_100'])
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
## 现在同时调整 Lslow 和 Lfast
rule=TradingRule("systems.provided.rules.ewmac.ewmac_forecast_with_defaults")

## 每个具体规则由一个 dict 指定;我们也可以用 lambda 自动生成这个列表
variations=create_variations(rule, [dict(Lfast=2, Lslow=8), dict(Lfast=4, Lslow=16)], key_argname="Lfast")
variations.keys()
   dict_keys(['ewmac_Lfast_4', 'ewmac_Lfast_2'])

variations['Lfast_2'].other_args
   {'Lfast': 4, 'Lslow': 16}

接下来,我们只需创建一个 Rules() 实例,并把 variations 作为参数传入即可。

使用新创建的 Rules() 实例

一旦有了新的 Rules 对象,我们就可以用它来创建一个新的系统:

1
2
## 构建系统
system=System([rules, RawData()], data, config)

通常比较好的做法是,把新的固定预测缩放因子(见预测缩放与截断阶段)和预测权重写入配置(见预测合成阶段)。如果你是自动估计这些参数,那就无需担心这一点。如果你只是临时试验一些想法,也可以先接受默认的预测缩放因子 1.0,并删除预测权重,让系统退回到等权:

1
del(config.forecast_weights)

将交易规则传入预制系统函数

如果你已经有一个预制系统,并且想尝试一组不在配置里的新交易规则,可以在创建系统时把它们直接传入:

1
2
3
4
5
6
7
from systems.provided.futures_chapter15.basesystem import futures_system

## 像前文示例一样创建 my_rules,例如:
trading_rule1=(ewmac, 'rawdata.get_daily_prices', dict(Lfast=2, Lslow=8))
trading_rule2=dict(function=ewmac, other_args=dict(Lfast=4, Lslow=16))

system=futures_system(trading_rules=dict(ewmac2_8=trading_rule1, ewmac4_16=tradingrule2)) ## 可能还需要调整配置

在系统中“动态”修改交易规则(高级)

上面的工作流中,我们先创建一个 Rules 实例(可能是空的,也可能在构造时传入一组规则),再创建引用它的 system。但有时我们希望在系统对象已经存在的情况下修改规则列表。例如,你加载了一个预制系统;这个系统中的 Rules() 实例本身是空的,因此实际使用的是配置中的规则。这时你可能不想整体替换规则集,而只是想删除一条规则、添加一条新规则,或修改某条已有规则。

要做到这一点,我们需要直接访问存储“处理后规则集合”(processed trading rules)的私有属性 _trading_rules。这意味着此处不能像前面那样传入“任意可被解析成规则的东西”,而必须传入真正的 TradingRule 实例。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
from systems.provided.futures_chapter15.basesystem import futures_system
from systems.trading_rules import TradingRule

system = futures_system()

## 解析配置中的现有规则
## (如果你已经先跑过一次回测,这一步会在过程中自动完成)
system.rules.trading_rules()

#############
## 通过访问私有属性添加一条新规则
new_rule = TradingRule(
   "systems.provided.futures_chapter15.rules.ewmac")  ## 任何形式的 TradingRule 都可以
system.rules._trading_rules['new_rule'] = new_rule
#############


#############
## 修改 key 为 'ewmac2_8' 的既有规则
modified_rule = system.rules._trading_rules['ewmac2_8']
modified_rule.other_args['Lfast'] = 10

## 还可以做类似操作:
## modified_rule.function=new_function
## modified_rule.data='data.get_raw_price'
##

system.rules._trading_rules['ewmac2_8'] = modified_rule
#############


#############
## 删除一条规则(不推荐)
## 从固定预测权重列表或 rule_variations(用于估计预测权重)中移除该规则也能达到类似效果——而且你无论如何都要这么做
## 不在权重或变体列表中的规则不会参与计算,因此删除规则在速度或空间上没有实质收益
##
system.rules._trading_rules.pop("ewmac2_8")
#############

阶段:预测缩放与截断(Stage: Forecast scale and cap)

这是一个比较简单的阶段,主要做两件事:

  1. 通过把原始预测乘以预测缩放因子(forecast scalar),把预测缩放到合适的平均绝对值水平;
  2. 把预测值限制在某个最大值之内(预测截断,forecast cap)。

使用固定权重

标准配置使用固定的缩放因子和截断上限。这一配置包含在标准期货系统中。

预测缩放因子是按每条规则分别设置的。它既可以写在配置中的 trading_rules 部分,也可以写在单独的 forecast_scalars 部分;如果两处都给出了同一条规则的缩放因子,以前者(trading_rules 内的值)为准:

YAML 示例(方式一):

1
2
3
4
trading_rules:
  some_rule_name:
     function: systems.futures.rules.arbitrary_function
     forecast_scalar: 10.6

YAML 示例(方式二):

1
2
forecast_scalars:
   some_rule_name: 10.6

预测截断上限同样可配置,但必须对所有规则都相同:

YAML 示例:

1
forecast_cap: 20.0

如果完全不配置,预测缩放因子和截断上限的默认值分别为 1.0 和 20.0。

在线计算估计的预测缩放

参见这篇博客文章

你也可以根据已有数据来估计预测缩放因子。这在为一条全新的交易规则选取缩放因子、而你完全不知道应该用什么数值时尤其有用。要启用这种估计方式,需要设置 config.use_forecast_scale_estimates=True。这一行为包含在预制的估计版期货系统中。

所有相关配置参数都存放在 config.forecast_scalar_estimate 中。

你可以选择按单个品种分别估计缩放因子,也可以把多个品种的数据合并在一起进行估计。使用哪种方式由配置参数 pool_instruments 决定。

合并后的预测缩放估计(默认)

pool_instruments=True 时使用“合并估计”。默认使用的函数是 "sysquant.estimators.forecast_scalar.forecast_scalar",但也可以通过参数 func 来替换;如果你要改这个函数,请先阅读“当你修改某些函数时如何处理默认值”

我强烈建议使用合并估计,因为“多数据总是好事”。唯一不该使用的情况,是你自己设计了一个预测,其自然尺度在不同品种之间就应该互不相同(参见我书中第 7 章的讨论)。

该函数会计算每个时间点上绝对值的横截面中位数,然后用滚动移动平均(因此始终是样本外估计)来求出把这一中位数缩放到 10 所需的缩放因子。

我也建议直接使用默认参数:window 设为 250000(足够大,相当于用扩展窗口来估计),min_periods 设为 500(大约两年的日度数据;比这更少会让估计非常不稳定,尤其是对于交易频率较低的规则;再多则需要等很久才能得到第一个数值)。另一个重要参数是布尔型的 backfill,默认值为 True。开启后,第一次得到的缩放因子会被向前“回填”到更早的时间点,这样我们就不会因为没有缩放因子而丢掉早期数据;严格来说这有点“作弊”,但我们并不是为了优化绩效而调这个参数,所以至少我可以心安理得。

注意:合并后的估计值会作为一个“跨 system、非品种特定”的条目被缓存

按单个品种估计预测缩放

pool_instruments=False 时,会按单个品种分别估计缩放因子。其他参数的含义和用法与合并估计相同。

注意:这种估计会在缓存中按品种分别存储。

阶段:预测合成(Stage: Forecast combine)

在这一阶段,我们按标的权重(instrument weights)对各个预测做加权平均,然后再乘以预测分散化乘数(forecast diversification multiplier)。

使用固定权重和分散化乘数

默认配置使用固定的预测权重和固定的预测分散化乘数。这一配置包含在预制的标准期货系统中。

权重和分散化乘数都是可配置的。

预测权重可以是:(a)所有品种共用一组权重;或者(b)为每个品种分别指定一组权重。如果完全不配置,将使用等权重。

YAML 示例(a:共用一组权重):

1
2
3
forecast_weights:
     ewmac: 0.50
     carry: 0.50

YAML 示例(b:按品种分别设置):

1
2
3
4
5
6
7
forecast_weights:
     SP500:
      ewmac: 0.50
      carry: 0.50
     US10:
      ewmac: 0.10
      carry: 0.90

预测分散化乘数同样可以是:(a)所有品种共用一个数;或者(b)为每个品种分别设定(如果不同品种的权重也不同,这通常是更合理的做法)。

YAML 示例(a:共用一个分散化乘数):

1
forecast_div_multiplier: 1.0

YAML 示例(b:按品种分别设置):

1
2
3
forecast_div_multiplier:
     SP500: 1.4
     US10:  1.1

需要注意的是,标准固定类中的 get_combined_forecast 方法会在不同交易规则的预测起始日期不同的情况下自动调整预测权重;但它不会调整分散化乘数。这意味着,在较早的历史时期,实际有效的分散化乘数往往会偏高。

使用估计的权重和预测分散化乘数

这种“在线估计权重与分散化乘数”的行为包含在预制的估计版期货系统中。要启用,需要设置 config.use_forecast_weight_estimates=True 和/或 config.use_forecast_div_mult_estimates=True

估计预测权重

详见优化一节。

移除成本较高的交易规则

详见优化一节。

估计预测分散化乘数

详见估计分散化乘数

预测映射

在 0.18.2 版本中新增了一个可选功能:forecast mapping(预测映射)。这是一种非线性映射方法,细节见这篇博客文章。其思想是:在预测值绝对值尚未达到某个阈值之前,我们完全不持仓;由于这样会降低预测序列的标准差,为了维持整体风险水平,我们会在预测值足够大时加快仓位增加的速度,直到原始预测达到既有的预测截断上限(默认为 20)。下面这个非线性映射函数可以更直观地说明这一点:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
#This is syscore.algos.map_forecast_value
def map_forecast_value_scalar(x, threshold, capped_value, a_param, b_param):
    """
    Non linear mapping of x value; replaces forecast capping; with defaults will map 1 for 1

    We want to end up with a function like this, for raw forecast x and mapped forecast m,
        capped_value c and threshold_value t:

    if -t < x < +t: m=0
    if abs(x)>c: m=sign(x)*c*a
    if c < x < -t:   (x+t)*b
    if t < x < +c:   (x-t)*b

    :param x: value to map
    :param threshold: value below which map to zero
    :param capped_value: maximum value we want x to take (without non linear mapping)
    :param a_param: multiple at capped value
    :param b_param: slope
    :return: mapped x
    """
    x = float(x)
    if np.isnan(x):
        return x
    if abs(x)<threshold:
        return 0.0
    if x >= -capped_value and x <= -threshold:
        return b_param*(x+threshold)
    if x >= threshold and x <= capped_value:
        return b_param*(x-threshold)
    if abs(x)>capped_value:
        return sign(x)*capped_value*a_param

    raise Exception("This should cover all conditions!")

我们希望为参数 a、b 和阈值 t 选择一组数值,使得下列条件同时成立:

  • a_param 应该设得足够大,以便当原始预测等于截断上限 capped_value 时,我们通常会持有大约 4 份合约(见我那本《Systematic Trading》一书第 12 章);
  • 我们要求满足关系式:b = (c * a) / (c - t)
  • 给定这些参数,以及原始预测的分布(假设为高斯分布),映射之后预测分布的平均绝对值应与映射前保持一致。

这些参数默认不会被自动估计,因此你可以使用下面这个外部函数来帮助确定它们:

1
2
3
4
5
# Assuming futures_system already contains a system which has positions
from systems.diagoutput import systemDiag

sysdiag = systemDiag(futures_system)
sysdiag.forecast_mapping()

参数可以按市场(品种)逐个指定,例如:

YAML 示例:

1
2
3
4
5
forecast_mapping:
  AEX:
    a_param: 2.0
    b_param: 8.0
    threshold: 15.0

如果配置对象中缺少 forecast_mapping 这一键,或者某个品种没有出现在该字典中,那么该品种的预测就不会做任何映射(原始预测值保持不变)。另外,如果设置 a_param = b_param = 1threshold = 0,那么效果也与“不开启预测映射”完全等价。

阶段:头寸缩放(Stage: Position scaling)

在这一阶段,我们根据目标年化波动率(percentage volatility target;见我书的第 9 和第 10 章)来缩放头寸。在这里,我们认为目标波动率——也就等价于账户规模——是固定不变的,因此忽略了盈亏带来的复利效应。正因如此,我把这里的头寸称为“名义头寸(notional position)”。在后文中,我们会放松这一假设。

使用标准 PositionSizing

目标年化百分比波动率、名义交易资本(notional trading capital)以及交易资本的货币单位都是可配置的。

YAML 示例:

1
2
3
percentage_vol_target: 16.0
notional_trading_capital: 1000000
base_currency: "USD"

需要注意的是,本阶段的代码会优先尝试从原始数据阶段获取品种的百分比波动率。如果系统中没有原始数据阶段,则会退而求其次,直接从数据对象出发,使用默认的波动率计算方法从头计算这一数值。

阶段:构建投资组合(Stage: Creating portfolios)

标的权重(instrument weights)和标的分散化乘数(instrument diversification multiplier)用于把不同的品种组合成最终的投资组合(详见我书第 11 章)。

使用固定权重和标的分散化乘数(/systems/portfolio.py

默认配置使用固定的标的权重和固定的标的分散化乘数。

二者都是可配置的。如果完全不配置,将默认使用等权重,并把分散化乘数设为 1.0。

YAML 示例:

1
2
3
4
instrument_weights:
    DAX: 0.5
    US10: 0.5
instrument_div_multiplier: 1.2

需要注意的是,标准固定类中的 get_instrument_weights 方法会在不同品种的价格历史和预测起始日期不同的情况下自动调整“原始”标的权重;但它不会调整分散化乘数。这意味着,在较早的历史时期,实际有效的分散化乘数往往会偏高。

使用估计权重和标的分散化乘数(/systems/portfolio.py

你可以“在线”估计合适的标的分散化乘数,并同时估计标的权重。这一功能包含在预制的估计版期货系统中;要启用,需要设置 config.use_instrument_weight_estimates=True 和/或 config.use_instrument_div_mult_estimates=True

估计标的权重

详见优化一节。

使用估计的预测分散化乘数

详见估计分散化乘数

缓冲与头寸惯性

头寸惯性(position inertia),或称“缓冲”(buffering),是一种降低交易成本的手段。它的基本思想是在当前头寸附近设定一个“不交易缓冲区”(no trade buffer):如果最优头寸相对于当前头寸的变化很小,就保持不动,只在变化足够大时才进行交易。关于这一点,我的书第 11 章中有更详细的讨论。

这里我使用两种缓冲方式。第一种是头寸缓冲(position buffering),与我书中介绍的头寸惯性方法相同:我们比较当前头寸与最优头寸;如果差距超过 10%(即缓冲区宽度),就把头寸调整到最优水平,否则就不交易。

下面这段配置会实现与我书中一致的头寸惯性行为:

YAML 示例:

1
2
3
buffer_trade_to_edge: False
buffer_method: position
buffer_size: 0.10

第二种方式是预测缓冲(forecast buffering)。在这里,我们取平均绝对头寸的某个固定比例(即在预测值为 10 时得到的平均绝对头寸)作为缓冲区宽度。这在理论上更加合理,因为随着预测接近 0,缓冲区不会随之缩小。其次,当当前头寸落在缓冲区之外时,我们只把头寸调整到缓冲区边缘,而不是直接调到最优头寸,这进一步降低了交易成本。下面是我对预测缓冲的推荐配置:

YAML 示例:

1
2
3
buffer_trade_to_edge: True
buffer_method: forecast
buffer_size: 0.10

注意,缓冲既可以作用在未四舍五入的连续头寸上,也可以作用在已经四舍五入的“整数合约数”头寸上。对后者来说,我们会在计算缓冲区上下限之后对这两个边界做四舍五入。

下面这些 Python 方法可以让你看到缓冲的具体效果:

1
2
3
system.portfolio.get_notional_position("US10") ## 获取应用缓冲前的名义头寸
system.portfolio.get_buffers_for_position("US10") ## 获取缓冲区的上下边界
system.accounts.get_buffered_position("US10", roundpositions=True) ## 获取应用缓冲后的头寸

需要注意的是,在实盘交易系统中,缓冲逻辑是在 system 模块的下游执行的,那里的流程还能看到我们实际持有的头寸(参见策略下单生成流程)。

最后,如果把 buffer_method 设置为 none,则不会应用任何缓冲。

资本校正

如果你希望看到能反映资本变动的头寸,请参见资本校正一节。

阶段:记账(Stage: Accounting)

最后一个阶段就是非常关键的记账阶段,用来计算整个系统的 P&L(盈亏)。

使用标准 Account

标准的记账类提供了若干常用方法:

  • portfolio:计算整个系统层面的 P&L(返回 accountCurveGroup
  • pandl_for_instrument:某个具体品种对整体 P&L 的贡献(返回 accountCurve
  • pandl_for_subsystem:某个品种“单独作为一个子系统”时的表现(返回 accountCurve
  • pandl_across_subsystems:把所有子系统的 P&L 合并在一起(注意这与 portfolio 不同:这里不会使用标的权重;返回 accountCurveGroup
  • pandl_for_trading_rule:某条交易规则在所有品种上汇总后的表现(返回 accountCurveGroup
  • pandl_for_trading_rule_weighted:某条交易规则在所有品种上、按总资本占比加权后的表现(返回 accountCurveGroup
  • pandl_for_trading_rule_unweighted:某条交易规则在所有品种上、未加权的表现(返回 accountCurveGroup
  • pandl_for_all_trading_rules:所有交易规则在所有品种上的表现(返回“嵌套”的 accountCurveGroup
  • pandl_for_all_trading_rules_unweighted:所有交易规则在所有品种上的未加权表现(返回“嵌套”的 accountCurveGroup
  • pandl_for_instrument_rules:某个品种上所有交易规则的表现(返回 accountCurveGroup
  • pandl_for_instrument_rules_unweighted:某个品种上所有交易规则的未加权表现(返回 accountCurveGroup
  • pandl_for_instrument_forecast:某个具体“规则变体 + 品种”组合的表现(返回 accountCurve
  • pandl_for_instrument_forecast_weighted:某个具体“规则变体 + 品种”组合,按总资本占比加权后的表现(返回 accountCurve

(注意:缓冲 只在最终组合层面使用;对单个预测和子系统的头寸不会应用缓冲。因此这些头寸对应的交易成本可能会略微被高估。)

(警告:请务必阅读“加权与未加权的账户曲线组” 一节。)

上述多数方法共享几个常用参数(均为布尔值):

  • delayfill:是否假定我们在下一交易日的收盘价成交。默认 True(更保守);
  • roundpositions:是否把头寸四舍五入到最近的“合约块大小”。对组合和品种层面默认 True,对子系统默认 False。在 pandl_for_instrument_forecastpandl_for_trading_rule 中不会用到该参数(总是 False)。

所有 P&L 方法的返回对象要么是 accountCurve(用于单个品种、子系统或“品种+规则变体”组合),要么是 accountCurveGroup(用于组合层面或规则层面),或者是嵌套的 accountCurveGrouppandl_for_all_trading_rulespandl_for_all_trading_rules_unweighted)。这些类型都继承自 pandas 的 DataFrame,因此可以直接画图、求均值等,同时还提供了一些额外方法。可以通过 stats 方法查看支持的功能:

1
2
3
from systems.provided.futures_chapter15.basesystem import futures_system
system=futures_system()
system.accounts.portfolio().stats()

输出类似:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
[[('min', '-1.997e+05'),
  ('max', '4.083e+04'),
  ('median', '-1.631'),
  ('mean', '156.9'),
  ('std', '5226'),
  ('skew', '-7.054'),
  ('ann_mean', '4.016e+04'),
  ('ann_std', '8.361e+04'),
  ('sharpe', '0.4803'),
  ('sortino', '0.5193'),
  ('avg_drawdown', '-1.017e+05'),
  ('time_in_drawdown', '0.9621'),
  ('calmar', '0.1199'),
  ('avg_return_to_drawdown', '0.395'),
  ('avg_loss', '-3016'),
  ('avg_gain', '3371'),
  ('gaintolossratio', '1.118'),
  ('profitfactor', '1.103'),
  ('hitrate', '0.4968'),
  ('t_stat', '2.852'),
  ('p_value', '0.004349')],
 ('You can also plot / print:',
  ['rolling_ann_std', 'drawdown', 'curve', 'percent'])]

stats 方法给出了三类信息:

  1. 各类统计量;其中大部分都有对应的单独方法,比如要直接取 sortino,可以用 system.accounts.portfolio().sortino()
  2. 一些绘图型方法,比如 system.accounts.portfolio().drawdown()
  3. 一些属性,用来获取不同频率的收益序列,比如 system.accounts.portfolio().annual

accountCurve

accountCurve 和对应的“组”对象要比表面看起来复杂得多。

先从 accountCurve 本身说起,它是 system.accounts.pandl_for_subsystem("DAX") 等方法的返回结果:

1
acc_curve=system.accounts.pandl_for_subsystem("DAX")

乍一看,它就像是一个普通的 pandas DataFrame,每一行是一日的收益。然而实际上,其中“埋”着三条账户曲线:毛收益(不含成本)、成本以及净收益(含成本)。可以按如下方式访问:

1
2
3
4
acc_curve.gross
acc_curve.net
acc_curve.costs
acc_curve.to_ncg_frame() ## 返回一个包含上述三者为列的 DataFrame

默认情况下,这几条曲线都是以“日收益”的形式呈现;不过你也可以按频率(每日、每周、每月、每年)取用:

1
2
3
4
acc_curve.gross.daily ## 等价于 acc_curve.gross
acc_curve.net.daily ## 等价于 acc_curve 和 acc_curve.net
acc_curve.net.weekly ## 也可以写成 acc_curve.weekly
acc_curve.costs.monthly

拿到所需频率之后,就可以调用各种统计方法:

1
2
3
4
5
acc_curve.gross.daily.stats() ## 获取方法列表;等价于 acc_curve.gross.stats()
acc_curve.annual.sharpe() ## 基于年度收益的夏普比率(Sharpe ratio)
acc_curve.gross.weekly.std() ## 周收益的标准差
acc_curve.daily.ann_std() ## 日度(净收益)换算成年化的波动率
acc_curve.costs.annual.median() ## 年度成本的中位数

……以及其他有趣的方法:

1
2
3
4
5
6
import syscore.pandas.strategy_functions

acc_curve.rolling_ann_std()  ## 日度净收益的滚动年化波动率
acc_curve.gross.curve()  ## 毛收益的累计收益曲线(账户曲线)
syscore.pandas.strategy_functions.drawdown()  ## 月度净收益的回撤
acc_curve.costs.weekly.curve()  ## 周度成本的累计值

我个人更习惯用“百分比”口径来看这些统计量,这也很容易做到:只需在调用统计方法之前加上 .percent 即可:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
import syscore.pandas.strategy_functions

acc_curve.current_capital  ## 用于把绝对金额转成百分比的基准资本
acc_curve.percent
acc_curve.gross.daily.percent
acc_curve.net.daily.percent
acc_curve.costs.monthly.percent
acc_curve.gross.daily.percent.stats()
acc_curve.monthly.percent.sharpe()
acc_curve.gross.weekly.percent.std()
acc_curve.daily.percent.ann_std()
acc_curve.costs.annual.percent.median()
acc_curve.percent.rolling_ann_std()
acc_curve.gross.percent.curve()
syscore.pandas.strategy_functions.drawdown()
acc_curve.costs.weekly.percent.curve()

顺便提一句:.percent、频率选择(daily/weekly 等)以及 gross/net/costs 这几类“修饰符”可以以任意顺序串联使用;它们并不会改变底层数据,只是改变当前的“视图表示”。如果想把“百分比视图”恢复成绝对金额,可以调用 .value_terms()

accountCurveGroup 详细说明

accountCurveGroupsystem.accounts.portfoliosystem.accounts.pandl_across_subsystemspandl_for_instrument_rules_unweightedpandl_for_trading_rule 以及 pandl_for_trading_rule_unweighted 等方法的返回类型。例如:

1
acc_curve_group=system.accounts.portfolio()

同样地,它看起来像一个普通的 DataFrame,或者一个 accountCurve 对象。因此我们可以直接调用以下方法:

1
2
3
4
5
acc_curve_group.gross.daily.stats() ## 获取方法列表;等价于 acc_curve.gross.stats()
acc_curve_group.annual.sharpe() ## 基于年度收益的夏普比率(Sharpe ratio)
acc_curve_group.gross.weekly.std() ## 周度收益的标准差
acc_curve_group.daily.ann_std() ## 日度(净收益)换算成年化的波动率
acc_curve_group.costs.annual.median() ## 年度成本的中位数

这些操作实际上都在给整个组合求 P&L(即把所有资产的账户曲线汇总);默认仍然是“净收益、日频数据”。要查看内部包含哪些资产,可以用 acc_curve_group.asset_columns;要取出某个具体资产的曲线,则使用 acc_curve_group['资产名']

1
2
acc_curve_group.asset_columns
acc_curve_group['US10']

警告:请务必阅读“加权与未加权的账户曲线组” 一节。

第二行返回的就是“仅 US10”这一资产的账户曲线,因此可以像对单个 accountCurve 一样操作:

1
2
3
4
5
6
7
acc_curve_group['US10'].gross.daily.stats()
acc_curve_group['US10'].annual.sharpe()
acc_curve_group['US10'].gross.weekly.std()
acc_curve_group['US10'].daily.ann_std()
acc_curve_group['US10'].costs.annual.median()

acc_curve_group.gross['US10'].weekly.std() ## 另一种等价写法

有时我们更希望把所有资产的账户曲线都画出来,这时可以先把它们转成一个 DataFrame:

1
2
3
4
acc_curve_group.to_frame() ## 返回所有资产的净收益曲线
acc_curve_group.net.to_frame() ## 同上
acc_curve_group.gross.to_frame() ## 返回所有资产的毛收益曲线
acc_curve_group.costs.to_frame() ## 返回所有资产的成本曲线

另一件有用的事情,是针对所有资产,批量计算某个统计量并返回一个字典:

1
2
3
4
5
acc_curve_group.get_stats("sharpe", "net", "daily") ## 获取每个资产的年化夏普比率(Sharpe ratio,基于日度数据)
acc_curve_group.get_stats("sharpe", freq="daily") ## 等价
acc_curve_group.get_stats("sharpe", curve_type="net") ## 等价
acc_curve_group.net.get_stats("sharpe", freq="daily") ## 等价
acc_curve_group.net.get_stats("sharpe", percent=False) ## 默认返回百分比口径,这里改为绝对数值

再次提醒:请阅读“加权与未加权的账户曲线组”

对于这些“按资产展开的统计量字典”,我们还可以再做汇总:既可以简单地对所有资产做算术平均,也可以按数据长度加权(时间加权):

1
2
3
4
acc_curve_group.get_stats("sharpe").mean() ## 所有资产年化夏普比率的简单平均(净收益、日度数据)
acc_curve_group.get_stats("sharpe").std(timeweighted=True) ## 各资产夏普比率的时间加权标准差
acc_curve_group.get_stats("sharpe").tstat(timeweighted=False) ## 夏普比率均值的 t 统计量
acc_curve_group.get_stats("sharpe").pvalue(timeweighted=True) ## 时间加权夏普比率均值对应的 t 检验 p 值

嵌套的 accountCurveGroup

嵌套的 accountCurveGrouppandl_for_all_trading_rulespandl_for_all_trading_rules_unweighted 的返回类型。例如:

1
nested_acc_curve_group=system.accounts.pandl_for_all_trading_rules()

这个对象本身是一个 accountCurveGroup,其“元素”是每条交易规则的表现。因此类似下面的用法是可行的:

1
ewmac64_acc=system.accounts.pandl_for_all_trading_rules()['ewmac64_256']

ewmac64_acc 自身又是一个 accountCurveGroup!因此可以继续查看“该规则下各个品种的贡献”,并把结果转为 DataFrame:

1
ewmac64_acc.to_frame()
加权与未加权的账户曲线组

账户曲线大致分两类:加权(weighted)和未加权(unweighted)。

  • 加权曲线:每个品种(或交易规则)的收益都按“总在险资本占比”加权;
  • 未加权曲线:每个品种或交易规则是“单独看”的表现。

加权型:

  • portfolio:整个系统的 P&L(加权组,元素是 pandl_for_instrument;有效权重 = 标的权重 × IDM);
  • pandl_for_instrument:某个品种对整体 P&L 的贡献(加权的单品种曲线;有效权重 = 标的权重 × IDM);
  • pandl_for_instrument_rules:某个品种上所有交易规则的表现(加权组;元素是各规则的 pandl_for_instrument_forecast;有效权重 = 预测权重 × FDM);
  • pandl_for_instrument_forecast_weighted:某个“规则变体 + 品种”组合按总资本占比加权后的表现(加权的单一曲线;权重 = 预测权重 × FDM × 标的权重 × IDM);
  • pandl_for_trading_rule_weighted:某条规则在所有品种上的整体表现,以总资本占比计(加权组;元素是各品种的 pandl_for_instrument_forecast_weighted;有效权重 = 该品种对该规则的风险贡献);
  • pandl_for_all_trading_rules:所有交易规则在所有品种上的整体表现(加权组;元素是各规则的 pandl_for_trading_rule_weighted;有效权重 = 每条规则的风险贡献)。

部分加权(见下):

  • pandl_for_trading_rule:某条规则在所有品种上的表现(加权组;元素是各品种的 pandl_for_instrument_forecast_weighted;权重为“该品种对该规则的风险贡献”)。

未加权型:

  • pandl_across_subsystems:所有子系统的 P&L(未加权组;元素是 pandl_for_subsystem);
  • pandl_for_subsystem:单个品种“作为一个独立子系统”的表现(未加权的单一曲线);
  • pandl_for_instrument_forecast:某个“规则变体 + 品种”组合的表现(未加权的单一曲线);
  • pandl_for_instrument_rules_unweighted:某个品种上所有规则的表现(未加权组;元素是各规则的 pandl_for_instrument_forecast);
  • pandl_for_trading_rule_unweighted:某条规则在所有品种上的表现(未加权组;元素是各品种的 pandl_for_instrument_forecast);
  • pandl_for_all_trading_rules_unweighted:所有规则在所有品种上的表现(未加权组;元素是各规则的 pandl_for_trading_rule_unweighted)。

资本校正——变动的资本(Capital correction - varying capital)

资本校正(capital correction)指的是:根据已经发生的盈亏调整“在险资本”,从而随之调整持仓规模。pysystemtrade 中的大部分逻辑都假定资本是固定的,这样风险在时间上保持稳定,账户曲线也更易于解读。但在现实中,更常见的做法是使用复利资本(compounded capital):盈利会滚入本金,亏损则从本金中扣除。如果我们持续赚钱,那么资本、风险以及头寸规模都会随时间逐步上升。

更详细的说明见这篇博客文章。资本校正由下面这个配置参数控制,它通过“点号路径”指定一个函数(默认是模块 syscore.capital 中的 fixed_capital 函数):

YAML:

1
2
capital_multiplier:
   func: syscore.capital.fixed_capital

我还实现了 full_compoundinghalf_compounding 等函数,详情同样见该博客文章

要获取所选方法计算出的“变动资本乘数”,可以调用 system.accounts.capital_multiplier()。当某一时刻该乘数为 1.0 时,说明“变动资本”与“固定资本”相同。

下表列出了在固定资本与变动资本假设下,常用方法的对应关系:

固定资本变动资本
获取在险资本positionSize.get_daily_cash_vol_target()['notional_trading_capital']accounts.get_actual_capital()
获取某个系统组合的头寸portfolio.get_notional_positionportfolio.get_actual_position
获取某个头寸的缓冲区上下界portfolio.get_buffers_for_positionportfolio.get_actual_buffers_for_position
获取缓冲后头寸accounts.get_buffered_positionaccounts.get_buffered_position_with_multiplier
获取系统层面某品种的 P&Laccounts.pandl_for_instrumentaccounts.pandl_for_instrument_with_multiplier
获取整个系统的 P&Laccounts.portfolioaccounts.portfolio_with_multiplier

除上述方法外,pysystemtrade 中的其他逻辑一律基于“固定资本”假设。

Optimisation

关于“如何给预测和标的设置权重”的详细讨论,可以参考我两篇博客文章:一篇是不计成本的优化(correlations, weights, multipliers),另一篇是计入成本的优化(optimising weights with costs)。

我使用同一个优化器同时计算预测权重和标的权重;两者流程几乎完全相同。

The optimisation function, and data

在配置中的写法如下:

1
2
3
4
forecast_weight_estimate: ## 同样也适用于标的权重
   func: sysquant.optimisation.generic_optimiser.genericOptimiser ## 当前只提供这一个函数
   pool_instruments: True ## 对标的权重不起作用
   frequency: "W" ## 其他可选:D, M, Y

我建议使用周度("W")数据:这样回测会快很多,而且对样本外绩效几乎没有影响。

Removing expensive assets (forecast weights only)

仍然推荐先读这篇博客

1
2
3
forecast_weight_estimate:
   ceiling_cost_SR: 0.13 ## 允许资产进入优化的最大成本(按年化夏普比率(Sharpe ratio)计)
    

关于在估计预测成本时如何做“成本汇总(pooling)”,见“成本(costs)” 一节。

默认情况下,ceiling_cost_SR 被设置为 9999,也就是“在优化阶段不排除任何交易规则”。但你可以配合 post_ceiling_cost_SR 使用,在优化完成之后再把“成本过高”的规则删除。如果你在按品种合并毛收益(pooling gross returns),我建议这么做。

Pooling gross returns (forecast weights only)

“按品种合并毛收益”只适用于计算预测权重时。再次推荐阅读这篇博客

只有那些通过了 ceiling_cost_SR 成本门槛的品种,其对应的规则才会参与合并。如果你希望在合并时不考虑成本(即所有品种都参与合并),可以把 ceiling_cost_SR 设成一个非常大的数值,然后用 post_ceiling_cost_SR 在优化完成后再删除高成本规则(这也是默认行为)。

1
2
3
4
5
forecast_weight_estimate:
   pool_gross_returns: True ## 在估计时合并毛收益
forecast_cost_estimate:
   use_pooled_costs: False  ### 对于拥有同一组交易规则的品种,用 [SR 成本 * 换手率] 的加权平均作为成本
   use_pooled_turnover: True ### 对于拥有同一组交易规则的品种,用换手率的加权平均

关于在估计预测成本时如何进行合并,见“成本(costs)” 一节。

Working out net costs (both instrument and forecast weights)

同样建议先阅读这篇博客

1
2
3
forecast_weight_estimate:  ## 同样也适用于标的权重
   equalise_gross: False ## 是否先把毛收益调成一致,这样优化就只看成本
   cost_multiplier: 0.0 ## 成本放大倍数。0 表示忽略成本、只用毛收益;>1 表示把成本放大。若稍后使用 apply_cost_weight=True(见下文),这里应为 0

Time periods

拟合期(fitting period)有三种可选方式:

  • expanding:扩展窗口(推荐);
  • in_sample:只用样本内数据(永远不要这样做!);
  • rolling:固定长度的滚动窗口。

这部分内容在我书的第 3 章中也有详细讨论。

在配置中的写法如下:

1
2
   date_method: expanding ## 其他可选:in_sample, rolling
   rollyears: 20 ## 仅在 rolling 时使用,以“年份”指定窗口长度

Moment estimation

做优化之前,我们需要对相关系数、均值和标准差进行估计。

在配置中的写法如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
forecast_weight_estimate:  ## 同样也适用于标的权重
   correlation_estimate:
     func: sysquant.estimators.correlation_estimator.correlationEstimator
     using_exponent: False
     ew_lookback: 500
     min_periods: 20
     floor_at_zero: True

   mean_estimate:
     func: sysquant.estimators.mean_estimator.meanEstimator
     using_exponent: False
     ew_lookback: 500
     min_periods: 20

   vol_estimate:
     func: sysquant.estimators.stdev_estimator.stdevEstimator
     using_exponent: False
     ew_lookback: 500
     min_periods: 20

如果你使用的是 shrinkage(收缩)或 single period(单期)优化,我建议把 using_exponent 设为 True,也就是对相关系数、均值和波动率都采用指数加权。

Methods

当前的优化函数内置了 5 种方法可选。就我个人而言,我最推荐使用 handcrafting(“手工构造”),这也是默认值。

Equal weights

这一方法会给所有标的 / 预测“等权重”。

1
   method: equal_weights

小提示:可以把 date_method: in_sample,这样会快很多。

One period (not recommend)

这一方法是经典的 Markowitz 单期优化版本,可以选择是否让夏普比率(Sharpe ratio,使优化更稳定)和波动率都“相等化”。由于我们本来就希望各项的波动率差不多大,因此我建议启用波动率相等化。

1
2
3
4
   method: one_period
   equalise_SR: True
   ann_target_SR: 0.5  ## 在做夏普比率相等化时所瞄准的年化夏普比率
   equalise_vols: True

注意:一旦你把夏普比率设为相等化,那么之前在“是否合并收益”、“如何处理成本”等设置上的差异基本都会被覆盖掉。

Bootstrapping 目前已经不再实现;在一次代码重构之后,我没能找到一种优雅的实现方式。

Shrinkage (okay, but tricky to calibrate)

这是一个基础的“收缩”方法:把相关系数和夏普比率向一个“先验值”收缩。先验假设为“夏普比率相等、相关系数相等”,且都取自数据估计值的平均。shrinkage 设为 1.0 表示完全采用先验;设为 0.0 则表示完全采用经验估计。

1
2
3
4
5
   method: shrinkage
   shrinkage_SR: 0.90
   ann_target_SR: 0.5  ## 在做夏普比率收缩时所瞄准的年化夏普比率
   shrinkage_corr: 0.50
   equalise_vols: True

注意:如果你把夏普比率的收缩因子设为 1.0(即完全相等化),那么同样会覆盖掉先前在合并方式或成本计算上的差异。

关于“手工构造”的详细说明见这一系列博客

1
2
3
   method: handcraft
   equalise_SR: False # 可选
   equalise_vols: True ## 这一项 *必须* 为 True,代码才能正常工作

Post processing

如果在前面的步骤中还没有把成本考虑进去(例如设置了 cost_multiplier=0),那么在得到初步权重后,我们可以再根据成本对其做一次调整。详细讨论见这篇博客

如果权重被标记为 cleaned,那么在某个回溯拟合期内,如果我们需要某个权重但由于数据不足尚未计算出来,会给对应资产分配一部分权重。

1
2
   apply_cost_weight: False
   cleaning: True

在这一阶段,还会应用另一个成本上限(config.post_ceiling_cost_SR)。

Estimating correlations and diversification multipliers

详见这篇博客

你可以同时为标的(IDM,详见我书第 11 章)和预测(FDM,详见第 8 章)估计分散化乘数。

第一步是估计相关系数。标的与预测的处理过程基本相同,只是预测层面可以选择在多个品种之间“合并数据”。下面这段 YAML 示例展示了我推荐的设置方式:对周度数据使用指数加权移动平均来估计相关系数。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
forecast_correlation_estimate:
   pool_instruments: True ## 对 IDM 估计无效
   func:sysquant.estimators.pooled_correlation.pooled_correlation_estimator ## 负责估计相关系数的函数,同时支持合并与不合并的情况
   frequency: "W"   # 在做相关系数估计前,将数据下采样到的频率
   date_method: "expanding" # 回测时使用的窗口类型
   using_exponent: True  # 是否对相关系数采用指数加权
   ew_lookback: 250 ## 指数加权的回看期
   min_periods: 20  # 无论是否用指数加权,至少需要的样本数
   cleaning: True  # 用平均值填补缺失,以避免在早期丢失过多数据
   floor_at_zero: True
   forward_fill_data: True

instrument_correlation_estimate:
   func: sysquant.estimators.correlation_over_time.correlation_over_time_for_returns # 标的相关性不做合并
   frequency: "W"
   date_method: "expanding"
   using_exponent: True
   ew_lookback: 250
   min_periods: 20
   cleaning: True
   rollyears: 20
   floor_at_zero: True
   forward_fill_price_index: True # 这里对价格做前向填充,而不是对收益,否则会出问题

一旦拿到了相关系数和预测 / 标的权重,计算分散化乘数就是一个很简单的步骤:

1
2
3
4
instrument_div_mult_estimate:
   func: sysquant.estimators.diversification_multipliers.diversification_multiplier_from_list
   ewma_span: 125   ## 平滑窗口(按工作日计)
   div_mult: 2.5 ## 最大允许的分散化乘数

我在这里添加了平滑函数,否则分散化乘数的跳变会导致回测中产生大量交易。需要注意的是,FDM 是按品种逐个计算的;但如果你在权重和相关系数估计时做了“按品种合并(pooling)”,那么这些品种的 FDM 也会相同。另一个实用的建议是:把负相关系数抬高到 0,这样可以避免分散化乘数被推得过高。

按层级指定权重(Specifying weights as hierarchy)

你可以在配置中“按层级结构”指定标的权重和预测权重。这样做的好处是:即便因为成本等原因删除了某些规则(或品种),策略在“高层级”的特征仍然能够得到保持。更多背景讨论可以参考这里的 GitHub 讨论贴:link。下面给出一些配置片段示例。

分层预测权重示例(Hierarchical forecast weight example)

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
forecast_weights:
  auto_weight_from_grouping:
    parameters:
      use_approx_DM: False
      apply_forecast_post_ceiling_cost_SR_before_weighting: True
    groups:
      convergent:
        weight: 0.4
        mean_reversion:
          weight: 0.3333
          mrinasset1000: 1.0
        skew:
          weight:  0.3333
          skewabs180: 0.25
          skewabs365: 0.25
          skewrv180: 0.25
          skewrv365: 0.25
        carry:
          weight: 0.3333
          purecarry:
            weight: 0.6
            carry10: 0.25
            carry125: 0.25
            carry30: 0.25
            carry60: 0.25
          relcarry:
            weight: 0.4
            relcarry: 1.0
      divergent:
        weight: 0.6
        speed1:
          weight: 0.1
          relmomentum:
            weight: 0.4
            relmomentum10: 1.0
          trend:
            weight: 0.6
            breakout10: 0.25
            assettrend2: 0.25
            normmom2: 0.25
            momentum4: 0.25
        speed2:
          weight: 0.18
          accel:
            weight: 0.3
            accel16: 1.0
          relmomentum:
            weight: 0.3
            relmomentum20: 1.0
          trend:
            weight: 0.4
            breakout20: 0.25
            assettrend4: 0.25
            normmom4: 0.25
            momentum8: 0.25
        speed3:
          weight: 0.18
          accel:
            weight: 0.3
            accel32: 1.0
          relmomentum:
            weight: 0.3
            relmomentum40: 1.0
          trend:
            weight: 0.4
            breakout40: 0.25
            assettrend8: 0.25
            normmom8: 0.25
            momentum16: 0.25
        speed4:
          weight: 0.18
          accel:
            weight: 0.3
            accel64: 1.0
          relmomentum:
            weight: 0.3
            relmomentum80: 1.0
          trend:
            weight: 0.4
            breakout80: 0.25
            assettrend16: 0.25
            normmom16: 0.25
            momentum32: 0.25
        speed5:
          weight: 0.18
          trend:
            weight: 1.0
            breakout160: 0.25
            assettrend32: 0.25
            normmom32: 0.25
            momentum64: 0.25
        speed6:
          weight: 0.18
          trend:
            weight: 1.0
            normmom64: 0.3333
            assettrend64: 0.3333
            breakout320: 0.3333

分层标的权重示例(Hierarchical instrument weight example)

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
instrument_weights:
  auto_weight_from_grouping:
    parameters:
      use_approx_DM: True
    groups:
      ags:
        weight: 0.143
        grain:
          weight: 0.25
          corn:
            weight: 0.2
            CORN: 1.0
          oats:
            weight: 0.2
            OATIES: 1.0
          rapeseed:
            weight: 0.2
            RAPESEED: 1.0
          rice:
            weight: 0.1667
            RICE: 1.0
          soy:
            weight: 0.1667
            SOYBEAN_mini: 0.333
            SOYMEAL: 0.333
            SOYOIL: 0.3333
          wheat:
            weight: 0.1667
            REDWHEAT: 0.5
            WHEAT: 0.5
        index:
          weight: 0.25
          BBCOMM: 0.5
          GICS: 0.5
        meats:
          weight: 0.25
          cows:
            weight: 0.5
            FEEDCOW: 0.5
            LIVECOW: 0.5
          pigs:
            weight: 0.5
            LEANHOG: 1.0
        softs:
          weight: 0.25
          cotton:
            weight: 0.25
            COTTON: 1.0
          milk:
            weight: 0.25
            BUTTER: 0.16667
            CHEESE: 0.16667
            MILK: 0.16667
            MILKDRY: 0.16667
            MILKWET: 0.16667
            WHEY:  0.16667
          rubber:
            weight: 0.25
            RUBBER: 1.0
          wood:
            weight: 0.25
            LUMBER-new: 1.0
      rates:
        weight: 0.143
        Americas:
          weight: 0.4
          govvie:
            weight: 0.6
            US2: 0.143
            US3: 0.143
            US5: 0.143
            US10: 0.143
            US10U: 0.143
            US20: 0.143
            US30: 0.143
          STIR:
            weight: 0.4
            BB3M: 0.333
            SOFR: 0.333
            FED: 0.333
          Swaps:
            weight: 0.4
            USIRS2ERIS: 0.25
            USIRS5: 0.25
            USIRS5ERIS: 0.25
            USIRS10: 0.25
        Asia:
          weight: 0.2
          korea:
            weight: 0.4
            KR3: 0.5
            KR10: 0.5
          japan:
            weight: 0.6
            JGB-SGX-mini: 1.0
        EMEA:
          weight: 0.4
          swiss:
            weight: 0.16667
            CH10: 1.0
          german:
            weight: 0.16667
            SHATZ: 0.25
            BOBL: 0.25
            BUND: 0.25
            BUXL: 0.25
          spain:
            weight: 0.16667
            BONO: 1.0
          eu:
            weight: 0.16667
            EURIBOR: 1.0
          france:
            weight: 0.16667
            OAT: 1.0
          italy:
            weight: 0.16667
            BTP3: 0.5
            BTP: 0.5
      equity:
        weight: 0.143
        Americas:
          weight: 0.2
          index:
            weight: 0.6
            largecap:
              weight: 0.6
              DOW: 0.25
              SP500_micro: 0.25
              NASDAQ_micro: 0.25
              R1000: 0.25
            midcap:
              weight: 0.4
              RUSSELL: 0.5
              SP400: 0.5
          sector:
            weight: 0.4
            US-DISCRETE: 0.1
            US-ENERGY: 0.1
            US-FINANCE: 0.1
            US-HEALTH: 0.1
            US-INDUSTRY: 0.1
            US-MATERIAL: 0.1
            HOUSE-US: 0.0333
            US-PROPERTY: 0.0333
            US-REALESTATE: 0.0333
            US-STAPLES: 0.1
            US-TECH: 0.1
            US-UTILS: 0.1
        Asia:
          weight: 0.2
          panasia:
            weight: 0.3333
            MSCIASIA: 1.0
          japan:
            weight: 0.3333
            NIKKEI: 0.2
            NIKKEI400: 0.2
            TOPIX: 0.2
            MUMMY: 0.2
            JP-REALESTATE: 0.2
          singapore:
            weight: 0.333
            MSCISING: 0.5
            SGX: 0.5
        EM:
          weight: 0.2
          brazil:
            weight: 0.143
            BOVESPA: 1.0
          china:
            weight: 0.143
            FTSECHINAA: 0.5
            FTSECHINAH: 0.5
          indonesia:
            weight: 0.143
            IDX: 1.0
          malaysia:
            weight: 0.143
            KLCI: 1.0
          southafrica:
            weight: 0.143
            TOP40: 1.0
          taiwan:
            weight: 0.143
            TAIEX: 1.0
          thailand:
            weight: 0.143
            SET50: 1.0
        EMEA:
          weight: 0.2
          Scandinavia:
            weight: 0.3333
            norway:
              weight: 0.3333
              OBX: 1.0
            sweden:
              weight: 0.3333
              OMXS: 1.0
            denmark:
              weight: 0.3333
              OMXC20: 1.0
          eu:
            weight: 0.3333
            future:
              weight: 0.3333
              EUROSTX: 1.0
            index:
              weight: 0.6667
              EU-DJ-BANKS: 0.05556
              EU-CARS: 0.05556
              EU-CHEM: 0.05556
              EU-CONSTRUCTION: 0.05556
              EU-DJ-TELECOM: 0.05556
              EU-DJ-UTIL: 0.05556
              EU-DURABLES: 0.05556
              EU-FOOD: 0.05556
              EU-HEALTH: 0.05556
              EU-HOUSE: 0.05556
              EU-INDUSTRY: 0.05556
              EU-INSURE: 0.05556
              EU-MATERIAL: 0.05556
              EU-MEDIA: 0.05556
              EU-OIL: 0.05556
              EPRA-EUROPE: 0.05556
              EU-REALESTATE: 0.05556
              EU-RETAIL: 0.05556
              EU-TECH: 0.05556
              EU-TRAVEL: 0.05556
          finland:
            weight: 0.1667
            OMX: 1.0
          france:
            weight: 0.1667
            CAC: 1.0
          netherlands:
            weight: 0.1667
            AEX: 1.0
        World:
          weight: 0.2
          MSCIWORLD: 1.0
      fx:
        weight: 0.143
        cross:
          weight: 0.3333
          AUDJPY: 0.1111
          CHFJPY: 0.1111
          EURAUD: 0.1111
          EURCAD: 0.1111
          EURCHF: 0.1111
          GBPCHF: 0.1111
          GBPEUR: 0.1111
          GBPJPY: 0.1111
          YENEUR: 0.1111
        developed:
          weight: 0.3333
          AUD: 0.091
          CAD: 0.091
          CHF: 0.091
          EUR_micro: 0.091
          GBP: 0.091
          JPY: 0.091
          NOK: 0.091
          NZD: 0.091
          SEK: 0.091
          SGD: 0.091
          TWD: 0.091
        em:
          weight: 0.333
          BRE: 0.091
          CLP: 0.091
          CNH: 0.091
          CZK: 0.091
          INR: 0.091
          IRS: 0.091
          KRWUSD_mini: 0.091
          MXP: 0.091
          PLN: 0.091
          RUR: 0.091
          ZAR: 0.091
      metals:
        weight: 0.143
        crypto:
          weight: 0.333
          BITCOIN: 0.5
          ETHEREUM: 0.5
        industrial:
          weight: 0.333
          ALUMINIUM: 0.25
          COPPER-micro: 0.25
          IRON: 0.25
          STEEL: 0.25
        precious:
          weight: 0.333
          GOLD_micro: 0.25
          PALLAD: 0.25
          PLAT: 0.25
          SILVER: 0.25
      energies:
        weight: 0.143
        gas:
          weight: 0.333
          GAS-LAST: 0.333
          GAS-PEN: 0.333
          GAS_US_mini: 0.333
        oil:
          weight: 0.333
          BRENT-LAST: 0.5
          CRUDE_W: 0.5
        products:
          weight: 0.333
          ETHANOL: 0.333
          GASOILINE: 0.333
          HEATOIL: 0.333
      vol:
        weight: 0.143
        V2X: 0.35
        VIX: 0.35
        VNKI: 0.3

参考(Reference)

标准 system.datasystem.stage 方法表(Table of standard system.data and system.stage methods)

本节中的表格列出了 system 以及其各个“子阶段”(stages)中所有可用于“取数”的公共方法。你也可以直接调用 methods() 方法来查看某个阶段有哪些可用方法:

1
system.rawdata.methods() ## 对任何 stage 或 data 对象都适用

列说明(Explanation of columns)

为了简洁起见,下文表格中 “Call” 一列省略了 system 实例本身的名字(除非直接调用 system 对象本身)。例如,表格中写的是 data.get_raw_price,那么真正的调用形式类似:

1
2
3
from systems.provided.futures_chapter15.basesystem import futures_system
name_of_system=futures_system()
name_of_system.data.get_raw_price("DAX")

“标准方法”(Standard)在所有系统中都存在;“非标准”(通常在表中标成 Futures 或 Estimate)则是从标准类继承出来的特定阶段类才拥有的方法,例如专门用于期货的 RawData 方法,或者用于估计参数(而不是使用固定值)的 estimate 类。

常见参数包括:

  • instrument_code:字符串,表示品种名称;
  • rule_variation_name:字符串,表示交易规则变体的名称。

“Type” 一列可以包含 D、I、O 中的一个或多个:

  • D(Diagnostic):诊断用途的方法,用来查看中间计算结果;
  • I(Input):从其他阶段获取信息的关键输入方法。详见阶段接线。描述中会说明数据来源;
  • O(Output):供其他阶段调用的关键输出方法。详见阶段接线。表中不包含仅被特定交易规则使用的输出(例如 rawdata.daily_annualised_roll)。

私有方法不会出现在这些表中。

System 对象(System object)

CallStandard?ArgumentsTypeDescription
system.get_instrument_listStandardD,O可用品种列表;来源为 config.instrument_weightsconfig.instruments,或数据集本身

除此之外,System 还包含一些访问日志与缓存的辅助方法(未在表中列出)。

Data 对象(Data object)

CallStandard?ArgumentsTypeDescription
data.get_raw_priceStandardinstrument_codeD,O日内价格(如可用;必要时做 backadjust)
data.daily_pricesStandardinstrument_codeD,O交易规则分析所用的“默认价格”(必要时做 backadjust)
data.get_instrument_listStandardD,O数据集中可用的品种列表(不一定全部用于回测)
data.get_value_of_block_price_moveStandardinstrument_codeD,O当某个品种每“一个价格单位”(或一个 tick)变动时,对一个最小交易单位(block)的价值影响有多大
data.get_instrument_currencyStandardinstrument_codeD,O该品种以何种货币计价
data.get_fx_for_instrumentStandardinstrument_code, base_currencyD,O该品种计价货币与基准货币(base_currency)之间的汇率
data.get_instrument_raw_carry_dataFuturesinstrument_codeD,O返回包含四列的 DataFrame:PRICE、CARRY、PRICE_CONTRACT、CARRY_CONTRACT
data.get_raw_cost_dataStandardinstrument_codeD,O成本数据(滑点和不同类型的佣金)

原始数据阶段(Raw data stage)

CallStandard?ArgumentsTypeDescription
rawdata.get_daily_pricesStandardinstrument_codeIdata.daily_prices
rawdata.daily_denominator_priceStandardinstrument_codeO计算百分比波动率时使用的价格(对期货来说为当前合约价格)
rawdata.daily_returnsStandardinstrument_codeD,O以价格差表示的日收益
rawdata.get_daily_percentage_returnsStandardinstrument_codeD百分比形式的日收益
rawdata.daily_returns_volatilityStandardinstrument_codeD,O以价格差表示的日收益标准差
rawdata.get_daily_percentage_volatilityStandardinstrument_codeD,O百分比形式的日收益标准差(10.0 表示 10%)
rawdata.get_daily_vol_normalised_returnsStandardinstrument_codeD用波动率归一化后的日收益(1.0 表示 1 个 sigma)
rawdata.get_instrument_raw_carry_dataFuturesinstrument_codeIdata.get_instrument_raw_carry_data
rawdata.raw_futures_rollFuturesinstrument_codeD期货价格与 carry 之差(raw roll)
rawdata.roll_differentialsFuturesinstrument_codeD年化因子
rawdata.annualised_rollFuturesinstrument_codeD年化展期收益(roll)
rawdata.daily_annualised_rollFuturesinstrument_codeD日度年化展期收益;用于 carry 规则

交易规则阶段(Trading rules stage,书中第 7 章)

CallStandard?ArgumentsTypeDescription
rules.trading_rulesStandardD,O交易规则变体列表
rules.get_raw_forecastStandardinstrument_code, rule_variation_nameD,O未缩放、未截断的原始预测值

预测缩放与截断阶段(Forecast scaling and capping stage,书中第 7 章)

CallStandard?ArgumentsTypeDescription
forecastScaleCap.get_raw_forecastStandardinstrument_code, rule_variation_nameIrules.get_raw_forecast
forecastScaleCap.get_forecast_scalarStandard / Estimateinstrument_code, rule_variation_nameD返回该预测所使用的缩放因子
forecastScaleCap.get_forecast_capStandardD,O获取预测的上限值
forecastScaleCap.get_forecast_floorStandardD,O获取预测的下限值
forecastScaleCap.get_scaled_forecastStandardinstrument_code, rule_variation_nameD获取缩放(与截断)之后的预测值
forecastScaleCap.get_capped_forecastStandardinstrument_code, rule_variation_nameD,O同上:缩放与截断后的预测值

合成预测阶段(Combine forecasts stage,书中第 8 章)

CallStandard?ArgumentsTypeDescription
combForecast.get_trading_rule_listStandardinstrument_codeI从配置或前一阶段获取交易规则列表
combForecast.get_all_forecastsStandardinstrument_code, (rule_variation_list)D返回包含多个规则预测的 DataFrame
combForecast.get_forecast_capStandardIforecastScaleCap.get_forecast_cap
combForecast.calculation_of_raw_estimated_monthly_forecast_weightsEstimateinstrument_codeD预测权重估计对象
combForecast.get_forecast_weightsStandard / Estimateinstrument_codeD预测权重(对缺失预测做了调整)
combForecast.get_forecast_correlation_matricesEstimateinstrument_codeD预测之间的相关系数矩阵
combForecast.get_forecast_diversification_multiplierStandard / Estimateinstrument_codeD预测分散化乘数
combForecast.get_combined_forecastStandardinstrument_codeD,O某个品种的加权平均预测值

头寸规模阶段(Position sizing stage,书中第 9 和 10 章)

CallStandard?ArgumentsTypeDescription
positionSize.get_combined_forecastStandardinstrument_codeIcombForecast.get_combined_forecast
positionSize.get_price_volatilityStandardinstrument_codeIrawdata.get_daily_percentage_volatility(或 data.daily_prices
positionSize.get_underlying_priceStandardinstrument_codeIrawdata.daily_denominator_price(或 data.daily_prices);data.get_value_of_block_price_move
positionSize.get_fx_rateStandardinstrument_codeIdata.get_fx_for_instrument
positionSize.get_daily_cash_vol_targetStandardD返回字典:base_currencypercentage_vol_targetnotional_trading_capitalannual_cash_vol_targetdaily_cash_vol_target
positionSize.get_block_valueStandardinstrument_codeD价格变动 1% 时,一个最小交易单位对应的价值变动
positionSize.get_instrument_currency_volStandardinstrument_codeD以品种计价货币表示的日度波动率
positionSize.get_instrument_value_volStandardinstrument_codeD以账户基准货币表示的日度波动率
positionSize.get_average_position_at_subsystem_levelStandardinstrument_codeD目标波动率相对于品种自身货币下波动率的比值
positionSize.get_subsystem_positionStandardinstrument_codeD,O如果把全部交易资本投在某一品种上,对应的头寸规模

投资组合阶段(Portfolio stage,书中第 11 章)

CallStandard?ArgumentsTypeDescription
portfolio.get_subsystem_positionStandardinstrument_codeIpositionSize.get_subsystem_position
portfolio.pandl_across_subsystemsEstimateIaccounts.pandl_across_subsystems
portfolio.calculation_of_raw_instrument_weightsEstimateD标的权重估计对象
portfolio.get_unsmoothed_instrument_weights_fitted_to_position_lengthsStandard / EstimateD未平滑的原始标的权重
portfolio.get_instrument_weightsStandard / EstimateD考虑缺失品种后的标的权重
portfolio.get_instrument_diversification_multiplierStandard / EstimateD标的分散化乘数
portfolio.get_notional_positionStandardinstrument_codeD,O“名义”头寸(假设风险资本不随盈亏变化)
portfolio.get_buffers_for_positionStandardinstrument_codeD,O某个头寸的缓冲区上下界
portfolio.get_actual_positionStandardinstrument_codeD,O考虑资本乘数后的实际头寸
portfolio.get_actual_buffers_for_positionStandardinstrument_codeD,O考虑资本乘数后的缓冲区上下界

记账阶段(Accounting stage)

输入(Inputs):

CallStandard?ArgumentsTypeDescription
accounts.get_notional_positionStandardinstrument_codeIportfolio.get_notional_position
accounts.get_actual_positionStandardinstrument_codeIportfolio.get_actual_position
accounts.get_capped_forecastStandardinstrument_code, rule_variation_nameIforecastScaleCap.get_capped_forecast
accounts.get_instrument_listStandardIsystem.get_instrument_list
accounts.get_notional_capitalStandardIpositionSize.get_daily_cash_vol_target
accounts.get_fx_rateStandardinstrument_codeIpositionSize.get_fx_rate
accounts.get_value_of_block_price_moveStandardinstrument_codeIdata.get_value_of_block_price_move
accounts.get_daily_returns_volatilityStandardinstrument_codeIrawdata.daily_returns_volatilitydata.daily_prices
accounts.get_raw_cost_dataStandardinstrument_codeIdata.get_raw_cost_data
accounts.get_buffers_for_positionStandardinstrument_codeIportfolio.get_buffers_for_position
accounts.get_actual_buffers_for_positionStandardinstrument_codeIportfolio.get_actual_buffers_for_position
accounts.get_instrument_diversification_multiplierStandardIportfolio.get_instrument_diversification_multiplier
accounts.get_instrument_weightsStandardIportfolio.get_instrument_weights
accounts.list_of_rules_for_codeStandardinstrument_codeIcombForecast.get_trading_rule_list
accounts.has_same_rules_as_codeStandardinstrument_codeIcombForecast.has_same_rules_as_code

诊断(Diagnostics):

CallStandard?ArgumentsTypeDescription
accounts.list_of_trading_rulesStandardD跨所有品种的交易规则列表
accounts.get_instrument_scaling_factorStandardinstrument_codeDIDM × 标的权重
accounts.get_buffered_positionStandardinstrument_codeD组合层面的缓冲后头寸
accounts.get_buffered_position_with_multiplierStandardinstrument_codeD同上,但考虑资本乘数
accounts.subsystem_turnoverStandardinstrument_codeD子系统层面的年化换手率
accounts.instrument_turnoverStandardinstrument_codeD组合层面某品种头寸的年化换手率
accounts.forecast_turnoverStandardinstrument_code, rule_variation_nameD某条“品种+规则变体”组合的年化换手率
accounts.get_SR_cost_for_instrument_forecastStandardinstrument_code, rule_variation_nameD该组合的 SR 成本 × 换手率
accounts.capital_multiplierStandardD,O资本乘数(实际资本 / 固定名义资本)
accounts.get_actual_capitalStandardD实际资本(固定名义资本 × 资本乘数)

记账输出(Accounting outputs):

CallStandard?ArgumentsTypeDescription
accounts.pandl_for_instrumentStandardinstrument_codeD系统内某个品种的 P&L(固定资本)
accounts.pandl_for_instrument_with_multiplierStandardinstrument_codeD系统内某个品种在“变动资本”下的 P&L
accounts.pandl_for_instrument_forecastStandardinstrument_code, rule_variation_nameD某个“品种+规则变体”组合的 P&L
accounts.pandl_for_instrument_forecast_weightedStandardinstrument_code, rule_variation_nameD某个“品种+规则变体”组合按总资本占比加权后的 P&L
accounts.pandl_for_instrument_rulesStandardinstrument_codeD,O某个品种上所有交易规则的加权 P&L
accounts.pandl_for_instrument_rules_unweightedStandardinstrument_codeD,O某个品种上所有交易规则的未加权 P&L
accounts.pandl_for_trading_ruleStandardrule_variation_nameD某条规则在所有品种上的 P&L
accounts.pandl_for_trading_rule_weightedStandardrule_variation_nameD某条规则在所有品种上的、按总资本占比加权的 P&L
accounts.pandl_for_trading_rule_unweightedStandardrule_variation_nameD某条规则在所有品种上的未加权 P&L
accounts.pandl_for_subsystemStandardinstrument_codeD某个品种“独立持有”的 P&L(子系统层面)
accounts.pandl_across_subsystemsStandardinstrument_codeD,O所有子系统的总 P&L(按品种汇总)
accounts.pandl_for_all_trading_rulesStandardD整个系统中所有交易规则的 P&L(加权)
accounts.pandl_for_all_trading_rules_unweightedStandardD整个系统中所有交易规则的 P&L(未加权)
accounts.portfolioStandardD,O整个系统的总 P&L(固定资本)
accounts.portfolio_with_multiplierStandardD整个系统在“变动资本”下的总 P&L

配置选项(Configuration options)

下面列出了系统中所有可用的配置项。“YAML” 部分展示了它们在 YAML 文件中的写法;“Python” 部分则给出了一些示例,说明如何在内存中修改配置对象。你通常会先这样创建一个配置对象:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
## 方法一:从已有系统中取得配置
from systems.provided.futures_chapter15.basesystem import futures_system
system=futures_system()
new_config=system.config

## 方法二:从配置文件创建
from syscore.fileutils import get_pathname_for_package
from sysdata.config.configdata import Config

my_config=Config(get_pathname_for_package("private", "this_system_name", "config.yaml"))

## 方法三:创建一个空配置
from sysdata.config.configdata import Config
my_config=Config()

每个小节还会给出项目级默认值的示例;如果你想修改这些默认值,可以在这里进行。

在修改配置对象中某个“嵌套字段”时,你可以整体替换这一段:

1
2
new_config.instrument_weights=dict(SP500=0.5, US10=0.5))
new_config

也可以只改动其中的某个元素:

1
2
new_config.instrument_weights['SP500']=0.2
new_config

如果你采用第二种做法,需要自己保证整个配置的一致性。无论如何,比较好的做法是在配置被放入 system 之后再检查一遍(这时已经叠加了所有默认值),确认最终的配置确实是你想要的。

原始数据(Raw data)

计算波动率(Calculating volatility)

表示形式:dict[str, int | float];键名为参数名称。默认值见下。

该配置用于指定“计算波动率所使用的函数”,以及传给它的关键字参数。如果某个关键字缺失,则使用项目默认值。更多细节见“波动率计算”一节。

下面展示了如何在配置中修改这些参数(同时也是项目默认值的写法):

YAML:

1
2
3
4
5
6
7
8
9
volatility_calculation:
  func: "sysquant.estimators.vol.robust_vol_calc"
  days: 35
  min_periods: 10
  vol_abs_min: 0.0000000001
  vol_floor: True
  floor_min_quant: 0.05
  floor_min_periods: 100
  floor_days: 500

Python:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
config.volatility_calculation=dict(
    func="syscore.algos.robust.vol.calc",
    days=35,
    min_periods=10,
    vol_abs_min=0.0000000001,
    vol_floor=True,
    floor_min_quant=0.05,
    floor_min_periods=100,
    floor_days=500,
)

如果你打算使用自己的波动率计算函数,请参考“当你修改某些函数时如何处理默认值”

规则阶段(Rules stage)

交易规则(Trading rules)

表示形式:dict[str, dict],其中外层字典的键是“交易规则变体名”。默认值:无(必须显式给出)。

交易规则集的定义方式为:每条规则对应一个字典,其中至少包含:

  • 一个 function 字符串,用来标识使用哪个函数;
  • 一个可选的 data 字符串列表,表示要传入哪些数据;
  • 一个可选的 other_args 字典,包含要传入函数的关键字参数。

在 YAML 中只能使用这种方式来定义交易规则。

使用 Python 代码时,定义交易规则还有很多其他方法,详见“Rules 阶段”一节。

需要注意的是:forecast_scalar 严格来说并不是交易规则定义的一部分;但如果你把它写在这里,将会优先使用这里的值,而不是单独的 config.forecast_scalar 参数(见下一节)。

YAML 示例:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
trading_rules:
  ewmac2_8:
     function: systems.futures.rules.ewmac
     data:
         - "rawdata.daily_prices"
         - "rawdata.daily_returns_volatility"
     other_args:
         Lfast: 2
         Lslow: 8
     forecast_scalar: 10.6

Python 示例:

1
2
3
4
5
6
7
8
config.trading_rules = dict(
    ewmac2_8=dict(
        function="systems.futures.rules.ewmac",
        data=["rawdata.daily_prices", "rawdata.daily_returns_volatility"],
        other_args=dict(Lfast=2, Lslow=8),
        forecast_scalar=10.6,
    )
)

预测缩放与截断阶段(Forecast scaling and capping stage)

在“固定参数”(默认)和“估计参数”之间切换的方式如下:

YAML 示例:

1
use_forecast_scale_estimates: True

Python 示例:

1
config.use_forecast_scale_estimates = True

预测缩放因子(固定)(Forecast scalar, fixed)

表示形式:dict[str, float];键名为交易规则变体名。默认值:1.0。

当使用“固定缩放”时,为每条交易规则指定一个预测缩放因子。如果未显式配置,则使用默认值 1.0。

缩放因子也可以写在交易规则定义内部(这是系统优先查找的地方):

YAML 示例:

1
2
3
4
trading_rules:
  rule_name:
     function: systems.futures.rules.arbitrary_function
     forecast_scalar: 10.6

Python 示例:

1
2
3
config.trading_rules = dict(
    rule_name=dict(function="systems.futures.rules.arbitrary_function", forecast_scalar=10.6)
)

如果在交易规则定义中没找到缩放因子,也可以单独提供一张缩放因子表;如果两处都给出了同一条规则的缩放因子,则以“规则定义中的值”为准:

YAML 示例:

1
2
forecast_scalars:
   rule_name: 10.6

Python 示例:

1
config.forecast_scalars = dict(rule_name=10.6)

预测缩放因子(估计)(Forecast scalar, estimated)

表示形式:dict[str, float | int];键名为参数名称。默认值:见下文示例。

用于“滚动、样本外”估计预测缩放因子的方法。若配置中缺少某些元素,将自动从项目默认值中补齐。必填参数包括:

  • pool_instruments:是否在多个品种之间合并数据来估计缩放因子;
  • func:用于估计的函数指针(字符串形式)。

其余参数会原样传入估计函数。

更详细的背景与方法,见“预测缩放估计”

如果打算使用自定义的估计函数,请参考“当你修改某些函数时如何处理默认值”

YAML 示例(同时也是默认值):

1
2
3
4
5
6
7
8
# 下面的写法既说明了我们如何做估计,也给出了 *默认配置*。
use_forecast_scale_estimates: True
forecast_scalar_estimate:
   pool_instruments: True
   func: "sysquant.estimators.forecast_scalar.forecast_scalar"
   window: 250000
   min_periods: 500
   backfill: True

Python 示例:

1
2
3
4
5
6
7
8
## “合并估计”的示例
config.forecast_scalar_estimate = dict(
    pool_instruments=True,
    func="sysquant.estimators.forecast_scalar.forecast_scalar",
    window=250000,
    min_periods=500,
    backfill=True,
)

预测截断上限(固定——所有类别共用)(Forecast cap, fixed – all classes)

表示形式:float

应用在所有交易规则上的预测截断上限。如果未设置,将使用项目默认值 20.0。

YAML 示例:

1
forecast_cap: 20.0

Python 示例:

1
config.forecast_cap = 20.0

预测合成阶段(Forecast combination stage)

在“固定权重”(默认)和“估计权重”之间切换的方式如下:

YAML 示例:

1
use_forecast_weight_estimates: True

Python 示例:

1
config.use_forecast_weight_estimates = True

你还可以调整固定与估计权重共同使用的平滑参数:

YAML 示例:

1
forecast_weight_ewma_span: 6

若希望删除对某个品种而言“成本过高”的交易规则,可以设置:

YAML 示例:

1
post_ceiling_cost_SR: 0.13

预测权重(固定)(Forecast weights, fixed)

表示形式:

  • (a) dict[str, float]:键名为交易规则变体名;
  • (b) dict[str, dict[str, float]]:外层键名为品种名,内层键名为规则名。

默认值:系统中所有交易规则“等权重”。

这些权重用于在合成预测时,对不同规则变体的预测进行加权。可以:

  • (a) 对所有品种共用一组权重;
  • (b) 为每个品种指定单独的权重集合。

注意:默认的等权重不会出现在默认配置文件中,而是在运行时按需计算。

YAML 示例(a:对所有品种共用一组权重):

1
2
3
forecast_weights:
     ewmac: 0.50
     carry: 0.50

Python 示例(a):

1
config.forecast_weights = dict(ewmac=0.5, carry=0.5)

YAML 示例(b:按品种分别设置):

1
2
3
4
5
6
7
forecast_weights:
     SP500:
      ewmac: 0.50
      carry: 0.50
     US10:
      ewmac: 0.10
      carry: 0.90

Python 示例(b):

1
2
3
4
config.forecast_weights = dict(
    SP500=dict(ewmac=0.5, carry=0.5),
    US10=dict(ewmac=0.10, carry=0.90),
)

预测权重(估计)(Forecast weights, estimated)

要估计预测权重,首先需要指定“参与估计的交易规则变体列表”。

用于估计的交易规则列表(List of trading rules to get forecasts for)

表示形式:

  • (a) list[str]:每个元素是一个规则变体名;
  • (b) dict[str, list[str]]:外层键名为品种名,内层为该品种使用的规则列表。

默认值:系统中定义的所有交易规则。

这些列表用来指定需要为哪些规则估计预测权重。可以:

  • (a) 对所有品种共用同一组规则列表;
  • (b) 为每个品种分别指定规则列表。

如果不显式指定,将使用系统中所定义的全部规则。

YAML 示例(a):

1
2
3
rule_variations:
     - "ewmac"
     - "carry"

Python 示例(a):

1
config.rule_variations = ["ewmac", "carry"]

YAML 示例(b):

1
2
3
4
5
6
rule_variations:
     SP500:
      - "ewmac"
      - "carry"
     US10:
      - "ewmac"

Python 示例(b):

1
config.rule_variations = dict(SP500=["ewmac", "carry"], US10=["ewmac"])
估计预测权重的参数(Parameters for estimating forecast weights)

具体配置见“优化(Optimisation)” 一节。

预测分散化乘数(固定)(Forecast diversification multiplier, fixed)

表示形式:

  • (a) float
  • (b) dict[str, float]:键名为品种代码。

默认值:1.0。

同样可以:

  • (a) 对所有品种共用一个分散化乘数;
  • (b) 为每个品种指定不同的乘数(当标的权重按品种有差异时,这通常更合理)。

YAML 示例(a):

1
forecast_div_multiplier: 1.0

Python 示例(a):

1
config.forecast_div_multiplier = 1.0

YAML 示例(b):

1
2
3
forecast_div_multiplier:
     SP500: 1.4
     US10:  1.1

Python 示例(b):

1
config.forecast_div_multiplier = dict(SP500=1.4, US10=1.1)

预测分散化乘数(估计)(Forecast diversification multiplier, estimated)

相关配置见“估计相关性和分散化乘数” 一节。

预测映射配置(Forecast mapping config)

表示形式:dict[str, dict[str, float]];外层键名为品种名,内层键名为 a_paramb_paramthreshold
默认值:dict(a_param = 1.0, b_param = 1.0, threshold = 0.0),其效果等价于“不开启预测映射”。

YAML 示例(展示默认值):

1
2
3
4
5
6
forecast_mapping:
  AUD:
    a_param: 1.0
    b_param: 1.0
    threshold: 0.0
# 其他品种类似

Python 示例(展示如何修改某个品种的参数):

1
2
3
config.forecast_mapping = dict()
config.forecast_mapping["AUD"] = dict(a_param=1.0, b_param=1.0, threshold=0.0)
config.forecast_mapping["AUD"]["a_param"] = 2.0  # 例如修改 a_param

头寸规模阶段(Position sizing stage)

资本缩放参数(Capital scaling parameters)

表示形式:floatintstr。默认值见下文。

这里配置的是:

  • 目标年化百分比波动率(percentage_vol_target);
  • 名义交易资本(notional_trading_capital);
  • 交易资本的货币(base_currency)。

如果某一项未在配置中给出,则使用下面示例中的默认值。

YAML 示例:

1
2
3
percentage_vol_target: 16.0
notional_trading_capital: 1000000
base_currency: "USD"

Python 示例:

1
2
3
config.percentage_vol_target = 16.0
config.notional_trading_capital = 1000000
config.base_currency = "USD"

投资组合合成阶段(Portfolio combination stage)

在“固定标的权重”(默认)和“估计标的权重”之间切换的方式如下:

YAML 示例:

1
use_instrument_weight_estimates: True

Python 示例:

1
config.use_instrument_weight_estimates = True

要调整固定与估计标的权重共同使用的平滑参数,可以设置:

YAML 示例:

1
instrument_weight_ewma_span: 125

标的权重(固定)(Instrument weights, fixed)

表示形式:dict[str, float];键名为品种代码。默认值:等权(equal weights)。

这些权重用于将不同品种组合成最终投资组合。

虽然默认是等权,但默认权重不会写入系统默认配置文件,而是在运行时按需计算。

YAML 示例:

1
2
3
instrument_weights:
    DAX: 0.5
    US10: 0.5

Python 示例:

1
config.instrument_weights = dict(DAX=0.5, US10=0.5)

标的权重(估计)(Instrument weights, estimated)

相关配置见“优化(Optimisation)” 一节。

标的分散化乘数(固定)(Instrument diversification multiplier, fixed)

表示形式:float。默认值:1.0。

YAML 示例:

1
instrument_div_multiplier: 1.0

Python 示例:

1
config.instrument_div_multiplier = 1.0

标的分散化乘数(估计)(Instrument diversification multiplier, estimated)

相关配置见“估计相关性和分散化乘数” 一节。

缓冲(Buffering)

表示形式:见下。默认值:见前文“缓冲与头寸惯性”及系统默认配置。

用于指定使用哪种缓冲 / 头寸惯性方法

  • position:基于最优头寸的“头寸缓冲”;
  • forecast:基于“预测值为 +10 时的头寸”的“预测缓冲”;
  • none:不使用缓冲。

同时还需指定缓冲区宽度 buffer_size,它是相对于头寸规模或“平均预测头寸”的比例,例如 0.1 表示 10%。

YAML 示例:

1
2
buffer_method: position
buffer_size: 0.10

记账阶段配置(Accounting stage config)

缓冲配置(Buffering config)

在计算组合头寸时,当最优头寸超出缓冲区时,我们是“只交易到缓冲区边缘”,还是“一次性调整到最优头寸”?该配置控制这一行为。

表示形式:bool。默认值:True(即“交易到缓冲区边缘”)。

YAML 示例:

1
buffer_trade_to_edge: True

成本配置(Costs config)

在计算品种层面的 P&L 时(预测层面一律使用“夏普比率(SR)成本”,即 SR 成本),是否使用“标准化的夏普比率(SR)成本”,还是使用实际金额成本?

YAML 示例:

1
use_SR_costs: True

在计算预测层面的 P&L 时,是否在品种之间对 SR 成本进行合并(pooling)?

YAML 示例:

1
2
3
forecast_cost_estimate:
   use_pooled_costs: False  ### 对拥有同一组交易规则的品种,使用 SR 成本 * 换手率 的加权平均
   use_pooled_turnover: True ### 对拥有同一组交易规则的品种,使用换手率的加权平均

资本校正配置(Capital correction config)

指定应当使用哪种资本校正方法。

YAML 示例:

1
2
capital_multiplier:
   func: syscore.capital.fixed_capital

其他可用的函数包括 full_compoundinghalf_compounding,详见“资本校正——变动的资本” 一节。