新浪财经

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

中金固定收益研究

关注

策略观点---

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

defgetRet(obj, start, end=None):

# 整合函数,需要用到下面的三个函数,返回dfRet。obj为保存转债基础数据的class,frameStrategy请见《转债策略测试》

if end isNone:

        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

defbigCB(obj, codes, date, tempCodes, dfAssetBook):

    t = obj.DB["Oustanding"].loc[date, tempCodes] > 5000000000.0# 大盘转债:溢价率

return t[t].index

defLowPremLowPrice(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)。

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

classfundStyle(object):

def__init__(self, codes, dfRet):

self.codes = codes

self.dfRet = dfRet

self.navReturn = self.getNavPctChg() # 基金净值回报

defgetNavPctChg(self):

        codes = self.codes

        start = dfRet.index[1]

end = dfRet.index[-1]

ifnot 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即可解决问题,如下:

# 初步回归

defreg5(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即实现该功能。

# 带约束的多元回归

defconstrainErrV3(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

defconstrainRegV3(x, y, beta):

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

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

# 完整回归函数reg5

defreg5(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日中金固定收益研究发表的研究报告

加载中...