Py学习  »  Python

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

中金固定收益研究 • 3 年前 • 522 次点击  


策略观点---    

从新年开始,固收+投资者的情绪起伏恐怕不小。先是作为主力轮转工具的转债在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/108518
 
522 次点击