在上一篇文章中,我们研究了股债二八策略和股债风险平价策略。(详情请见:【基金量化研究系列】大类资产配置研究(一)——股债二八配置策略与股债风险平价策略)在这篇文章中,我们仍以“易方达沪深300ETF(510310)”与“国泰上证5年国债ETF(511010)”作为股票市场与债券市场的基准可投标的,来引入一种新的策略——股债二八轮动策略。
本文仍延续前文的基本假设,规定:
(1)市场中不允许融资做多与融券做空;
(2)建仓调仓过程中不允许借款、参与国债回购;
(3)每日仅在收盘时间以收盘价格进行交易;
(4)未特别规定,忽略调、建仓手续费。
所谓股债二八轮动策略,顾名思义,就是股票和债券“换着”配以20%和80%的仓位,而对仓位进行调节的“转换信号”就是基于“价格动量”指标。
策略如下:以前 m 个交易日作为窗口期,若沪深300ETF在窗口期中的收益率高于国债ETF对应的收益率,则认为在股票市场相比于债券市场有更强的上升趋势,并在当日收盘时将沪深300ETF的仓位调整为80%,并将国债ETF的仓位调整为20%,以博取更高收益;反之,若沪深300ETF在窗口期中的收益率高于国债ETF对应的收益率,则认为股市走弱,并在当日收盘时将沪深300ETF的仓位调整为20%,并将国债ETF的仓位调整为80%,以规避风险。
策略的伪代码如下:
Step 0 初始化参数:规定调仓频率(以日频为例),并规定回看窗口期长度为 m(即 m 个单位的调仓周期),规定 t = 0;
Step 1 在 t 个交易周期,计算股票和债券的窗口期累计收益率 Rs(t) 与 RB(t):
R S ( t ) = ∏ k = t − m t − 1 [ 1 + r S ( k ) ] − 1 = P S ( t − 1 ) P S ( t − m ) − 1 R_S(t) = \prod_{k = t-m}^{t-1} [1+r_S(k)] -1 = \frac{P_S(t-1)}{P_S(t-m)} - 1 RS(t)=k=t−m∏t−1[1+rS(k)]−1=PS(t−m)PS(t−1)−1 R B ( t ) = ∏ k = t − m t − 1 [ 1 + r B ( k ) ] − 1 = P B ( t − 1 ) P B ( t − m ) − 1 R_B(t) = \prod_{k = t-m}^{t-1} [1+r_B(k)] -1 = \frac{P_B(t-1)}{P_B(t-m)} - 1 RB(t)=k=t−m∏t−1[1+rB(k)]−1=PB(t−m)PB(t−1)−1
其中:
· rS(t) 为股票在第 t 个子周期内收益率;
· rB(t) 为债券在第 t 个子周期内收益率;
· PS(t) 为股票在第 t 个子周期收盘价格;
· PB(t) 为债券在第 t 个子周期收盘价格。
Step 2 比较 Rs 与 RB 的大小。若 Rs > RB,则规定 ωS(t) = 0.8,ωB(t) = 0.2;若 Rs ≤ RB,则规定 ωS(t) = 0.2,ωB(t) = 0.8;
Step 3 令 t = t + 1,重复 Step 1 直至回测结束。
实现的python代码请见第四章。
我们不难发现,这一“轮动策略”的思想实际上就是希望利用股票市场中的“动量效应(Momentum Effect)”,在股票市场走牛的时候获取更高的收益,并且在走熊的时候及时止损。
股债二八轮动策略跟上一篇文章中的股债二八策略一样有一个缺陷,就是这个“二八”配置比例是基于欧美市场的实证经验而得来的,直接套用中国市场仍然有一种“拍脑门”的嫌疑。因此,我们需要一款更适应于A股市场情况的配置比例。
其中一种想法是:我们可以在 保留“动量效应” 的基础上 加入“风险平价”配置比例,即:
ω S ( t ) = { σ S ( t ) σ S ( t ) + σ B ( t ) if R S ( t ) > R B ( t ) σ B ( t ) σ S ( t ) + σ B ( t ) if R S ( t ) ≤ R B ( t ) \omega_S(t) = \begin{cases} \frac{\sigma_S(t)}{\sigma_S(t) + \sigma_B(t)} &\text{if } R_S(t) > R_B(t) \\ \frac{\sigma_B(t)}{\sigma_S(t) + \sigma_B(t)} &\text{if } R_S(t) ≤ R_B(t) \end{cases} ωS(t)={σS(t)+σB(t)σS(t)σS(t)+σB(t)σB(t)if RS(t)>RB(t)if RS(t)≤RB(t) ω B ( t ) = 1 − ω S ( t ) \omega_B(t) = 1 - \omega_S(t) ωB(t)=1−ωS(t)
利用上述公式代替传统股债二八轮动策略中计算权重的方式,即可得到“升级版”模型——动态再平衡股债二八轮动策略。
注:该策略为笔者原创策略。
话不多说,让我们上码跑一跑,看看各种策略的表现如何吧!
# -*- coding: utf-8 -*-
# author: Mikey_Sun time: 5/15/2020
# all copyright belongs to the author. NO COPY ALLOWED.
from EmQuantAPI import * #使用chioce数据库。详细使用说明请查阅百度
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
# Module 1: 下载数据,并存储到本地
def download_data_from_choice():
loginresult = c.start()
codes = "510310.SH,511010.SH" # 所需数据的代码。510310为沪深300ETF,511010为五年国债ETF
indicators = "open,high,low,close" # 需要下载的数据指标
startdate = "20150101" # 回测起始时间,可自行修改
enddate = "20200511" # 回测结束时间,可自行修改
data = c.csd(codes=codes, indicators=indicators, startdate=startdate, enddate=enddate,
options=("Ispandas,1")) # 导入数据,以pd.DataFrame类型格式存储
path = '你数据的存储地址' # 将数据保存到你的本地地址(需自行修改)
data.to_excel(path, index=True, header=True)
download_data_from_choice() #下载数据
path = '你数据的存储地址'
data = pd.read_excel(path) #读取数据
data.set_index(data["DATES"], inplace=True) #设置数据索引为交易日
HS300ETF = pd.DataFrame(data[data["CODES"] == '510310.SH']["CLOSE"].astype(float)) #沪深300ETF专属数据结构。仅保留收盘价格
GZ5yETF = pd.DataFrame(data[data["CODES"] == '511010.SH']["CLOSE"].astype(float)) #国债ETF专属数据结构。仅保留收盘价格
td_dates = HS300ETF.index #记录交易日信息,方便以后进行查找
n = len(td_dates)
# Module2: 计算收益率
pd.set_option('mode.chained_assignment', None)
# 创建收益率序列
HS300ETF['RETURN'] = 0 #创建收益率序列
GZ5yETF['RETURN'] = 0 #创建收益率序列
HS300ETF['RETURN'].astype(float)#设定格式为浮点
GZ5yETF['RETURN'].astype(float)#设定格式为浮点
# 计算收益率
HS300ETF["RETURN"][td_dates[1:]] = np.array(HS300ETF["CLOSE"][td_dates[1:]]) / np.array(HS300ETF["CLOSE"][td_dates[:-1]]) - 1 #计算HS300ETF日收益率
GZ5yETF["RETURN"][td_dates[1:]] = np.array(GZ5yETF["CLOSE"][td_dates[1:]]) / np.array(GZ5yETF["CLOSE"][td_dates[:-1]]) - 1 #计算GZ5yETF日收益率
# 将收益率序列单独存储至returns
returns = pd.DataFrame({'510310.SH':HS300ETF["RETURN"], '511010.SH':GZ5yETF["RETURN"]},
index= td_dates)
#print(returns.head()) #打印表头查看
# Module 3: 纯股与纯债策略
nv_HS300ETF = [1] #记录全仓股票策略净值
nv_GZ5yETF = [1] #记录全仓债券策略净值
r_HS300ETF = [] #记录全仓股票策略收益率
r_GZ5yETF = [] #记录全仓债券策略收益率
for i in range(n):
nv_HS300ETF.append( nv_HS300ETF[i] *(1 + returns['510310.SH'][td_dates[i]]))
nv_GZ5yETF.append( nv_GZ5yETF[i] *(1 + returns['511010.SH'][td_dates[i]]))
r_HS300ETF.append(returns['510310.SH'][td_dates[i]])
r_GZ5yETF.append(returns['511010.SH'][td_dates[i]])
# Module 4: 计算窗口期收益率与波动率函数
# 定义计算移动平均波动率函数:
def Sigma_MA(t, return_series, type):
"""
t: 回溯窗口长度
return_series: 收益率序列
type: 输出序列格式。支持list格式与pd.DataFrame格式
"""
td_dates = return_series.index
return_seires = list(return_series)
n = len(return_series)
sigma_series = []
for i in range(t, n):
sigma_series.append(np.std(return_series[i-t:i]))
if type == "list":
return sigma_series
elif type == "DataFrame":
return pd.DataFrame({"sigma_series":sigma_series}, index = td_dates[t:n])
else:
return "Type Input Error!"
def Momentum_MA(t, return_series, type):
"""
t: 回溯窗口长度
return_series: 收益率序列
type: 输出序列格式。支持list格式与pd.DataFrame格式
"""
td_dates = return_series.index
return_seires = list(return_series)
n = len(return_series)
momentum_series = []
for i in range(t, n):
momentum = 1
for j in range(i-t, i):
momentum *= 1 + return_series[td_dates[j]]
momentum_series.append(momentum)
if type == "list":
return momentum_series
elif type == "DataFrame":
return pd.DataFrame({"momentum_series":momentum_series}, index = td_dates[t:n])
else:
return "Type Input Error!"
# Module 5: 计算策略净值与日收益率
t = 20 #定义窗口期长度
# Step 1: 计算动量序列与波动率序列:
momentum_seires_HS300ETF = Momentum_MA(t = t, return_series = returns['510310.SH'], type = "DataFrame")['momentum_series']
momentum_seires_GZ5yETF = Momentum_MA(t = t, return_series = returns['511010.SH'], type = "DataFrame")['momentum_series']
sigma_seires_HS300ETF = Sigma_MA(t = t, return_series = returns['510310.SH'], type = "DataFrame")['sigma_series']
sigma_seires_GZ5yETF = Sigma_MA(t = t, return_series = returns['511010.SH'], type = "DataFrame")['sigma_series']
# Sstep 2: 计算各策略收益率与净值序列:
nv_strategy_momentum = [1] # 记录股票动量策略净值
nv_strategy_2080 = [1] # 记录股债二八策略净值
nv_strategy_28rotate = [1] # 记录股债二八轮动策略净值
nv_strategy_28rotate_rebalance = [1] # 记录动态再平衡股债二八轮动策略净值
r_strategy_momentum = [] # 记录股票动量策略收益率
r_strategy_28rotate = [] # 记录股债二八策略收益率
r_strategy_2080 = [] # 记录股债二八轮动策略收益率
r_strategy_28rotate_rebalance = [] # 记录动态再平衡股债二八轮动策略收益率
for i in range(n):
nv_strategy_2080.append(nv_strategy_2080[i] * (
1 + 0.2 * returns['510310.SH'][td_dates[i]] + 0.8 * returns['511010.SH'][td_dates[i]]))
r_strategy_2080.append(0.2 * returns['510310.SH'][td_dates[i]] + 0.8 * returns['511010.SH'][td_dates[i]])
# 由于这一策略对样本有损耗,因此需要单独计算
if td_dates[i] in momentum_seires_HS300ETF.index:
# 模型1:股债28轮动模型
# 计算权重
if momentum_seires_HS300ETF.loc[td_dates[i]] > momentum_seires_GZ5yETF.loc[td_dates[i]]:
w_bond_daily = 0.2
w_stock_daily = 1 - w_bond_daily
else:
w_bond_daily = 0.8
w_stock_daily = 1 - w_bond_daily
# 计算净值
nv_strategy_28rotate.append(nv_strategy_28rotate[i] * ( 1 + w_stock_daily * returns['510310.SH'][td_dates[i]] + w_bond_daily * returns['511010.SH'][
td_dates[i]]))
r_strategy_28rotate.append(w_stock_daily * returns['510310.SH'][td_dates[i]] + w_bond_daily * returns['511010.SH'][td_dates[i]])
# 模型2:股票纯动量模型
if momentum_seires_HS300ETF.loc[td_dates[i]] > 0:
nv_strategy_momentum.append(nv_strategy_momentum[i]* (1 + returns['510310.SH'][td_dates[i]] ))
r_strategy_momentum.append( returns['510310.SH'][td_dates[i]] )
else:
nv_strategy_momentum.append(nv_strategy_momentum[i] * (1 + returns['511010.SH'][td_dates[i]]))
r_strategy_momentum.append(returns['511010.SH'][td_dates[i]])
# 模型3:组合策略
# 计算权重
if momentum_seires_HS300ETF.loc[td_dates[i]] > momentum_seires_GZ5yETF.loc[td_dates[i]]:
w_stock_daily = sigma_seires_HS300ETF.loc[td_dates[i]] / (
sigma_seires_HS300ETF.loc[td_dates[i]] + sigma_seires_GZ5yETF.loc[td_dates[i]])
w_bond_daily = 1 - w_bond_daily
else:
w_bond_daily = sigma_seires_HS300ETF.loc[td_dates[i]] / (
sigma_seires_HS300ETF.loc[td_dates[i]] + sigma_seires_GZ5yETF.loc[td_dates[i]])
w_stock_daily = 1 - w_bond_daily
# 计算净值
nv_strategy_28rotate_rebalance.append(nv_strategy_28rotate_rebalance[i] * (1 + w_stock_daily * returns['510310.SH'][td_dates[i]] + w_bond_daily * returns['511010.SH'][td_dates[i]]))
r_strategy_28rotate_rebalance.append(w_stock_daily * returns['510310.SH'][td_dates[i]] + w_bond_daily * returns['511010.SH'][td_dates[i]])
else:
nv_strategy_28rotate.append(nv_strategy_28rotate[i])
r_strategy_28rotate.append(0)
nv_strategy_momentum.append(nv_strategy_momentum[i])
r_strategy_momentum.append(0)
v_strategy_28rotate_rebalance.append(nv_strategy_28rotate_rebalance[i])
r_strategy_28rotate_rebalance.append(0)
# Step 3: 计算各策略日波动率
sigmas = []
sigmas.append(np.std(r_HS300ETF[t:n-t]))
sigmas.append(np.std(r_GZ5yETF[t:n-t]))
sigmas.append(np.std(r_strategy_momentum[t:n-t]))
sigmas.append(np.std(r_strategy_2080[t:n-t]))
sigmas.append(np.std(r_strategy_28rotate[t:n-t]))
sigmas.append(np.std(r_strategy_28rotate_rebalance[t:n-t]))
# Step 4: 计算各策略日收益率
mean_returns = []
mean_returns.append(np.mean(r_HS300ETF[t:n-t]))
mean_returns.append(np.mean(r_GZ5yETF[t:n-t]))
mean_returns.append(np.mean(r_strategy_momentum[t:n-t]))
mean_returns.append(np.mean(r_strategy_2080[t:n-t]))
mean_returns.append(np.mean(r_strategy_28rotate[t:n-t]))
mean_returns.append(np.mean(r_strategy_28rotate_rebalance[t:n-t]))
# Step 5: 计算各策略日夏普比率
sharpe_ratios = []
for i in range(len(sigmas)):
sharpe_ratios.append(mean_returns[i] / sigmas[i])
# Step 6: 打印结果,并绘制净值走势图
outcome = pd.DataFrame({'mean_returns': mean_returns, 'sigmas': sigmas, 'sharpe_ratios': sharpe_ratios}, index = ['纯股','纯债','动量' ,'股债二八','二八轮动','平价轮动'])
print(outcome)
plt.plot(nv_HS300ETF[t:n], label = "nv_HS300ETF", color = "black")
plt.plot(nv_GZ5yETF[t:n], label = "nv_GZ5yETF", color = "red")
plt.plot(nv_strategy_2080[t:n], label = "nv_strategy_2080", color = "blue")
plt.plot(nv_strategy_momentum[t:n], label="nv_strategy_momentum", color="purple")
plt.plot(nv_strategy_28rotate[t:n], label = "nv_strategy_28rotate", color = "green")
plt.plot(nv_strategy_28rotate_rebalance[t:n], label = "nv_strategy_28rotate", color = "grey")
plt.show()
运行结果如下:
mean_returns sigmas sharpe_ratios
纯股 0.000281 0.016301 0.017224
纯债 0.000133 0.001754 0.076070
动量 0.000281 0.016301 0.017224
股债二八 0.000163 0.003378 0.048229
二八轮动 0.000386 0.007622 0.050675
平价轮动 0.000475 0.008058 0.058903
Process finished with exit code 0
图1 2015年1月30日至2020年5月11日各策略净值走势对比图(以2015年1月29日为基日,定义净值为1。黑线为全股策略,红线为全债策略,紫线为股票动量策略,蓝线为股债二八策略,绿线为股债二八轮动策略,灰线为动态再平衡股债二八轮动策略)
从输出结果可以看出,相比于一般的股债二八模型和股债二八轮动模型,动态再平衡股债轮动模型有用最高的累计收益率:五年累计收益率达到80%(年化12.47%),远超纯股与纯债策略。同时,该策略亦拥有更高的日收益率和收益-风险比率。相比于纯股策略而言,其具有更高的收益水平和更低的波动率,因此是替代纯股策略的一个十分优秀的策略。
然而,通过净值走势图(图1)可以发现,该策略大部分收益是在股票大牛市的时候(2015年)获得的,在熊市阶段收益几乎腰斩,而在震荡市之中轮动策略并没有明显跑赢纯债策略和纯股策略。同时,轮动策略也会出现较长时间和较大幅度的回撤,对于一般的中小投资者很可能是无法容忍的。
此外,由于在回测过程中忽略了交易成本,因此动态再平衡策略相比于非动态调仓策略而言需要花费更多的手续费,在计算手续费的基础上动态再平衡策略很可能并没有显著的优势。
至此,在仅考虑股票和债券两类资产的前提下,我们已经介绍了:
在上述策略中,风险平价策略可以理解为纯债策略的增厚,而轮动策略可以理解为纯股策略的增厚。
在后面的研究中,我们将加入更多类别的资产,从而进一步体验资产配置对风险对冲于收益增厚的重要作用吧!
若想查阅本系列全部文章,请参见目录页:系列文章目录索引。
欢迎感兴趣的小伙伴来跟作者一起挑刺儿~ 包括但不限于语言上的、排版上的和内容上的不足和疏漏~ 一起进步呀!
有任何问题,欢迎在本文下方留言,或者将问题发送至勘误邮箱: [email protected]
谢谢大家!