行业轮动策略模板

策略分享
标签: #<Tag:0x00007fcc11531580>

(XiaoyuDu) #1

用纯代码的方式实现了一个行业轮动策略,分享给大家,欢迎大家使用文中的代码框架

1) 思路与逻辑

2)代码与解释

3)结果分析

1)

本文解决的问题

组合优化方法能否产生收益?有哪几种常见的组合优化方法?他们的效果如何?中国的行业指数之间有什么关联?

组合优化的目的在于给予高收益,低风险的标的更多的权重,或者通过对冲风险来提高组合整体表现。策略里面大部分情况下都会默认平均持仓的方法,这里面的“平均”二字既可以是按数量平均,也可以是按市值平均,我们通过计算协方差(或者相关系数)来观察组合内成员相互关联的强度

本文的组合优化思路集中于降低风险(波动率)

接下来我们将28个申万一级行业指数进行研究,进行行业的不同权重配置

  • 第一部分:数据准备阶段
    • 通过BigQuant平台数据获取申万一级行业指数行情
    • 构造收益序列
    • 相关性矩阵展示行业关联信息
  • 第二部分:构造组合目标函数
    • 获取调仓日历信息
    • 构造组合优化方法
    • 计算并记录调仓权重
  • 第三部分:回测收益检查
    • 进行回测收益统计
    • 风险指标汇总
四种策略方法

)平均分仓

平均分仓就是对28个行业进行等权重资金配置

2)等波动率

等波动率是使得每个股票贡献的表征风险相等,如组合中第i个标的的权重为wiwi,波动率为wiσiwiσi

image

3)最小方差

最小方差,也可以称为最小波动或最低风险

image
4)风险平价

风险平价模型是通过衡量组合各标的对于组合的风险贡献度,来制定投资模型的各类资产权重。

风险平价的目标是均衡配置多个资产的风险,使得组合标的达到等风险贡献

研究结论

以下结论是基于对近5年申万行业指数时间序列的不同组合优化方法得出

(1)和上证综指的对比来看,通过行业组合优化的效果明显的强于上证综指,主要表现为熊市抗跌性强,牛市上涨幅度大。

(2)几种优化方法区分度不够明显,但是长期来看,风险平价法始终有微弱的优势。

(3)各行业指数之间以及与大盘有高度的相关关系,因此在A股市场内进行行业配置很难抵消整体的系统性风险。

组合优化方法是分散化投资的理论工具,分散化投资降低风险,是寻找相关度低的资产作为投资标的,而A股市场标的基本都有着较高的贝塔值,这使得所有行业同样与大盘有着一致的牛熊,因此组合优化效果不尽如人意,基于组合优化方法,在更为宏观的市场领域内,选择相关度低的资产进行配置,这样的场景能更好的发挥组合优化模型的工具优势。

2)

所需要的除了平时的标准库外还需要一个用于做优化库
通过DataSource()函数获得行业指数行情和行业指数名称表并对两者进行连接
对原始行业指数行情数据画图展示发现极高的相关性
在交易日期和频次的选择上进行一月一动,因为市场千变万化,每个月都会有不同的新闻
组合优化的方法实现,(风险平价,等波动率,最小方差)
权重调整的展示,通过频率累积图
对策略进行一个整体的概括,通过计算年化收益,夏普比率,最大回撤等等

克隆策略

组合优化策略比较分析与纯代码框架介绍

在股票市场上的交易过程实际上就是配股过程,那么怎样的组合可以降低风险提高收益呢?

我们这里以申万行业指数数据为数据源讨论一下组合优化的几个策略并提供一个纯代码的框架

1) 所需库的加载

In [84]:
# Load the packages
# 调用包的部分

import pandas as pd  #金融数据处理包
import numpy as np   #数学包
import seaborn as sns  #数据可视化包
import matplotlib as mpl   #数据可视化包
#from cvxopt import solvers, matrix  #优化包
import datetime as dt   #时间对象
import re #正则表达式
from scipy.optimize import minimize  #优化包
from matplotlib import pyplot as plt #画图

print('Package successfully loaded')
Package successfully loaded

2)获取行业的日频行情列表

In [85]:
# Get the SW industry index chart
# 获得行业指数价格时间序列表

def Get_SW_Industry_Index_CN(date1,date2):
    
    # 获取行业指数代码和名称对照表
    hy_df = DataSource('basic_info_IndustrySw').read()
    hy_df = hy_df[hy_df['industry_sw_level']==1]    # 我们用的是一级行业指数
    hy_df['code']=hy_df['code'].apply(lambda x:str('SW'+x+'.HIX'))  # 我们需要对行业指数字符串做改动方便和后面的join
    
    X = DataSource('basic_info_index_CN_STOCK_A').read()
    
    # X here is all of the indexes and we need to filter the SW index out
    X['flag'] = X['instrument'].apply(lambda x:x[:2]) 
    X=X[X['flag']=='SW']
    
    # Then using The names from X and date1,date2 we can get the datasource Y as timeseries of thoese indexes now
    Y=DataSource('bar1d_index_CN_STOCK_A').read(start_date=date1,end_date=date2,instruments=list(X['instrument']))
    #Y['instrument'] = Y['instrument'].apply(lambda x:re.findall("\d+",x)[0])
    # 这里我们无法在图上用matplotlib标出中文,但是代码可以
    Y=Y.merge(hy_df,how='inner',left_on='instrument',right_on='code')
    
    
    # Do the pivot to transform the chart
    Y_Industry = pd.pivot_table(Y,values='close',index='date',columns='name')
    
    hy_df = pd.DataFrame(Y_Industry.columns)
    return Y_Industry,hy_df

def Get_SW_Industry_Index_EN(date1,date2):
    
    # 获取行业指数代码和名称对照表
    hy_df = DataSource('basic_info_IndustrySw').read()
    hy_df = hy_df[hy_df['industry_sw_level']==1]    # 我们用的是一级行业指数
    hy_df['code']=hy_df['code'].apply(lambda x:str('SW'+x+'.HIX'))  # 我们需要对行业指数字符串做改动方便和后面的join
    
    X = DataSource('basic_info_index_CN_STOCK_A').read()
    
    # X here is all of the indexes and we need to filter the SW index out
    X['flag'] = X['instrument'].apply(lambda x:x[:2]) 
    X=X[X['flag']=='SW']
    
    # Then using The names from X and date1,date2 we can get the datasource Y as timeseries of thoese indexes now
    Y=DataSource('bar1d_index_CN_STOCK_A').read(start_date=date1,end_date=date2,instruments=list(X['instrument']))
    Y['instrument'] = Y['instrument'].apply(lambda x:re.findall("\d+",x)[0])
    # 这里我们无法在图上用matplotlib标出中文,但是代码可以
    #Y=Y.merge(hy_df,how='inner',left_on='instrument',right_on='code')
    
    # Do the pivot to transform the chart
    Y_Industry = pd.pivot_table(Y,values='close',index='date',columns='instrument')
    return Y_Industry
SW_Index_C,hy_df = Get_SW_Industry_Index_CN('2005-01-04','2019-01-04')
SW_Index_E = Get_SW_Industry_Index_EN('2005-01-04','2019-01-04')
SW_Index_C.head()
Out[85]:
name 交通运输 休闲服务 传媒/信息服务 公用事业 农林牧渔 化工 医药生物 商业贸易 国防军工 家用电器 ... 纺织服装 综合 计算机 轻工制造 通信 采掘 钢铁 银行 非银金融 食品饮料
date
2005-01-04 1176.119995 766.140015 248.720001 920.809998 485.750000 852.640015 760.869995 752.590027 161.869995 470.890015 ... 724.750000 553.719971 576.090027 676.570007 738.750000 1436.189941 1366.790039 567.130005 285.700012 837.820007
2005-01-05 1187.599976 781.289978 260.109985 929.150024 493.750000 857.909973 773.169983 762.070007 164.770004 476.100006 ... 738.549988 565.539978 589.729980 685.799988 747.330017 1442.589966 1377.569946 562.239990 288.750000 847.619995
2005-01-06 1184.819946 777.229980 259.739990 917.380005 489.339996 844.830017 767.460022 754.380005 162.919998 477.920013 ... 731.890015 562.510010 585.289978 683.909973 733.979980 1412.219971 1359.500000 556.309998 286.739990 840.159973
2005-01-07 1183.479980 789.619995 259.899994 915.989990 492.470001 846.400024 767.900024 758.030029 164.119995 479.420013 ... 734.299988 565.809998 589.309998 685.849976 734.799988 1363.310059 1354.180054 556.469971 288.000000 844.960022
2005-01-10 1191.459961 799.880005 263.890015 924.729980 497.399994 848.890015 775.840027 765.869995 165.529999 482.089996 ... 742.520020 571.390015 596.429993 690.580017 738.859985 1375.670044 1366.199951 571.729980 292.429993 856.440002

5 rows × 28 columns

3)对行业数据进行画图比较分析

In [88]:
# Plot the Sw industry indexes and the correlation matrix
# 画图

mpl.pyplot.style.use('ggplot')
mpl.rcParams['font.family']='serif'
mpl.rcParams['axes.unicode_minus']=False # 处理负号

prices=SW_Index_C
pct_daily = prices.pct_change()

print('画出动态图')
T.plot(prices/prices.iloc[0],title='行业指数收盘价', chart_type='line',options={'chart':{'height':800}})
prices=SW_Index_E
pct_daily = prices.pct_change()
# Plot the correlation matrix,  No use at all actually
fig = plt.figure(figsize= (20,15))
ax = fig.add_subplot(111)
ax = sns.heatmap(pct_daily.corr(),annot=True,annot_kws={'size':9,'weight':'bold'})
画出动态图

4) 交易日期与频次的选择

本部分提供了很多灵活变化的可能,我们可以修改交易频次,甚至可以在其中加上择时的因素将策略结合起来使用

In [89]:
# 我们做行业轮动计划每个月重新配置一下,用过往90天的历史数据
# Get the trade days

Frequency = 30

def Get_Trade_day_with_frequency(frequency):
    Trade_day = D.trading_days(start_date='2005-01-05',end_date='2019-01-05')
    Trade_day_F = Trade_day[0:-1:frequency]
    return Trade_day_F

tradedays = Get_Trade_day_with_frequency(Frequency)
tradedays['date'] = tradedays['date'].apply(lambda x:pd.to_datetime(x).strftime('%Y-%m-%d'))
tradedays = tradedays['date']

# 需要知道的是我们的数据并不足以支撑我们向前回溯2005年前的120天,所以,我们需要再往后取几个
tradedays = tradedays[3:]
tradedays.head()
Out[89]:
3545    2005-05-27
3575    2005-07-08
3605    2005-08-19
3635    2005-09-30
3665    2005-11-18
Name: date, dtype: object

5)组合优化实现

In [66]:
#计算调仓时权重配置并保存为字典
# Calculate the weights then save them as weights_dict

#记录加入权重后的组合收益时间序列
# Record the return timeseries

# 这些很重要,都是主要的数据存储部分

t0 = dt.datetime.now()  # Get the orginal time for the codes 存储了程序运行开始时间,用于后面输出整体的运行时间
weights_dict = {} # 存储权重信息,Store the weights information, 权重的变化是策略的核心
returns_df = pd.DataFrame()  # 存储组合收益率信息,store the portfolio information
num = 1  # num here is just a process control  num在这里只是一个控制变量
ret_daily = pct_daily+1 #(We need the 1+r%, not r%)
weights_name = ['mean_weights','mean_vol','min_var','risk_parity']  # 四种不同的思路


# 因为我们是在固定时点交易,因此我们仅仅存储那些天的头寸,  We only need to make our decisions at certain time.
for date_temp1,date_temp2 in zip(tradedays[:-1],tradedays[1:]):
    
    t1=dt.datetime.now()
    
    #获取指定日期前的交易日日期
    def get_before_tradeday(date,n):
        
        today = date
        
        # We transfer the type from str to datetime then back becasue we need to find the historical period
        today_datetime = datetime.datetime.strptime(today,"%Y-%m-%d")
        past_datetime = today_datetime - datetime.timedelta(days=n)
        past = past_datetime.strftime("%Y-%m-%d")
        
        return past
    
    date_before = get_before_tradeday(date_temp1,250)
    
    #最小方差
    def fun1(x):
        
        # Here risk is the total risk of a portfolio
        # 这里的风险是一个对投资组合风险的总衡量
        risk =  np.dot(x.T,np.dot(cov_mat,x))
        return risk
    
    
    #风险平价
    def fun3(x):
        tmp = (omega * np.matrix(x).T).A1
        risk = x * tmp/ np.sqrt(np.matrix(x) * omega * np.matrix(x).T).A1[0]
        delta_risk = [sum((i - risk)**2) for i in risk]
        return sum(delta_risk)
    
    #计算协方差矩阵
    pct_temp = pct_daily.loc[date_before:date_temp1,:]
    cov_mat = pct_temp.cov()
    omega = np.matrix(cov_mat.values)
    
    #sicpy优化方法参数设置
    x0 = np.ones(omega.shape[0]) / omega.shape[0]  
    bnds = tuple((0,None) for x in x0)
    cons = ({'type':'eq', 'fun': lambda x: sum(x) - 1})   # 投资组合权重加总为1
    options={'disp':False, 'maxiter':1000, 'ftol':1e-20}
    
    #平均分配权重
    weights1 = np.array([1.0/pct_temp.shape[1]]*pct_temp.shape[1])
    #等波动率
    wts = 1/pct_temp.std()
    weights2 = wts/wts.sum()
    #模型求解组合方差最小时权重
    weights3 = minimize(fun1,x0,bounds=bnds,constraints=cons,method='SLSQP',options=options)['x']
    #风险平价
    weights4 = minimize(fun3,x0,bounds=bnds,constraints=cons,method='SLSQP',options=options)['x']
    weights_list = [weights1,weights2,weights3,weights4]
    #统计收益
    if num == 0:
        returns1_df = pd.DataFrame()
        for i in range(len(weights_list)):
            weights_temp =[j if j > 0.0001 else 0.0 for j in weights_list[i]]
            returns1_df[weights_name[i]] = (ret_daily.loc[date_temp1:date_temp2,:]*weights_temp).cumsum(axis=1).iloc[:,-1]
        returns_df = pd.concat([returns_df,returns1_df],axis=0)
    else:
        #按权重的方式计算
        for i in range(len(weights_list)):
            weights_temp =[j if j > 0.0001 else 0.0 for j in weights_list[i]]
            returns_df[weights_name[i]] = (ret_daily.loc[date_temp1:date_temp2,:]*weights_temp).cumsum(axis=1).iloc[:,-1]
        num = 0
    weights_dict[date_temp1] = weights_list
    t2=dt.datetime.now()
    #print('计算到%s,已耗时%s秒'%(date_temp2,(t2-t1).seconds))
#print('计算完毕,总耗时%s秒'%(t2-t0).seconds)

6) 对比分析,

本部分是上证综指

In [90]:
P = DataSource('bar1d_index_CN_STOCK_A').read(instruments = ['000001.HIX'],start_date='2005-05-27',end_date='2018-12-14',fields=['date','close'])
P=P.set_index('date')
P['close'] = P['close']/P['close'].iloc[0]
T.plot(P,title='上证综指',options={'chart':{'height':800}})

本部分是组合优化策略净值曲线

In [91]:
T.plot(returns_df.cumprod(axis=0),options={'chart':{'height':800}},title='策略净值曲线')

7) 权重调整分析

In [92]:
# 初始化四个组合优化策略的权重表
mean_wts_df = pd.DataFrame()
meanvol_wts_df = pd.DataFrame()
minvar_wts_df = pd.DataFrame()
rp_wts_df = pd.DataFrame()


# 将数据存储在这些表里
for date in weights_dict.keys():
    #print(weights_dict[date][1])
    mean_wts_df[date]  = weights_dict[date][0]
    meanvol_wts_df[date]  = weights_dict[date][1]
    minvar_wts_df[date]  = weights_dict[date][2]
    rp_wts_df[date]  = weights_dict[date][3]

# 将数据打上中文标签
mean_wts_df = mean_wts_df.T
mean_wts_df.columns = list(hy_df['name'].values)
meanvol_wts_df = meanvol_wts_df.T
meanvol_wts_df.columns = list(hy_df['name'].values)
minvar_wts_df = minvar_wts_df.T
minvar_wts_df.columns = list(hy_df['name'].values)
rp_wts_df = rp_wts_df.T
rp_wts_df.columns = list(hy_df['name'].values)

# 接下来画出频率累计图
In [93]:
#权重设置示意图
# 最小方差
T.plot(minvar_wts_df,chart_type='area',options={'plotOptions': {'area': {'stacking': 'percent','lineColor': '#ffffff','lineWidth': 1,'marker': {'lineWidth': 1,'lineColor': '#ffffff'}}},'chart':{'height':600}},title='最小方差策略频率累积图')
In [94]:
#权重设置示意图
#风险平价
T.plot(rp_wts_df,chart_type='area',options={'plotOptions': {'area': {'stacking': 'percent','lineColor': '#ffffff','lineWidth': 1,'marker': {'lineWidth': 1,'lineColor': '#ffffff'}}},'chart':{'height':600}},title='风险评价策略频率累积图')
In [95]:
#权重设置示意图
#等波动率
T.plot(meanvol_wts_df,chart_type='area',options={'plotOptions': {'area': {'stacking': 'percent','lineColor': '#ffffff','lineWidth': 1,'marker': {'lineWidth': 1,'lineColor': '#ffffff'}}},'chart':{'height':600}},title='等波动率策略频率累积图')
In [97]:
#计算风险指标函数
# 按照一年的交易日来计算时间为250天左右
def get_risk_index(pct_se):
    return_se = pct_se.cumprod()-1  #累乘减一
    total_returns = return_se[-1]   #取最后一天的值就是策略的累计收益
    total_an_returns = ((1+total_returns)**(250/len(return_se))-1)   #加权了250天后就可以得到年化收益
    vol = pct_se.std()*np.sqrt(250)  #将日波动率加权了250天后得到年化波动率
    sharpe = (total_an_returns-0.04)/(np.std(pct_se)*250**0.5)  #假设无风险利率是0.04,那么夏普比率如此计算
    ret = return_se.dropna()  # 将可能存在的的缺失值丢弃
    ret = ret+1
    
    # 计算最大回撤的方法
    maxdown_list = []
    for i in range(1,len(ret)):
        low  = min(ret[i:])
        high = max(ret[0:i]) 
        if high>low:
            #print(high,low)
            maxdown_list.append((high-low)/high)
            #print((high-low)/high)
        else:
            maxdown_list.append(0)
    max_drawdown = max(maxdown_list)
    '''
    print('策略收益:%s'%round(total_returns*100,2)+'%')
    print('策略年化收益:%s'%round(total_an_returns*100,2)+'%')
    print('夏普比率:%s'%round(sharpe,2))
    print('最大回撤:%s'%round(max_drawdown*100,2)+'%')
    print('年化波动率:%s'%round(vol,2))
    '''
    return total_returns,total_an_returns,sharpe,max_drawdown,vol

results=pd.DataFrame(index = ['策略收益','策略年化收益','夏普比率','最大回撤','年化波动率'])
for colu in returns_df.columns:
    results[colu] = get_risk_index(returns_df[colu])
results
Out[97]:
mean_weights mean_vol min_var risk_parity
策略收益 5.218835 5.408414 6.234183 5.636687
策略年化收益 0.143378 0.145898 0.156126 0.148842
夏普比率 0.358232 0.370081 0.471761 0.383859
最大回撤 0.715027 0.713349 0.709561 0.713772
年化波动率 0.288620 0.286190 0.246190 0.283588

3)

本文的不足

结果分析,本文有很多不足的地方,比如没有将择时策略和行业轮动结合,没有探索最优的轮动频次;
本文中的组合优化方法思路很简单,事实上我们可以进行更加精细的探讨,比如在思考波动率时,需要考虑的历史有效数据时长的选取。

本文的亮点,行业轮动策略在这十年间的累计收益率远高于指数,这说明了分散风险的重要性。

风险平价和等波动率在权重配置频率累计图上表现很一致
长期来看,风险评价策略一直拥有较高的净值,这种策略有其内在的优势。
因为A股的行业指数受大盘影响过大,系统性风险无法避免,因此这种行业轮动策略也无法分散系统性风险。