译文说明
注:本文为非官方翻译,可能存在疏漏;请以原文为准。
这是对 pysystemtrade 目前功能的一次“快速巡游”。读完之后,你大概率会想继续阅读用户指南。
需要注意的是,你在自己机器上跑出来的结果会和这里展示的不同,因为你使用的是更加最新的数据。
概要: 本文先用一个简单的 EWMAC 交易规则演示数据与预测的基本用法,然后逐步引入配置对象(Config)、多阶段的 System 构建流程以及预制系统(pre‑baked system),最后展示一个完整的期货系统示例。
A simple trading rule(简单交易规则)
(本节示例代码见 /examples/introduction/asimpletradingrule.py)
作为系统化交易员,我们相信“未来至少会有一点像过去”。所以第一步,我们需要一段历史数据。原则上,历史数据可以来自很多地方,但这里先用项目自带的 CSV 示例数据:
1
2
3
| from sysdata.sim.csv_futures_sim_data import csvFuturesSimData
data=csvFuturesSimData()
data
|
1
| FuturesData object with 38 instruments
|
我们手头有哪些品种(instrument)?
1
| data.get_instrument_list()
|
1
| ['CORN', 'LEANHOG', 'LIVECOW', 'SOYBEAN', 'WHEAT', 'KR10', 'KR3', 'BOBL', 'BTP', 'BUND', 'OAT', 'SHATZ', 'US10', 'US2', 'US20', 'US5', 'V2X', 'VIX', 'KOSPI', 'AEX', 'CAC', 'SMI', 'NASDAQ', 'SP500', 'AUD', 'EUR', 'GBP', 'JPY', 'MXP', 'NZD', 'COPPER', 'GOLD', 'PALLAD', 'PLAT', 'CRUDE_W', 'GAS_US', 'SOFR', 'EUROSTX']
|
并不是所有品种都一眼就能看出是什么:
1
| data.get_instrument_object_with_meta_data("MUMMY")
|
1
| futuresInstrumentWithMetaData(instrument=MUMMY, meta_data=instrumentMetaData(Description='TSE Mothers Index', Pointsize=1000.0, Currency='JPY', AssetClass='Equity', Slippage=0.5, PerBlock=50.0, Percentage=0.0, PerTrade=0.0))
|
那我们到底能拿到哪些类型的数据呢?
1
| data.get_raw_price("SOFR").tail(5)
|
1
2
3
4
5
6
| 2016-05-05 98.6400
2016-05-06 98.6050
2016-05-09 98.6550
2016-05-10 98.6500
2016-05-11 98.6675
Name: price, dtype: float64
|
我会定期更新这些数据,也会逐步加入从不同数据源获取你自己数据的方法
技术说明:这里的价格是通过“Panama 方法”拼接相邻合约得到的期货回溯调整价(back‑adjusted price)
data 对象的行为有点像 dict(虽然并没有真正继承自它),所以下面两种写法都可以:
1
2
| data.keys() ## 等价于 data.get_instrument_list
data['SP500_micro'] ## 等价于 data.get_instrument_price
|
价格数据当然很有用,但还有别的数据吗?对于期货来说,答案是“有的”,我们可以获取实现 carry 交易规则所需的数据:
1
| data.get_instrument_raw_carry_data("SOFR").tail(6)
|
1
2
3
4
5
6
7
| PRICE CARRY CARRY_CONTRACT PRICE_CONTRACT
2016-05-04 98.5800 98.645 201903 201906
2016-05-05 98.6400 98.700 201903 201906
2016-05-06 98.6050 98.665 201903 201906
2016-05-09 98.6550 98.715 201903 201906
2016-05-10 98.6500 98.705 201903 201906
2016-05-11 98.6675 NaN 201903 201906
|
下面我们来写一个最简单的交易规则。
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
|
import pandas as pd
from sysquant.estimators.vol import robust_vol_calc
def calc_ewmac_forecast(price, Lfast, Lslow=None):
"""
Calculate the ewmac trading rule forecast, given a price and EWMA speeds Lfast, Lslow and vol_lookback
"""
## price: This is the stitched price series
## We can't use the price of the contract we're trading, or the volatility will be jumpy
## And we'll miss out on the rolldown. See https://qoppac.blogspot.com/2015/05/systems-building-futures-rolling.html
price = price.resample("1B").last()
if Lslow is None:
Lslow = 4 * Lfast
## We don't need to calculate the decay parameter, just use the span directly
fast_ewma = price.ewm(span=Lfast).mean()
slow_ewma = price.ewm(span=Lslow).mean()
raw_ewmac = fast_ewma - slow_ewma
vol = robust_vol_calc(price.diff())
return raw_ewmac / vol
|
运行一下,看看输出:
1
2
3
4
5
6
7
8
| instrument_code='SOFR'
price=data.daily_prices(instrument_code)
ewmac=calc_ewmac_forecast(price, 32, 128)
ewmac.tail(5)
from matplotlib.pyplot import show
ewmac.plot()
show()
|
1
2
3
4
5
6
| 2016-05-05 4.602536
2016-05-06 4.649790
2016-05-09 4.726840
2016-05-10 4.896521
2016-05-11 5.083896
Freq: B, dtype: float64
|
我们到底赚到钱了吗?
1
2
3
| from systems.accounts.account_forecast import pandl_for_instrument_forecast
account = pandl_for_instrument_forecast(forecast = ewmac, price=price)
account.percent.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', '-7.911'),
('max', '5.22'),
('median', '0'),
('mean', '0.01644'),
('std', '0.5173'),
('skew', '-0.5757'),
('ann_mean', '4.208'),
('ann_std', '8.276'),
('sharpe', '0.5084'),
('sortino', '0.569'),
('avg_drawdown', '-11.93'),
('time_in_drawdown', '0.9731'),
('calmar', '0.1269'),
('avg_return_to_drawdown', '0.3526'),
('avg_loss', '-0.3307'),
('avg_gain', '0.3418'),
('gaintolossratio', '1.034'),
('profitfactor', '1.12'),
('hitrate', '0.5201'),
('t_stat', '2.929'),
('p_value', '0.003405')],
('You can also plot / print:',
['rolling_ann_std', 'drawdown', 'curve', 'percent'])]
|
看起来还是赚到了点钱的。顺便说一下,account 继承自 pandas 的 DataFrame。我们还能这样使用它:
1
2
3
4
5
6
7
8
| import syscore.pandas.strategy_functions
account.sharpe() ## 计算年化夏普比率(Sharpe Ratio),以及 stats 列表中其他任意指标
account.curve().plot() ## 绘制账户累计收益曲线(等价于 account.cumsum().plot())
account.percent ## 查看百分比收益曲线
syscore.pandas.strategy_functions.drawdown().plot() ## 查看以百分比表示的回撤曲线
account.weekly ## 周收益(默认是日收益,也可以取月度、年度)
account.gross.ann_mean() ## 毛收益的年化平均值(也可以取成本等;本例中没有成本)
|
在 notebooks/introduction_with_fxdata.ipynb 中,你可以看到一个稍作修改的版本:使用 IB 数据而不是预先准备好的 CSV 文件。
A simple system(简单系统)
(本节示例代码见 /examples/introduction/simplesystem.py)
上面的例子不错,但我们真正想要的,是构建一个由多条交易规则和更多品种组成的交易系统(system)。
一个系统由若干部分组成:
- 一份
data(刚才已经见过); - 一系列处理流程的阶段(stage);
- 可选的配置对象(configuration),用于控制每个阶段的行为。
完整的阶段列表通常包括:
- 预处理原始数据(本介绍不涉及)
- 运行若干交易规则以生成预测(forecast)
- 对预测做缩放和截断(scaling & capping)
- 把多条预测合成在一起
- 计算头寸规模(position sizing)
- 在多品种之间构建投资组合
- 计算 P&L(盈亏)
现在我们先从最简单的系统开始:只包含“交易规则阶段”的系统。先把环境重新搭起来:
1
2
3
4
| from sysdata.sim.csv_futures_sim_data import csvFuturesSimData
data=csvFuturesSimData()
from systems.provided.rules.ewmac import ewmac_forecast_with_defaults as ewmac
|
这里用到的 ewmac 是我们刚才那条规则的一个略微不同版本,内置了 Lfast 和 Lslow 的默认值。创建交易规则集合(trading rules set)有不少方式,这里先看最简单的一种:
1
2
3
| from systems.forecasting import Rules
my_rules=Rules(ewmac)
my_rules.trading_rules()
|
1
| {'rule0': TradingRule; function: <function ewmac_forecast_with_defaults at 0xb727fa4c>, data: and other_args: }
|
现在你可能还看不太懂(不用在意输出里的十六进制地址)。只要知道:我们创建了一个包含单一变体的交易规则字典,这条规则被自动命名为 rule0。rule0 这个名字并不直观,我们可以给它换个好记的名字:
1
2
| my_rules=Rules(dict(ewmac=ewmac))
my_rules.trading_rules()
|
1
| {'ewmac': TradingRule; function: <function ewmac_forecast_with_defaults at 0xb72bca4c>, data: and other_args: }
|
下一步是创建一个系统,把 data 对象和 my_rules 阶段组合进去:
1
2
3
| from systems.basesystem import System
my_system=System([my_rules], data)
my_system
|
1
| System with stages: rules
|
现在我们可以从系统里拿预测值了:
1
| my_system.rules.get_raw_forecast("SOFR", "ewmac").tail(5)
|
1
2
3
4
5
6
| 2016-05-05 4.602536
2016-05-06 4.649790
2016-05-09 4.726840
2016-05-10 4.896521
2016-05-11 5.083896
Freq: B, dtype: float64
|
这和前面“简单示例”得到的结果完全一样,只是我们这次多绕了几步。不要紧,后面它会变得非常有价值。
你会反复看到类似这样的调用模式:my_system...阶段名...get_something()。Rules 对象变成了系统上的一个属性,名字叫 rules。需要注意的是:每个阶段在系统里的属性名是固定的,与类名或实例名无关,这样我们总能按约定的名字找到它们。
如果我们想有多条交易规则呢?比如 ewmac 的两个不同变体?要定义两种不同口味的 ewmac,我们需要先多了解一点“交易规则”的概念。还记得刚才的 my_rules=Rules(dict(ewmac=ewmac)) 吗?下面是等价写法:
1
2
3
4
5
6
|
from systems.trading_rules import TradingRule
ewmac_rule = TradingRule(ewmac)
my_rules = Rules(dict(ewmac=ewmac_rule))
ewmac_rule
|
1
| TradingRule; function: <function ewmac_forecast_with_defaults at 0xb734ca4c>, data: and other_args:
|
现在来揭晓这个“神秘对象”到底是什么:一个 TradingRule 包含三部分——一个函数(function)、一组该函数需要的数据(data 列表)、以及一个包含其它参数的字典(other_args)。在这个简单例子里,函数就是我们导入的 ewmac;没有额外数据,也没有额外参数。
数据为空没问题,因为默认假设:如果你没有特别指定要用什么数据,就会把品种价格传给交易规则函数。没有参数也没关系,因为我们用的这版 ewmac 函数中已经提供了默认参数。
如果你熟悉 Python 里的 *args 和 **kwargs 概念:data 有点像 args——我们总是把一个“位置参数列表”传给 function;other_args 则像 kwargs——我们总是把一个“命名参数字典”传给 function
完整定义交易规则有几种不同写法,这里给出两种:
1
2
3
4
| ewmac_8=TradingRule((ewmac, [], dict(Lfast=8, Lslow=32))) ## 以元组形式 (function, data, other_args),中间的空列表表示没有额外数据
ewmac_32=TradingRule(dict(function=ewmac, other_args=dict(Lfast=32, Lslow=128))) ## 以字典形式
my_rules=Rules(dict(ewmac8=ewmac_8, ewmac32=ewmac_32))
my_rules.trading_rules()['ewmac32']
|
1
| TradingRule; function: <function ewmac_forecast_with_defaults at 0xb7252a4c>, data: and other_args: Lfast, Lslow
|
我们再确认一下:ewmac32 是否和前面那条 ewmac 规则完全一致(应该是的,因为 32、128 正好就是底层交易规则函数的默认参数):
1
2
| my_system=System([my_rules], data)
my_system.rules.get_raw_forecast("SOFR", "ewmac32").tail(5)
|
1
2
3
4
5
6
| 2016-05-05 4.602536
2016-05-06 4.649790
2016-05-09 4.726840
2016-05-10 4.896521
2016-05-11 5.083896
Freq: B, dtype: float64
|
下面引入 config 对象的概念。config(配置对象)让我们可以控制系统中各个阶段的行为。
配置对象既可以在交互环境里临时创建,也可以通过读取 YAML 文件来生成(后面会讲)。本质上,配置对象只是一组属性的集合。交互式创建方式如下:
1
2
3
| from sysdata.config.configdata import Config
my_config=Config()
my_config
|
1
2
| Config with elements:
## this line intentionally left blank. Apart from this comment of course.
|
目前看起来没什么内容。我们来看看怎么用 config 来定义交易规则:
1
2
3
4
| empty_rules=Rules()
my_config.trading_rules=dict(ewmac8=ewmac_8, ewmac32=ewmac_32)
my_system=System([empty_rules], data, my_config)
print(my_system.rules.get_raw_forecast("SOFR", "ewmac8"))
|
和之前相比,有几点不同:
- 我们传入的是一个“空”的
Rules 实例,不带任何参数; - 在
config 里创建了一个名为 trading_rules 的元素,把交易规则字典挂在其上; - 系统内部实际使用的是
config.trading_rules; - 我们依然通过系统对象访问
empty_rules 阶段。在系统内部,不同阶段的属性名是固定的,不受类名或实例名影响(永远叫 rules,而不是 Rules 或 empty_rules)。
注意:如果你既在 Rules() 里传入了交易规则字典,又在 config 里设置了 trading_rules,那么只会采用前者
为什么还要提供这种“通过 config 指定交易规则”的方式?换句话说,为什么要有 config?原因在于:相对于在一堆脚本里硬编码系统行为,用配置来控制系统要稳健得多——在生产环境中尤为重要。
那为什么还需要一个空的 Rules() 实例?本质上,我们是用 Rules 等积木来拼出整个 System 对象。这些积木通常是“不带参数的空实例”,其行为完全由传给系统的配置对象来控制。
从这个角度看,Rules(dict(...)) 并不是“默认用法”,只是有时候这样写比较方便,所以我也支持这种写法。
目前这些交易规则产生的预测既没有正确缩放(平均绝对值并非 10),也没有应用我推荐的 20 的截断上限。要修正这一点,我们需要给系统再加一个阶段:预测缩放与截断(forecast scaling and capping)。
要增加阶段,只需要在传给 System 的第一个列表参数里多放几个对象即可,顺序并不重要。
我们可以在“滚动的样本外窗口”上估计这些参数:
1
2
3
4
5
6
7
8
9
10
11
12
13
| from systems.forecast_scale_cap import ForecastScaleCap
## 默认情况下,系统会在多品种之间对估计结果做池化。这里最好告诉系统我们要用哪些品种:
#
my_config.instruments=["SOFR", "US10", "CORN", "SP500_micro"]
## 这个参数表示要进行估计:
my_config.use_forecast_scale_estimates=True
fcs=ForecastScaleCap()
my_system = System([fcs, my_rules], data, my_config)
print(my_system.forecastScaleCap.get_forecast_scalar("SOFR", "ewmac32").tail(5))
|
1
2
3
4
5
6
| 2016-05-05 2.846447
2016-05-06 2.846448
2016-05-09 2.846442
2016-05-10 2.846438
2016-05-11 2.846425
Freq: B, dtype: float64
|
或者,我们可以直接使用我第一本书《Systematic Trading》附录 B 里的固定数值:
1
2
3
4
5
6
7
8
9
10
11
| my_config.forecast_scalars=dict(ewmac8=5.3, ewmac32=2.65)
## 这个参数表示不再估计:
my_config.use_forecast_scale_estimates=False
my_system=System([fcs, empty_rules], data, my_config)
print(my_system.forecastScaleCap.get_forecast_scalar("SOFR", "ewmac32"))
2.65 # 现在是一个单一浮点数
my_system.forecastScaleCap.get_capped_forecast("SOFR", "ewmac32")
|
注意:传给 System([...], ...) 的阶段列表里,各阶段顺序并不重要
1
2
3
4
5
6
| 2016-05-05 12.196721
2016-05-06 12.321943
2016-05-09 12.526127
2016-05-10 12.975781
2016-05-11 13.472323
Freq: B, dtype: float64
|
我们没有显式传入 20.0 的预测截断上限,系统会使用默认值(写在系统默认配置文件里,更多细节见完整的用户指南)
既然有了两条交易规则变体,自然会想到要把它们“合成”(见我书中第 8 章)。下面给出一个“非常粗糙、但足够演示”的示例:使用各品种内等权的预测权重,并且不使用任何分散化乘数:
1
2
3
4
5
6
| from systems.forecast_combine import ForecastCombine
combiner = ForecastCombine()
my_system = System([fcs, empty_rules, combiner], data, my_config)
my_system.combForecast.get_forecast_weights("SOFR").tail(5)
my_system.combForecast.get_forecast_diversification_multiplier("SOFR").tail(5)
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
| WARNING: No forecast weights - using equal weights of 0.5000 over all 2 trading rules in system
#weights
ewmac32 ewmac8
2016-05-05 0.5 0.5
2016-05-06 0.5 0.5
2016-05-09 0.5 0.5
2016-05-10 0.5 0.5
2016-05-11 0.5 0.5
#fdm
2016-05-05 1
2016-05-06 1
2016-05-09 1
2016-05-10 1
2016-05-11 1
Freq: B, dtype: float64
|
当然,你也可以通过估计来得到分散化乘数(diversification multipliers)和权重。
注意:要估计这些东西,我们需要知道不同交易规则各自的表现情况,因此必须加入一个 Accounts 阶段来计算账户曲线;而账户曲线又依赖于头寸规模和原始数据阶段:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
| from systems.rawdata import RawData
from systems.positionsizing import PositionSizing
from systems.accounts.accounts_stage import Account
combiner = ForecastCombine()
raw_data = RawData()
position_size = PositionSizing()
my_account = Account()
## 使用最朴素的 Markowitz 方法,让结果更“有意思”一些……
my_config.forecast_weight_estimate = dict(method="one_period")
my_config.use_forecast_weight_estimates = True
my_config.use_forecast_div_mult_estimates = True
combiner = ForecastCombine()
my_system = System([my_account, fcs, my_rules, combiner, position_size, raw_data], data, my_config)
print(my_system.combForecast.get_forecast_weights("US10").tail(5))
print(my_system.combForecast.get_forecast_diversification_multiplier("US10").tail(5))
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
| # results may differ with other data
ewmac32 ewmac8
2016-11-07 0.131591 0.868409
2016-11-08 0.131753 0.868247
2016-11-09 0.131913 0.868087
2016-11-10 0.132072 0.867928
2016-11-11 0.132230 0.867770
## FDM
2016-11-07 1.048816
2016-11-08 1.048927
2016-11-09 1.049036
2016-11-10 1.049144
2016-11-11 1.049250
Freq: B, dtype: float64
|
这个结果略显“激进”。在这里我们改用一组手工设定的固定预测权重和分散化乘数:
1
2
3
4
5
6
| my_config.forecast_weights=dict(ewmac8=0.5, ewmac32=0.5)
my_config.forecast_div_multiplier=1.1
my_config.use_forecast_weight_estimates = False
my_config.use_forecast_div_mult_estimates = False
my_system=System([fcs, empty_rules, combiner, raw_data, position_size], data, my_config)
my_system.combForecast.get_combined_forecast("SOFR").tail(5)
|
1
2
3
4
5
6
| 2016-05-05 8.316347
2016-05-06 8.906937
2016-05-09 9.856458
2016-05-10 10.714341
2016-05-11 11.613205
Freq: B, dtype: float64
|
如果你在配合我的书一起看,这时应该知道下一步是决定风险目标(第 9 章)以及计算头寸规模(第 10 章)。下面做头寸缩放:
1
2
3
4
5
6
7
8
|
my_config.percentage_vol_target=25
my_config.notional_trading_capital=500000
my_config.base_currency="GBP"
my_system=System([ fcs, empty_rules, combiner, position_size, raw_data], data, my_config)
my_system.positionSize.get_subsystem_position("SOFR").tail(5)
|
1
2
3
4
5
6
| 2016-05-05 76.093317
2016-05-06 81.825564
2016-05-09 90.915309
2016-05-10 101.814765
2016-05-11 110.356363
Freq: B, dtype: float64
|
离目标已经不远了。要得到最终持仓,还需要最后一个阶段:把所有子系统头寸合成投资组合(第 11 章)。
我们可以估计投资组合相关参数:
1
2
3
4
5
6
7
8
9
10
11
12
13
| from systems.portfolio import Portfolios
portfolio = Portfolios()
## 使用 shrinkage 会加快计算,但我并不推荐在真实交易中这么做……
my_config.use_instrument_weight_estimates = True
my_config.use_instrument_div_mult_estimates = True
my_config.instrument_weight_estimate=dict(method="shrinkage", date_method="in_sample") ## 这样会快一些
my_system = System([my_account, fcs, my_rules, combiner, position_size, raw_data,
portfolio], data, my_config)
print(my_system.portfolio.get_instrument_weights())
print(my_system.portfolio.get_instrument_diversification_multiplier())
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
| CORN SOFR SP500_micro US10
2016-05-05 0.273715 0.245994 0.281982 0.198309
2016-05-06 0.273715 0.245994 0.281982 0.198309
2016-05-09 0.273715 0.245994 0.281982 0.198309
2016-05-10 0.273715 0.245994 0.281982 0.198309
2016-05-11 0.273715 0.245994 0.281982 0.198309
## idm
2016-05-05 1.679175
2016-05-06 1.679169
2016-05-09 1.679163
2016-05-10 1.679157
2016-05-11 1.679151
Freq: B, dtype: float64
|
或者,我们也可以完全“手写”标的权重和分散化乘数:
再强调一次:即便你什么都不设,系统也会默认为等权重、分散化乘数为 1.0
1
2
3
4
5
6
7
8
| my_config.instrument_weights=dict(US10=.1, SOFR=.4, CORN=.3, SP500_micro=.8)
my_config.instrument_div_multiplier=1.5
my_config.use_instrument_weight_estimates = False
my_config.use_instrument_div_mult_estimates = False
my_system=System([ fcs, empty_rules, combiner, position_size, raw_data, portfolio], data, my_config)
my_system.portfolio.get_notional_position("SOFR").tail(5)
|
1
2
3
4
5
6
| 2016-05-05 45.655990
2016-05-06 49.095339
2016-05-09 54.549186
2016-05-10 61.088859
2016-05-11 66.213818
Freq: B, dtype: float64
|
虽然目前为止一切都挺顺利,但你大概会好奇:这到底赚没赚钱?我们还需要再加一个阶段,用来统计虚拟盈亏:
1
2
3
4
5
| from systems.accounts.accounts_stage import Account
accounts=Account()
my_system=System([ fcs, empty_rules, combiner, position_size, raw_data, portfolio, accounts], data, my_config)
profits=my_system.accounts.portfolio()
profits.percent.stats()
|
1
| [[('min', '-0.1349'), ('max', '0.1313'), ('median', '-4.308e-06'), ('mean', '0.0005715'), ('std', '0.01726'), ('skew', '-0.1568'), ('ann_mean', '0.1463'), ('ann_std', '0.2762'), ('sharpe', '0.5297'), ...)]
|
这里我们再一次得到了熟悉的“账户对象”(accounting object)。为了避免你看到打瞌睡,我省略了一部分输出。
这些是扣除成本后的净利润(net of tax)指标。你也可以分别查看毛收益和成本:
1
2
| profits.gross.percent.stats() ## 与前面类似,profits.gross.sharpe() 等方法都可以照用
profits.costs.percent.stats()
|
更多细节见用户指南中的“成本(costs)”和 accountCurve 章节。
Getting config from dictionaries and files(通过字典与文件获取配置)
为了加快配置速度,你可以直接把一个字典传给 Config()。要复现我们上面例子中的设置,可以像这样构建一个字典:
1
2
3
4
| from sysdata.config.configdata import Config
my_config=Config(dict(trading_rules=dict(ewmac8=ewmac_8, ewmac32=ewmac_32), instrument_weights=dict(US10=.1, SOFR=.4, CORN=.3, SP500_micro=.2), instrument_div_multiplier=1.5, forecast_scalars=dict(ewmac8=5.3, ewmac32=2.65), forecast_weights=dict(ewmac8=0.5, ewmac32=0.5), forecast_div_multiplier=1.1
,percentage_vol_target=25, notional_trading_capital=500000, base_currency="GBP"))
my_config
|
注意:我们不需要显式告诉 config “不要用估计的预测缩放因子、预测权重和标的权重”,因为默认行为本来就是不做估计。
1
| Config with elements: base_currency, forecast_div_multiplier, forecast_scalars, forecast_weights, instrument_div_multiplier, instrument_weights, notional_trading_capital, percentage_vol_target, trading_rules
|
另一种完全等价的方式,是从一个 YAML 文件读取配置(具体来说,就是 /systems/provided/example/simplesystemconfig.yaml 这个文件)。即便你对 YAML 不熟也没关系,它本质上就是一种把嵌套字典、列表等 Python 对象写成纯文本的方式。唯一要注意的是:缩进非常关键,就像在 Python 里一样。
1
| my_config=Config("systems.provided.example.simplesystemconfig.yaml")
|
(注意这里传的不是文件路径,而是一个“项目内部的 Python 风格引用”)
如果你打开这个 YAML 文件,会发现 trading rule 函数是以字符串形式写出来的:systems.provided.rules.ewmac.ewmac_forecast_with_defaults。这是因为我们没法在 YAML 文本中直接创建函数(理论上可以,但要做不少额外工作,而且存在安全风险)。因此我们改为在配置文件里写明“函数在项目目录结构中的位置”。
类似地,对于 ewmac8 规则,我们指定了一个数据源 data.daily_prices,它对应的是 system.data.daily_prices()。这是默认值,所以之前我们才不必显式写出这一项,也因此在 ewmac32 规则里没有再次包含它。
同理,只要方法接收 instrument_code 作为参数,我们就可以指定系统对象上的任意属性或方法;数据输入也可以是一个列表。通过这些机制,你可以仅通过调整配置就灵活地定义几乎所有交易规则。
A simple pre-baked system(简单预制系统)
通常我们不会像前面那样手动一个阶段一个阶段地往系统里加(导入一堆类,然后创建长长的阶段对象列表)。在多数情况下,你应该使用一个“预制系统(pre‑baked system)”,然后根据需要对其做少量修改。
例如,下面是我们刚才构建示例的预制版本(代码见 /examples/introduction/prebakedsystems.py):
1
2
3
| from systems.provided.example.simplesystem import simplesystem
my_system=simplesystem()
my_system
|
1
| System with stages: accounts, portfolio, positionSize, combForecast, forecastScaleCap, rules
|
所有东西都会像之前那样正常工作:
1
| my_system.portfolio.get_notional_position("SOFR").tail(5)
|
默认情况下,这会加载与前面示例相同的数据,并从同一个 YAML 文件读取配置。不过,我们也可以手动传入新的 data 和修改后的 config,仍然复用这个预制系统:
1
2
3
4
5
6
7
8
| from sysdata.config.configdata import Config
from sysdata.sim.csv_futures_sim_data import csvFuturesSimData
my_config=Config("systems.provided.example.simplesystemconfig.yaml")
my_data=csvFuturesSimData()
## 如果需要,这里可以修改 my_config 和 my_data
my_system=simplesystem(config=my_config, data=my_data)
|
在绝大多数场景下,你都会以这种方式来创建新系统。
A complete pre-baked system(完整预制系统)
最后,我们再看一个更完整的“预制系统”示例:也就是我书中第 15 章定义的“标准期货系统化交易者(staunch systems trader)”示例。同样默认使用 CSV 数据。
(代码见 /examples/introduction/prebakedsystems.py)
1
2
3
| from systems.provided.futures_chapter15.basesystem import futures_system
system=futures_system()
system.portfolio.get_notional_position("EUROSTX").tail(5)
|
1
2
3
4
5
6
| pos
2015-12-04 0.624183
2015-12-07 0.629924
2015-12-08 0.538517
2015-12-09 0.451322
2015-12-10 0.385892
|
建议对照这个系统的配置文件,以及书中第 15 章来一起阅读。
你也可以使用一个类似系统,不过会对以下内容进行估计:预测缩放因子、预测与标的的分散化乘数、以及相关权重。由于这些估计会比较耗时,建议把日志级别开到最高,方便跟踪进度。
1
2
3
| from systems.provided.futures_chapter15.estimatedsystem import futures_system
system = futures_system()
system.portfolio.get_notional_position("EUROSTX").tail(5)
|
因为运行较慢,你可能会想把系统数据保存下来。相关数据存放在 cache 属性中:
1
2
3
4
5
6
7
| system.cache.pickle("private.this_system_name.pck") ## 可以使用任意文件扩展名
## 在新的会话中
from systems.provided.futures_chapter15.estimatedsystem import futures_system
system = futures_system()
system.cache.unpickle("private.this_system_name.pck")
system.accounts.portfolio().sharpe() ## 这样会快很多,并复用之前的计算结果
|
接下来你大概会想继续阅读回测指南。