社区所有版块导航
Python
python开源   Django   Python   DjangoApp   pycharm  
DATA
docker   Elasticsearch  
aigc
aigc   chatgpt  
WEB开发
linux   MongoDB   Redis   DATABASE   NGINX   其他Web框架   web工具   zookeeper   tornado   NoSql   Bootstrap   js   peewee   Git   bottle   IE   MQ   Jquery  
机器学习
机器学习算法  
Python88.com
反馈   公告   社区推广  
产品
短视频  
印度
印度  
Py学习  »  Python

中金固收 · 固收+:“固收+”们也换风格了吗?——兼论固收+基金风格的分步过滤测算及Python实现

中金点睛 • 3 年前 • 448 次点击  


策略观点---    

从新年开始,固收+投资者的情绪起伏恐怕不小。先是作为主力轮转工具的转债在1月成了“固收-”——当然,这里还有很大的结构分化,当时的小票基本就是“禁触区”,好在高位品种还能独善其身。但问题又来了:春节后,高位品种(类似股票中的白马)开始瓦解,市场似乎又进入小票的节奏里。令投资者略感沮丧的点也在这里:对于“固收+”基金们而言,其上游客户不一定有太高的风险容忍度,而2021年到现在,想保一月的净值必然在二月付出代价,而反过来的代价也许更高,比如在一月就要面对大量赎回。以我们的十大转债组合为例,二月同样表现不佳。随之而来的,是不免迷茫。我们的一个小经验:如果心态不能很快整理好,就看一看“其他人”,至少知道市场的一个平均组合是怎样做的,在状态调整好或者市场环境变好之前,先守住中庸,再伺机而动。但前提又是:只要我们需要一个稳定的工具,去观察“其他人”。


在此我们先介绍一个结合稳定性、拟合优度和直观性来讲,比较可接受的一个模型。我们在《转债基金的风格分解与Python》中介绍过一个简单模型,用以将转债基金的收益分摊到不同的指数上,从而得到基金的择券alpha和风格alpha。不过此时此刻,我们想要的并不是观察真实仓位也不是做收益的归因与评价,而是需要一个稳定性更好、也更加直观反映基金风格分布和边际变化的工具,因而我们不能照搬,或者至少要做一些改进。这方面的问题,一个简单的思路是做一个多元回归,例如:

其中Ri为基金净值回报,Rm1为第1个指数(例如转债指数或者沪深300),beta1对其对应的beta系数,以此类推。但相比于统计领域的其他问题,这样做几乎必然要面对一个比较大的多重共线性问题,这也是基金仓位测算中普遍存在的问题。对于固收+基金可能尤其如此,多数固收+基金都有仓位不低的转债,而无论是转债指数还是细分的策略指数,都会与股票指数有较强相关性。


针对此,我们用分步过滤法解决。学界存在着很多方法,例如岭回归、Lasso回归、PCA、逐步回归等,但诸如岭回归存在一个关键变量(λ),实践中多用交叉验证法确定——这会引入新的问题。而PCA会使回归参数失去直观意义,逐步回归则难以形成稳定的时间序列。我们的思路是,先用最简单的一元线性回归,找到“最适合”待研究基金的指数作为“主风格指数”,然后再对其他指数提纯,并用提纯后的数据再去拟合“主风格指数”无法解释的部分。


具体步骤和程序实现方法如下:

1、准备指数:当然,首先是准备工作,我们需要基金净值数据,并确定一个指数篮,通常不需要多,3~5个有代表性的即可。后面分别以Rm1、Rm2等表示,假设共n个。考虑到差异化和基金实际可能用到的策略,我们选取:转债大盘指数(简称转债指数)、双低策略指数、基金重仓指数和国证2000指数(代表小票)。其中基金重仓指数、国证2000指数直接提取便可,转债大盘指数和双低策略指数需要我们临时编制,所幸比较简单。实现逻辑如下,最终我们得到代表指数篮子的变量dfRet。

为什么这里不放低溢价策略?普遍来讲,溢价率很低的转债与正股区别比较小,为节省变量个数,我们希望这方面的暴露在股票指数上反映即可,因为实质如此,尤其是基金重仓指数


# 风格指数编制逻辑

import libCB as cb # 我们的常用函数和数据库,这里仅用到计算转债策略指数的cb.frameStrategy
from WindPy import w
import readSql as rs
import pandas as pd
from scipy.optimize import leastsq
from sklearn.linear_model import LinearRegression


def getRet(obj, start, end=None):
    # 整合函数,需要用到下面的三个函数,返回dfRet。obj为保存转债基础数据的class,frameStrategy请见《转债策略测试》
    if end is None:
        end = obj.DB["Amt"].index[-1]

    dfBig = cb.frameStrategy(obj, start, roundMethod=21, weightMethod='fakeEv')
    dfSD = cb.frameStrategy(obj, start,selMethod=LowPremLowPrice, roundMethod=21)
    
    dfRet = pd.DataFrame(index = dfBig.index)
    dfRet[u"转债指数"] = dfBig.NAV
    dfRet[u"双低"] = dfSD.NAV
    
    dfRet.index = pd.to_datetime(dfRet.index) # 这里是因为万得API返回的日期为dt模式,我们则是str,所以在这里先转化统一好
    dfIndex = getIndex(start, end)
    
    dfRet[u"基金重仓股"] = dfIndex['8841271.WI'] * 100 / dfIndex['8841271.WI'][0]
    dfRet[u"国证2000"] = dfIndex['399303.SZ'] * 100 / dfIndex['399303.SZ'][0]

    return dfRet

def bigCB(obj, codes, date, tempCodes, dfAssetBook):
    t = obj.DB["Oustanding"].loc[date, tempCodes] > 5000000000.0 # 大盘转债:溢价率
    return t[t].index

def LowPremLowPrice(obj, codes, date, tempCodes, dfAssetBook):
    # 简易双低策略
    intLoc = obj.DB["Amt"].index.get_loc(date)
    date = obj.DB["Amt"].index[intLoc - 1] # 平移一下日期,在t日用t-1日的数据
    
    t = (obj.DB['Close'].loc[date, tempCodes] < \
         obj.DB['Close'].loc[date, tempCodes].quantile(0.5)) # 取价格前50%
    t *= (obj.DB['ConvPrem'].loc[date, tempCodes] < \
         obj.DB['ConvPrem'].loc[date, tempCodes].quantile(0.5)) # 取溢价率前50%
    
    return t[t].index


2、整合数据:除了指数外,自然我们还需要基金的净值回报,直接提取即可。我们在此顺便将前述基础数据一起整合到一个class中(命名为fundStyle)。


# 基金净值回报与基础数据整合

class fundStyle(object):
    
    def __init__(self, codes, dfRet):
        
        self.codes = codes
        self.dfRet = dfRet
        self.navReturn = self.getNavPctChg() # 基金净值回报

    def getNavPctChg (self):
        codes = self.codes
        start = dfRet.index[1]
        end = dfRet.index[-1]
        
        if not w.isconnected(): w.start()
    
        strCodes = ','.join(codes)
        _, dfNavPct = w.wsd(strCodes,'NAV_adj_return1',start,end, usedf=True)
        
        return dfNavPct


3、用普通的带截距的模型,先逐个用Rm1~Rmn回归Ri,得到n个一元线性模型的R^2(拟合优度)。选择其中R^2最大也就是拟合效果最好的那一个指数,作为“主风格指数”,该指数也被记作Rmk。这里的回归我们不加任何限制,于是sklearn的LinearRegression即可解决问题,如下:


# 初步回归

def reg5(self, code, n_window, n_calc):
    # n_window为单次计算所用的时间窗口, n_calc为计算间隔(当然可以为1), 最终结果以df形式存储于ret

    xPct = self.dfRet.pct_change().dropna() # 指数回报,备用
    srsChg = self.navReturn[code] # 该基金的净值回报

    idx = list(srsChg.index[n_window::n_calc]) # 日期轴
    ret = pd.DataFrame(index=idx,
                       columns=['alpha',u'转债指数',u'双低',u'国证2000',u'基金重仓股','level1'])
    for date in ret.index:

        i = srsChg.index.get_loc(date)
        y, x = srsChg[i-n_window+1:i+1], xPct.iloc[i-n_window+1:i+1] * 100.0

        # 初次回归
        _dfScore = pd.DataFrame(index=x.columns, columns=["score","beta"]) # 保存每一个单因子回归的beta和拟合优度(score)
        Line = LinearRegression(fit_intercept=True)

        srsPara = pd.Series(index=ret.columns) # 用来保存中转结果

        for col in x.columns:

            Line.fit(x[col].values.reshape(-1,1), y.values.reshape(-1,1))
            _dfScore.loc[col, "score"] = Line.score(x[col].values.reshape(-1,1), y.values.reshape(-1,1))
            _dfScore.loc[col, "beta"] = Line.coef_[0][0]

        level1 = pd.to_numeric(_dfScore["score"]).argmax() # 直接argmax会有数据类型报错,这里需要中转一下
        srsPara['level1'] = level1

        beta = _dfScore.loc[level1, "beta"]
        alpha = Line.intercept_[0]


4、计算用Rmk一元回归得到的残差值,即Ri’= Ri – betak * Rmk - alpha。下一步我们要去用其他指数拟合Ri’。但在此之前,其他指数之间的多重共线依然存在。所以,这里我们先用Rmk对其他指数“提纯”。我们先用Rmk去做单因子拟合其他指数,得到诸如Rm1= c1 * Rmk + e的线性关系,然后得到Rm1’= Rm1 – c1*Rmk,以此类推。下面的程序仍在“reg5”函数内部。


# 非主要指数的提纯

y -= (beta * x[level1] + alpha) # 首先要对y也做提纯

LineX = LinearRegression(fit_intercept=False)
dictCoef = {} # 保存c1,c2,c3,c4

for k in x.columns:
    if k != level1:
        LineX.fit(x[level1].values.reshape(-1,1), x[k].values.reshape(-1,1))
        dictCoef[k] = LineX.coef_[0][0]
        x[k] -= x[level1] * LineX.coef_[0][0]

x.drop(columns=level1, inplace=True)


5、最后,我们用一个带约束的多元回归,去得到上述x与y之间的关系。由于sklearn不支持简易的带约束回归,我们需要用scipy中的leastsq并手动做约束条件。这里我们不多加限定,只要参数回归出来不要小于0即可,如下文的constrainRegV3即实现该功能。


# 带约束的多元回归

def constrainErrV3(p, x, y, beta):
    err = list(p[0] + (p[1:]* x).sum(axis=1) - y)
    pen2 = max([-min(p[1:]), 0])
    
    err.append(pen2*100000000)
    
    return err

def constrainRegV3(x, y, beta):
    
    return leastsq(constrainErrV3, [0,0.2,0.2,0.2], args=(x,y, beta), maxfev=1000)[0]


6、使用方式:由于封装完整,使用时只需要初始化后调用reg5即可,不赘述。


# 完整回归函数reg5

def reg5(self, code, n_window, n_calc):
    # n_window为单次计算所用的时间窗口, n_calc为计算间隔(当然可以为1), 最终结果以df形式存储于ret

    xPct = self.dfRet.pct_change().dropna() # 指数回报,备用
    srsChg = self.navReturn[code] # 该基金的净值回报

    idx = list(srsChg.index[n_window::n_calc]) # 日期轴
    ret = pd.DataFrame(index=idx,
                       columns=['alpha',u'转债指数',u'双低',u'国证2000',u'基金重仓股','level1'])
    for date in ret.index:

        i = srsChg.index.get_loc(date)
        y, x = srsChg[i-n_window+1 :i+1], xPct.iloc[i-n_window+1:i+1] * 100.0

        # 初次回归
        _dfScore = pd.DataFrame(index=x.columns, columns=["score","beta"]) # 保存每一个单因子回归的beta和拟合优度(score)
        Line = LinearRegression(fit_intercept=True)

        srsPara = pd.Series(index=ret.columns) # 用来保存中转结果

        for col in x.columns:

            Line.fit(x[col].values.reshape(-1,1), y.values.reshape(-1,1))
            _dfScore.loc[col, "score"] = Line.score(x[col].values.reshape(-1,1), y.values.reshape(-1,1))
            _dfScore.loc[col, "beta"] = Line.coef_[0][0]

        level1 = pd.to_numeric(_dfScore["score"]).argmax() # 直接argmax会有数据类型报错,这里需要中转一下
        srsPara['level1'] = level1

        beta = _dfScore.loc[level1, "beta"]
        alpha = Line.intercept_[0]

        # 二次回归看残余

        y -= (beta * x[level1] + alpha) # 首先要对y也做提纯

        LineX = LinearRegression(fit_intercept=False)
        dictCoef = {} # 保存c1,c2,c3,c4

        for k in x.columns:
            if k != level1:
                LineX.fit(x[level1].values.reshape(-1,1), x[k].values.reshape(-1,1))
                dictCoef[k] = LineX.coef_[0 ][0]
                x[k] -= x[level1] * LineX.coef_[0][0]

        x.drop(columns=level1, inplace=True)

        coef = list(constrainRegV3(x, y, beta))

        for i, v in enumerate(x.columns):
            srsPara[v] = coef[i+1]
            beta -= srsPara[v] * dictCoef[v]

        alpha += coef[0]

        srsPara[level1] = beta
        srsPara['alpha'] = alpha

        ret.loc[date] = srsPara

    return ret



针对当下,固收+们有什么动作?以下结论:

1、首先看转债基金,其整体仓位仍在中位水平。但由于转债基金本身调仓余地不大,我们更关注构成。与直觉不同的是,我们未能看到这些基金加码双低小转债或者国证2000小股票。这些产品在股票仓位上由此前高点有所收敛,国证2000小股票未有明显变化(因为本来的数字也已经很小),提高的则是大盘转债的占比。


2、积极型固收+基金(非转债基金,但转债+股票仓位高于40%)在近期几乎没有结构上的调整。仅仅出现大盘转债些许提升的迹象。


3、稳健性固收+基金(不属于前两者的二级债基与偏债混基)的仓位略有提高,且高在了“基金重仓股”上。


4、当然上述只是特定类型的平均情况,不同经理、产品自然有个性化差异。但总体来看,头部大固收+产品倾向于保持原有的风格,只是仓位的加加减减。例如以下案例,产品名称暂以字母代替。



报告原文请见2021年03月05日中金固定收益研究发表的研究报告

Python社区是高质量的Python/Django开发社区
本文地址:http://www.python88.com/topic/108622
 
448 次点击