被动型FOF产品规划方案(附代码)

大憨熊 提交于 2020-01-10 07:55:08

被动型FOF产品规划方案(附代码)

前言

按照基本的投资策略,基金可以被分类为被动型基金和主动性基金。被动型基金主要投资于指数产品,主动性基金则会采取更加灵活多变的策略来选择标的资产池。但是,两者都需要对标的资产的权重进行优化。

本文将介绍了笔者设计的一个用于被动型FOF(“ Fund of Fund” ,中文 “基金中的基金” )投资的产品方案设计思路,涉及到的调仓权重优化方法包括马科维兹优化、风险平价优化、Black-Litterman模型。

在连接至Wind 数据库的情况下,运行程序并输入您所需要的投资组合的起止日期、标的资产的代码、回测数据所使用的的时间窗口、调仓频率,您就可以得到投资组合在三种优化方法下每日的持仓权重。 (代码和数据于本文末下载) ## 资产配置模型 ### 马科维兹模型 马科维茨的均值一方差组合模型(Markovitz Mean-Variance Model)用收益率序列均值来代表投资组合的收益,用收益率序列方差来代表投资组合的风险,以此来平衡投资组合的风险和收益。 投资组合预期收益的计算公式为:

E(Rp)=iωiE(Ri) E(R_p)=\sum_{i}^{}\omega_iE(R_i)

投资组合预期方差的计算公式为:
σp2=iωi2σi2+iijωiωjσiσjρij \sigma_{p}^{2}=\sum_{i}^{}\omega_{i}^{2}\sigma_{i}^{2}+\sum_{i}^{}\sum_{i\neq j}^{}\omega_i\omega_j\sigma_i\sigma_j\rho_{ij}
用矩阵的方法来表示有效前沿:
ωTω q RTω \omega ^{T}\sum \omega-~q~*R^{T}\omega
用代码实现,如下:

#定义马科维茨优化的目标函数
def funs(weight,sigma):
    weight = np.array([weight]).T
    result = np.dot(np.dot(weight.T,np.mat(sigma)),weight)[0,0]
    return(result)

风险平价模型

风险平价模型即构造等风险投资组合,这里采用的是标的资产的波动率作为风险指标。

我们定义投资组合的波动率为:
σ(ω)=ωΣω \sigma(\omega)=\sqrt{\omega^{'}\Sigma\omega^{'} }
我们的目标是令标的资产i对投资组合波动率的风险贡献度为投资组合总风险的的1/N:
σ(ωi)=ωiσ(ω)ωi=ωi(Σω)iωΣω=σ(ω)N \sigma(\omega_i)=\omega_i\ast \frac{\partial \sigma(\omega)}{\partial \omega_i} \\=\frac{\omega_{i}(\Sigma\omega)_{i}}{\sqrt{\omega^{'}\Sigma\omega^{'}} } =\frac{\sigma(\omega)}{N}
我们需要解下面这个最小化问题:
argminωi=1N[ωiω(σ)2(Σω)iN]2 argmin\omega \sum_{i=1}^{N}[\omega_i-\frac{\omega(\sigma)^{2}}{(\Sigma\omega)_iN}]^{2}
用代码求解并实现,如下:

#定义风险平价优化的目标函数
def funsRP(weight,sigma):
    weight = np.array([weight]).T
    X = np.multiply(weight,np.dot(sigma.values,weight))
    result = np.square(np.dot(X,np.ones([1,X.shape[0]])) - X.T).sum()
    return(result)

Black Littterman模型

Black Litterman模型对马科维兹模型进行改进,在收益率序列分布上,用贝叶斯统计理论将投资者对大类资产的观点与市场均衡回报相结合。

由马科维兹模型,我们知道
β=Cov(r,wTr)Var(wTr)=Cov(r,r)wVar(wTr)=1σ2Σw \beta=\frac{Cov(r,w^Tr)}{Var(w^Tr)}=\frac{Cov(r,r)w}{Var(w^Tr)}=\frac{1}{\sigma^2}\Sigma w
市场超额收益为
Excess Return=(wTr)β=(wTr)β=wTrσ2Σw Excess~Return=(w^Tr)\beta \\=(w^Tr)\beta=\frac{w^Tr}{\sigma^2}\Sigma w
现在我们要构建一个投资组合ww满足在既定风险下的期望收益最大化。引入对个股期望收益的主观判断如下:
PE(r)=q+vvN(0,Ω)PE(r)N(q,Ω) Excess eturnE(r)N(E(r),τΣ) PE(r)=q+v \\ v\sim N(0,\Omega) \\ PE(r)\sim N(q,\Omega) \\假设~Excess~eturn|E(r)\sim N(E(r),\tau\Sigma)
我们需要解下面这个最小化问题:
minE(r) (E(r)Excess Return)T(τΣ)1(E(r)Excess Return)s.t. PE(r)=q \min_{E(r)}\ (E(r)-Excess~Return)^T(\tau\Sigma)^{-1}(E(r)-Excess~Return) \\ s.t.\ PE(r)=q
用代码求解并实现,如下:

#定义blacklitterman函数
def blacklitterman(returns, Tau, P, Q):
    mu = returns.mean()
    sigma = returns.cov()
    pi1 = mu
    ts = Tau * sigma
    Omega = np.dot(np.dot(P, ts), P.T) * np.eye(Q.shape[0])
    middle = linalg.inv(np.dot(np.dot(P, ts), P.T) + Omega)
    er = np.expand_dims(pi1, axis=0).T + np.dot(np.dot(np.dot(ts, P.T), middle),
                                                (Q - np.expand_dims(np.dot(P, pi1.T), axis=1)))
    newList = []
    for item in er:
        if type(item) == list:
            tmp = ''
            for i in item:
                tmp += float(i) + ' '
                newList.append(tmp)
        else:
            newList.append(item)
    New = []
    for j in newList:
        k = float(j)
        New.append(k)
    posteriorSigma = sigma + ts - np.dot(ts.dot(P.T).dot(middle).dot(P), ts)
    return [New, posteriorSigma]

策略实现

加载包

import pandas as pd
import numpy as np
from scipy import stats
import math
import matplotlib.pyplot as plt
#import ffn  
#计算最大回撤时使用的包,也可以使用自行撰写的函数运行,使用该包可以通过pip install进行安装
from tqdm import tqdm
#查看程序运行进度的包,如果没有安装请注释掉该行,之后程序仍然可以正常运行
import statsmodels.api as sm
from scipy import linalg
import scipy.optimize as sco
import datetime
from datetime import date
import seaborn as sns
from scipy.optimize import minimize
from sklearn.covariance import  ledoit_wolf
from matplotlib.font_manager import FontProperties
font = FontProperties(fname=r'c:\windows\fonts\simkai.ttf',size = 15)
# 如果有wind数据库,请加载wind数据库接口,接口修复在“我的-自动修复插件-修复Python接口”,修复时需要关闭Python
from WindPy import *
w.start()

投资组合评价

#定义策略期间表现的函数,策略需要包括一列列标为nav,含义为投资组合的净值
def performance(strategy):
    def MaxDrawdown(return_list):
        RET_ACC = []
        sum = 1
        for i in range(len(return_list)):
            sum = sum * (return_list[i] + 1)
            RET_ACC.append(sum)
        index_j = np.argmax(np.array((np.maximum.accumulate(RET_ACC) - RET_ACC) / 								np.maximum.accumulate(RET_ACC)))
        index_i = np.argmax(RET_ACC[:index_j])
        MDD = (RET_ACC[index_i] - RET_ACC[index_j]) / RET_ACC[index_i]
        return sum, MDD, RET_ACC
    """def MaxDrawdown2(return_list):
        value = (1 + return_list).cumprod()
        MDD = ffn.calc_max_drawdown(value)
        return -MDD"""
    # 如果要考虑rf的话启用下面这个sharp函数
    def sharp(return_list, std):
        returnew = pd.DataFrame(return_list, columns=['R'])
        m = pd.concat([returnew.R], axis=1)
        ret_adj = np.array(m)
        sharpratio = np.average(ret_adj) * 12 / std
        return sharpratio

    def Reward_to_VaR(strategy=strategy, alpha=0.99):  # 99%置信水平,历史收益率法计算VaR
        RET = strategy.nav.pct_change(1).fillna(0)
        sorted_Returns = np.sort(RET)
        index = int(alpha * len(sorted_Returns))
        var = abs(sorted_Returns[index])
        RtoVaR = np.average(RET) / var
        return -RtoVaR

    def Reward_to_CVaR(strategy=strategy, alpha=0.99):   # 99%置信水平,历史收益率法计算CVaR
        RET = strategy.nav.pct_change(1).fillna(0)
        sorted_Returns = np.sort(RET)
        index = int(alpha * len(sorted_Returns))
        sum_var = sorted_Returns[0]
        for i in range(1, index):
            sum_var += sorted_Returns[i]
            CVaR = abs(sum_var / index)
        RtoCVaR = np.average(RET) / CVaR
        return -RtoCVaR

    ts = strategy.nav.pct_change(1).fillna(0)
    RET = (strategy.nav[strategy.shape[0] - 1] /strategy.nav[0])** 
    									(252 / strategy.shape[0]) - 1
    T = stats.ttest_1samp(ts, 0)[0]
    STD = np.std(ts) * np.sqrt(252)
    MDD = MaxDrawdown(ts)[1]
    ACC = MaxDrawdown(ts)[0]
    SHARP = (RET - 0.03) / STD  # 默认年化无风险收益率为3%
    R2VaR = Reward_to_VaR(strategy)
    R2CVaR = Reward_to_CVaR(strategy)
    print('annual-return', round(RET, 4))
    print('t-statistic', round(T, 4))
    print('volitility', round(STD, 4))
    print('MaxDrawdown', round(MDD, 4))
    print('Accumulated return', round(ACC, 4))
    print('sharp-ratio', round(SHARP, 4))
    print('Reward_to_VaR', round(R2VaR, 4))
    print('Reward_to_CVaR', round(R2CVaR, 4))
    return RET, T, STD, MDD, ACC, SHARP, R2VaR, R2CVaR

输入相关参数

start = input("请输入开始日期,格式为xxxx/xx/xx: ")
end = input("请输入结束日期,格式为xxxx/xx/xx: ")
startdate = datetime.strptime(start,'%Y/%m/%d')
enddate = datetime.strptime(end,'%Y/%m/%d')
codes=input("请输入要研究的指数代码,并以逗号隔开:")
#000985.CSI,H11001.CSI,CCFI.WI,AU9999.SGE,SPX.GI,HSI.HI
datas = w.wsd(codes, "close", startdate, enddate, "")
datas = pd.DataFrame(np.array(datas.Data).T,columns = datas.Codes,index = datas.Times)
rolling = input("请输入滚动回测区间长度(天):")
rollingtimeinput = float(rolling)
#本产品方案的回测区间为输入数据第一天至历史上的当前时间,
#滚动回测的数据输入仅用于blacklitterman策略的开始生效日
#以及输出结果的开始日期与输入数据第一天之间的间隔长度
perio = input("请输入调仓频率(月/次):")
periodinput = float(perio)

策略的主函数

def ProductPlan(datas, expected_return, period=1, rollingtime=126, method='MAC', tau=0.01,wmin=0,wmax=1):
    #datas为指数或者标的资产的日度收盘价数据,格式为dataframe,行标为日期(格式为日期),列表为资产的代码或者名称
    #expected_return格式和形式同datas
    #peiod为策略频率,单位为月,缺省值为1
    #rollingtime为策略滚动回测时间,单位为天,缺省值为126
        #本产品方案的回测区间为输入数据第一天至历史上的当前时间,
        #滚动回测的数据输入仅用于blacklitterman策略的开始生效日
        #以及输出结果的开始日期与输入数据第一天之间的间隔长度
    #method为策略的方式,选项为 MAC, RP, BL,缺省值为MAC
    #tau为blacklitterman模型的输入参数,为主观观点与历史收益率的比率,缺省值为0.01
    #wmin为资产配置比例的最小值,缺省值为0
    #wmax为资产配置比例的最大值,缺省值为1
    ret = datas.pct_change(1).fillna(0) #计算标的资产的日度收益率数据,缺失值填充为0
    data_norm = datas / datas.iloc[0,] * 1000 #将指数收盘价的第一天统一调整为1000
    result = data_norm.copy() #构架过程输入的dataframe
    result['m'] = result.index
    result['m'] = result.m.apply(lambda x: x.month) #计算每一天属于一年12个月中的哪一个月,方便月度调仓的计算
    weights = pd.DataFrame(columns=datas.columns, index=datas.index).fillna(0)  #构建空的dataframe用于存放权重
    N = pd.DataFrame(columns=datas.columns, index=datas.index).fillna(0)  #构建空的dataframe用于存放资产的数量
    datas_index = np.array(datas.index) #日期序列
    noa = datas.shape[1] #number of asset 资产的数量
    position = 0 #用于数据迭代的一个初始值
    #优化问题的约束条件,可以设置不等式约束或者方程约束,eq表示等式,ineq表示不等式(小于等于)
    cons = ({'type': 'eq', 'fun': lambda x:  1 - sum(x)})#所有权重相加为1
    #优化问题的边界
    bnds = tuple((wmin, wmax) for i in range(datas.shape[1]))#所有权重的上下界

    if method == 'MAC':
        for i in tqdm(range(result.shape[0])):
            if i == 0:
                pass
            elif result.m[i] != result.m[i - 1] and result.m[i] % int(period) == 0:
                sigma = ret.iloc[position:i].cov()
                position = i              
                weight = [0 for i in range(datas.shape[1])]
                res =  minimize(funs,weight, method='SLSQP',args = (sigma,),
                bounds=bnds,constraints=cons,tol=1e-8)
                weights.iloc[i,:] =  res.x 
                #将优化出来的权重输入到weights这个空的dataframe中
                price = datas.loc[datas.index[i],:]
                V = (weights.iloc[i,:]*price).sum()
                n = V*weights.iloc[i,:].values/price.values      
                N.loc[result.index[i],:] = n  
            else:

                N.iloc[i,:] = N.iloc[i-1,:]
                w = N.iloc[i,:]*datas.loc[datas.index[i],:] 
                weights.iloc[i,:] = w/w.sum() 
    elif method =='RP':
        for i in tqdm(range(result.shape[0])):
            if i == 0:
                pass
            elif (result.m[i] != result.m[i - 1])and result.m[i] % int(period) == 0:        
                sigma = ret.iloc[position:i].cov()
                position = i              
                weight = [0 for i in range(datas.shape[1])]
                res =  minimize(funsRP,weight, method='SLSQP',args = (sigma,),
                bounds=bnds,constraints=cons,tol=1e-20)
                
                weights.iloc[i,:] =  res.x
                price = datas.loc[datas.index[i],:]
                V = (weights.iloc[i,:]*price).sum()
                n = V*weights.iloc[i,:].values/price.values      
                N.loc[result.index[i],:] = n
            else:
                N.iloc[i,:] = N.iloc[i-1,:]
                w = N.iloc[i,:]*datas.loc[datas.index[i],:]                 
                weights.iloc[i,:] = w/w.sum() 
    elif method =='BL':
        # 构造观点矩阵
        pick1 = np.array([1 for i in range(noa)]) # 所有资产的预期收益
        pick21 = [0 for i in range(noa-2)]
        pick22 = [0.5,-0.5]
        pick2 = np.array(pick22+pick21)  # 股票和债券的溢价
        P = np.array([pick1, pick2])
        for i in tqdm(range(result.shape[0])):
            # 输入每一期的目标收益率
            #targetanlrate = goalrate.iat[i, 0]
            if i == 0:
                weights.iloc[i, :] = 1 / datas.shape[1]
                price = datas.loc[datas.index[i], :]
                n = weights.iloc[i, :].values / price.values
                N.loc[result.index[i], :] = n
                del price, n
            elif (result.m[i] != result.m[i - 1]) and (i > int(rollingtime))and (result.m[i] % int(period)) == 0:
                Rett = ret[i - int(rollingtime):i]
                expected_return['sum'] = expected_return.sum(axis=1)
                expected_return['premium'] = expected_return.iloc[:,0] 
                							- expected_return1.iloc[:,1]
                Q = expected_return.iloc[i:i + 1, 
                    					(expected_return.shape[1] -2):
                                         expected_return.shape[1]].T.values

                Returns = blacklitterman(Rett, tau, P, Q)[0]
                sigma = blacklitterman(Rett, tau, P, Q)[1]          
                weight = [0 for i in range(datas.shape[1])]
                res =  minimize(funs,weight, method='SLSQP',args = (sigma,),
                bounds=bnds,constraints=cons,tol=1e-8)
                
                weights.iloc[i,:] =  res.x
                price = datas.loc[datas.index[i],:]
                V = (weights.iloc[i,:]*price).sum()
                n = V*weights.iloc[i,:].values/price.values      
                N.loc[result.index[i],:] = n  
            else:

                N.iloc[i,:] = N.iloc[i-1,:]
                w = N.iloc[i,:]*datas.loc[datas.index[i],:]                 
                weights.iloc[i,:] = w/w.sum() 
            
    else:
        pass

    result['mv'] = 0
    result['mv_adj_last_day'] = 0
    result['nav'] = 1
    for i in tqdm(range(result.shape[0])):
        result.loc[result.index[i],'mv'] = (datas.iloc[i,:]*N.iloc[i,:]).sum()
        if all(N.iloc[i,:]==0):
            pass
        elif all(N.iloc[i,:] == N.iloc[i-1,:] ):             
            result.loc[result.index[i],'mv_adj_last_day'] = 	
            						result.loc[result.index[i-1],'mv']
            result.loc[result.index[i],'nav'] = result.nav[i-1]*result.mv[i]
            									/result.mv_adj_last_day[i]
        else:
            result.loc[result.index[i],'mv_adj_last_day'] =  
            								(datas.iloc[i-1,:]*N.iloc[i,:]).sum()
            result.loc[result.index[i],'nav'] = 
            				result.nav[i-1]*result.mv[i]/result.mv_adj_last_day[i]
             
    result = result.iloc[int(rollingtime):,:]  
    weights = weights.iloc[int(rollingtime):,:]              
    result['nav'] = result.nav/result.nav[0]*1000
    return weights,result
    #最后输出两个dataframe,weight为权重,result为计算过程,开始日期为原datas往后推rollingtime个交易日

策略运行结果

数据起止时间为2009/03/01-2019/12/31,指数采用500、中证全债、万德商品指数、999黄金期货、标普500、恒生指数,滚动回测区间长度为100天,调仓频率为每月一次。

items Markovitz Risk parity Black litterman
annual-return 0.0536 0.0576 0.0514
t-statistic 2.4237 4.3853 2.4006
volatility 0.0717 0.0411 0.0695
Max Draw-down 0.1504 0.0738 0.1203
Accumulated return 1.692 1.7578 1.6579
sharp-ratio 0.3286 0.6701 0.3083
Reward_to_VaR -0.0199 -0.033 -0.0195
Reward_to_CVaR -2.7436 -1.6979 -2.8700

马科维茨模型运行的结果如下图所示:
avatar
风险平价模型运行的结果如下图所示:
avatar
Black Litterman模型运行的结果如下图所示:
avatar
三种资产配置方法的净值曲线:
avatar
Reference: Black Litterman 数学推导

项目Github链接: Negative_FOF_Product_Plan

欢迎关注二幺子的知识输出通道:
avatar

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!