LESSON 7 Portfolio Management
上一章中我们把 data pipeline 包含进了我们的策略中,现在就是要定义我们如何利用生成的数据来重新平衡重新布局我们的资产组合。我们的目标是基于资产的情绪分数,找到一个利润最高的目标资产组合结构,同时还要维持一个有一些约束限制的结构(出于考虑风险?)。这就是组合优化问题。
Quantopian 所带的 Optimize API 让我们可以很方便的将我们 pipeline 的输出结果转化成一个对象以及一些约束,我们可以使用 order_optimal_portfolio 来将我们现在的资产组合过渡到满足我们目标的目标资产组合组成。
第一步是定义对象。我们会用到函数 MaximizeAlpha,这个函数会按照各自的情绪分数把资源成比例的分配给各个资产。
# Import Optimize API module
import quantopian.optimize as opt
def rebalance(context, data):
# Create MaximizeAlpha objective using
# sentiment_score data from pipeline output
objective = opt.MaximizeAlpha(
context.output.sentiment_score
)
接下来我们需要指定一个关于约束的列表内容,正是我们希望我们的资产组合所能满足的,就是在初始化 initial 中定义一些门槛值并把它们储存成 context 的变量:
# Constraint parameters
context.max_leverage = 1.0
context.max_pos_size = 0.015
context.max_turnover = 0.95
现在要在 rebalance 中指出这些约束,通过我们上面所定义的门槛。
# Import Optimize API module
import quantopian.optimize as opt
def rebalance(context, data):
# Create MaximizeAlpha objective using
# sentiment_score data from pipeline output
objective = opt.MaximizeAlpha(
context.output.sentiment_score
)
# Create position size constraint
constrain_pos_size = opt.PositionConcentration.with_equal_bounds(
-context.max_pos_size,
context.max_pos_size
)
# Ensure long and short books
# are roughly the same size
dollar_neutral = opt.DollarNeutral()
# Constrain target portfolio's leverage
max_leverage = opt.MaxGrossExposure(context.max_leverage)
# Constrain portfolio turnover
max_turnover = opt.MaxTurnover(context.max_turnover)
可以看到我们在定义了对象 objective 之后,又定义了三个约束。第一个是 constrain_pos_size,约定的内容是持有的比重,也就是单只资产我们所能持有的占我们所有资产里面的比重,我们例子中给出的数值是0.015,也就是说,不管我们是做多还是做空,它都不应该占我们所持有的净资产的0.015以上,毕竟单只资产持有比重太高就意味着风险更高。第二个是 max_leverage,杠杆率,我们限制最大的杠杆率为1,也就是不允许使用杠杆工具来冒更大的风险。第三个是 turnover 周转率,0.95的数值也是在限制我们不能一次性将所有的资产做出改变,也是出了稳定的目的。
最后把我们对象 objective 和约束传递给 order_optimal_portfolio 来计算得到一个目标组成,并给出需要执行的命令来调整我们的资产组合,总之就是这个函数就可以完成我们的要的结果。把这个放在 rebalance 中,重新理一下思路,就是我们在每周的第一个交易日就会执行 rebalance ,重新布局我们的资产,而我们所做的期望,就是通过情绪分数来判断的;同时不是简单的多多挑选情绪分数高的资产,那样子的话就会变成只持有最高的那只资产,这也是我们为什么要给出那几个约束,通过持有率的限制,禁止借入杠杆,周转率也不能高到极值100%,相当于线性优化问题,得到的结果就是我们所能达到的总的情绪分数最高的一个组合。
# Import Algorithm API functions
from quantopian.algorithm import order_optimal_portfolio
# Import Optimize API module
import quantopian.optimize as opt
def rebalance(context, data):
# Create MaximizeAlpha objective using
# sentiment_score data from pipeline output
objective = opt.MaximizeAlpha(
context.output.sentiment_score
)
# Create position size constraint
constrain_pos_size = opt.PositionConcentration.with_equal_bounds(
-context.max_pos_size,
context.max_pos_size
)
# Ensure long and short books
# are roughly the same size
dollar_neutral = opt.DollarNeutral()
# Constrain target portfolio's leverage
max_leverage = opt.MaxGrossExposure(context.max_leverage)
# Constrain portfolio turnover
max_turnover = opt.MaxTurnover(context.max_turnover)
# Rebalance portfolio using objective
# and list of constraints
order_optimal_portfolio(
objective=objective,
constraints=[
max_leverage,
dollar_neutral,
constrain_pos_size,
max_turnover,
]
)
order_optimal_portfolio 需要传入的几个参数都在上面给好了,也是用 opt,也就是 optimize 这个模块来完成的。
Risk Management 风险管理
除了给我们的目标资产组合设置约束,我们还要限制面对各种风险因子的敞口,因为这些会深远地影响我们策略。例如,因为 stockwits 中情绪指标的瞬变性质 transient nature,以及我们依赖于情绪指标的意向也会波动,我们的策略也就是暴露在短期的反复无常的风险中。
我们将会使用 Quantopian 中的风险模型来管理我们面对各种风险时的敞口,这个模型可以计算16种不同的风险因子对应的敞口。如果要在我们的算法调用这些数据,就用 risk_loading_pipeline 函数,将会返回一个 pipeline 可以产生一个输出列表,包含了 Risk Model 里面的各个风险因子对应的敞口的数据。
跟我们前面做的一样,我们也需要将这个 data pipeline 连接进我们的策略中,并且给它命名。之后我们就可以在 before_trading_start 中获取我们要的输出并且存储在 context 中:
# Import Algorithm API functions
from quantopian.algorithm import (
attach_pipeline,
pipeline_output,
)
# Import Risk API method
from quantopian.pipeline.experimental import risk_loading_pipeline
def initialize(context):
# Constraint parameters
context.max_leverage = 1.0
context.max_pos_size = 0.015
context.max_turnover = 0.95
# Attach data pipelines
attach_pipeline(
make_pipeline(),
'data_pipe'
)
attach_pipeline(
risk_loading_pipeline(),
'risk_pipe'
)
# Schedule rebalance function
schedule_function(
rebalance,
date_rules.week_start(),
time_rules.market_open(),
)
def before_trading_start(context, data):
# Get pipeline outputs and
# store them in context
context.output = pipeline_output(
'data_pipe'
)
context.risk_factor_betas = pipeline_output(
'risk_pipe'
)
接下来就是添加一个 RiskModelExposure 来约束我们的资产组合的优化逻辑。这个约束用到了从 Risk Model 中生成的数据,并给所有的各项风险敞口设置上限。这下面的代码中,传递的第一个对象是 context.risk_factor_betas,就是我们在上面获取的关于风险因子的敞口的数据 pipeline 的输出,正是一个 DataFrame 格式的内容;然后 version,我的理解应该就是 optimize 中有着关于风险敞口的默认上限可以使用。
# Constrain target portfolio's risk exposure
# By default, max sector exposure is set at
# 0.2, and max style exposure is set at 0.4
factor_risk_constraints = opt.experimental.RiskModelExposure(
context.risk_factor_betas,
version=opt.Newest
)
最终,接下来的算法围绕我们的策略和资产组合构建逻辑,已经准备好接受回测了。拷贝,点击相关的按钮就可以运行完整的回测了。以下就是完整的代码,重新整理一下思路:可以大致分成两个重点部分,初始化和交易前的操作。我们在前面 import 了一些工具来帮助我们获取历史数据并且进行一定的加工,例如 Pipeline,stockwits.bull_bear_minus以及均线工具SimpleMovingAverage;在选定了以三日情绪指标均值作为参考之后我们就要开始构建资产组合,但出了风险的考虑,我们不能直接就全部份额持有最高的那只,所以一方面我们在 context 中补充了一些约束 constraints,然后又 import 了风险敞口的数据 pipeline,来供我们在满足简单的持有份额周转率杠杆率以外,还可以再考虑上各个风险因子敞口也不能太大,这一系列内容放置在了 rabalance 中,因为我们的策略可以看成一个周策略,每周调整一次,而 rebalancec 设定在 schedule_function 中,设定的执行时间就是每周的第一个交易日开始之时。里面有一些语句的意思不是特别明朗,不过大致也还可以猜得出前后呼应的意思,多联系应该就会熟悉了。
# Import Algorithm API functions
from quantopian.algorithm import (
attach_pipeline,
pipeline_output,
order_optimal_portfolio,
)
# Import Optimize API module
import quantopian.optimize as opt
# Pipeline imports
from quantopian.pipeline import Pipeline
from quantopian.pipeline.data.psychsignal import stocktwits
from quantopian.pipeline.factors import SimpleMovingAverage
# Import built-in universe and Risk API method
from quantopian.pipeline.filters import QTradableStocksUS
from quantopian.pipeline.experimental import risk_loading_pipeline
def initialize(context):
# Constraint parameters
context.max_leverage = 1.0
context.max_pos_size = 0.015
context.max_turnover = 0.95
# Attach data pipelines
attach_pipeline(
make_pipeline(),
'data_pipe'
)
attach_pipeline(
risk_loading_pipeline(),
'risk_pipe'
)
# Schedule rebalance function
schedule_function(
rebalance,
date_rules.week_start(),
time_rules.market_open(),
)
def before_trading_start(context, data):
# Get pipeline outputs and
# store them in context
context.output = pipeline_output('data_pipe')
context.risk_factor_betas = pipeline_output('risk_pipe')
# Pipeline definition
def make_pipeline():
sentiment_score = SimpleMovingAverage(
inputs=[stocktwits.bull_minus_bear],
window_length=3,
mask=QTradableStocksUS()
)
return Pipeline(
columns={
'sentiment_score': sentiment_score,
},
screen=sentiment_score.notnull()
)
def rebalance(context, data):
# Create MaximizeAlpha objective using
# sentiment_score data from pipeline output
objective = opt.MaximizeAlpha(
context.output.sentiment_score
)
# Create position size constraint
constrain_pos_size = opt.PositionConcentration.with_equal_bounds(
-context.max_pos_size,
context.max_pos_size
)
# Ensure long and short books
# are roughly the same size
dollar_neutral = opt.DollarNeutral()
# Constrain target portfolio's leverage
max_leverage = opt.MaxGrossExposure(context.max_leverage)
# Constrain portfolio turnover
max_turnover = opt.MaxTurnover(context.max_turnover)
# Constrain target portfolio's risk exposure
# By default, max sector exposure is set at
# 0.2, and max style exposure is set at 0.4
factor_risk_constraints = opt.experimental.RiskModelExposure(
context.risk_factor_betas,
version=opt.Newest
)
# Rebalance portfolio using objective
# and list of constraints
order_optimal_portfolio(
objective=objective,
constraints=[
max_leverage,
dollar_neutral,
constrain_pos_size,
max_turnover,
factor_risk_constraints,
]
)