Brinson Model 绩效归因

Photo by Adeolu Eletu on Unsplash

本文重点概要

  • 文章难度:★★☆☆☆
  • 绩效归因(performance attribution)是指将投资组合与标竿指数间的超额报酬拆解成更细致的报酬,而被拆解的报酬可用来量化投资组合的产业配置、市场择时与选股能力对报酬与风险的影响。
  • 完整程式码附在TEJ Github

前言

投资组合的绩效表现受到许多因素影响,我们不容易清楚区分绩效表现是源于大盘上涨、交易员的选股能力,还是资产配置或产业配置得宜? 故我们可以透过 Brinson(1985)提出的绩效归因方法,帮助我们将投资组合与标竿指数间的超额报酬拆解为选择效果、交互效果与配置效果,了解绩效归因来源,作为日后投资决策的参照。

本文将以台湾50指数做为 00881 ETF的标竿指数,透过 Brinson model来分析 00881 ETF的绩效归因。

编辑环境及模组需求

本文使用 Windows OS 并以 Jupyter Notebook 作为编辑器

# 功能模组 import pandas as pd import numpy as np import plotly.graph_objects as go #TEJ API import tejapi tejapi.ApiConfig.api_key = "Your key"

资料库使用

资料处理

Step 1. 汇入 ETF成份股、台湾50指数成分股

我们从 TEJ资料库汇入2021年9月至11月每日更新的 00881 ETF与台湾50指数成份股,可以节省我们至发行 ETF之投信公司查询的大量时间。若要抓取大量 ETF成份股的资料,使用 TEJ资料库的优势更加明显。

#%% 汇入 TEJ资料 etf = tejapi.get('TWN/AEHOLD',                  coid = '00881',                  mdate= {'gte': '2021-09-01','lte':'2021-11-30'},                  opts={'columns':['mdate', 'no','pct']},                  chinese_column_name=True,paginate=True) # 标竿指数:台50指数 benchmark = tejapi.get('TWN/AIDXS',                        coid = 'TWN50',                        mdate= {'gte': '2021-09-01','lte':'2021-11-30'},                        opts={'columns':['mdate','key3','mv_pct']},                        chinese_column_name=True,paginate=True) etf = etf[~etf['标的名称'].isin(['申赎应付款','保证金','现金'])] etf['证券码'] = etf['标的名称'].str[0:4] etf['证券码'] = np.where(etf['证券码'] == 'TX 台','Y9999',etf['证券码']) etf['年'] = etf['日期'].dt.year etf['月'] = etf['日期'].dt.month etf = etf.drop_duplicates(subset=['年','月','证券码'], keep='first') benchmark['证券码'] = benchmark['成份股'].str[0:4] benchmark['年'] = benchmark['年月日'].dt.year benchmark['月'] = benchmark['年月日'].dt.month benchmark = benchmark.drop_duplicates(subset=['年','月','证券码'], keep='first') etf.head(10)
资料表(一)

Step 2. 汇入成份股的产业名称与调整后股价

我们将得到的 00881 ETF与台湾50指数成份股整理成 list资料型态,并从 TEJ资料库捞取所需的成份股调整后股价与所属产业名称。

# 获得 etf与 benchmark的代码 coid_list = etf['证券码'].unique().tolist() coid_list.append('Y9999') coid_list = coid_list + benchmark['证券码'].unique().tolist() # 抓取公司的产业名称 code = tejapi.get("TWN/EWNPRCSTD",                   coid = coid_list,                   paginate=True,                   opts={'columns':['coid', 'coid_name','tseindnm']},                   chinese_column_name=True) code.head(5)
资料表(二)

透过 TEJ资料库直接获得成份股每月持有报酬率。

# 股价 price = tejapi.get('TWN/AAPRCM1',                    coid = coid_list,                    mdate= {'gte': '2021-09-01','lte':'2021-11-30'},                    opts={'columns':['coid', 'mdate','roi']},                    chinese_column_name=True,                    paginate=True) price['年'] = price['年月'].dt.year price['月'] = price['年月'].dt.month price.head(5)
资料表(三)

Step 3. 合并产业名称

我们将产业名称合并至 ETF与指数的 Dataframe,若 00881 ETF与 台湾50指数成份股所属产业存在差集,则以其他 替换 ETF与台湾50指数成份股所属产业差集的产业名称,以确保 00881 ETF成份股皆有相对应 台湾50指数成份股的产业名称。

#%% 合并产业名称 ETF # 合并产业名称 etf = pd.merge(etf ,code , how = 'left' , on = ['证券码']) etf = pd.merge(etf ,price ,how = 'left' , left_on=['年','月','证券码'], right_on=['年','月','证券代码']) benchmark = pd.merge(benchmark ,code , how = 'left' , on = ['证券码']) benchmark = pd.merge(benchmark ,price ,how = 'left' ,                      left_on=['年','月','年月日','证券码'], right_on=['年','月','年月日','证券代码']) # 处理产业不一致问题 # 若 benchmark的产业种类没有在 etf的产业种类中找到,则 benchmark中特殊的产业改成其他 benchmark['TSE产业名'] = np.where(benchmark['TSE产业名'].isin(etf['TSE产业名'].unique().tolist()),benchmark['TSE产业名'],'其他') # 若 etf的产业种类没有在 benchmark的产业种类中找到,则 etf中特殊的产业改成其他 etf['TSE产业名'] = np.where(etf['TSE产业名'].isin(benchmark['TSE产业名'].unique().tolist()),                           etf['TSE产业名'],'其他')
资料表(四)

Step 4. 计算 00881 ETF与台湾50指数的产业月报酬率

分别计算 00881 ETF与台湾50指数中各产业的产业月报酬率与产业权重。

#%% 计算产业与标竿指数的月报酬率,权重
etf = etf.sort_values(by=['年','月','TSE产业名','证券代码']).reset_index(drop=True) # 排序年月日 etf['TSE产业名'] = np.where(etf['TSE产业名'].isna(),'其他' ,etf['TSE产业名']) etf['权重'] = etf['权重'] * 0.01 etf['产业权重'] = etf.groupby(['TSE产业名','年','月'])['权重'].transform('sum') etf['实际当月报酬率'] = etf['权重'] * etf['当月报酬率'] etf['产业当月报酬率'] = etf.groupby(['TSE产业名','年','月'])['实际当月报酬率'].transform('sum') / etf['产业权重'] etf['实际产业当月报酬率'] = etf['产业当月报酬率'] * etf['产业权重'] etf['ETF 当月报酬率'] = etf.groupby(['年','月'])['实际当月报酬率'].transform('sum') etf = etf[['年','月','TSE产业名','标的名称','权重','当月报酬率','产业权重','产业当月报酬率']] benchmark = benchmark.sort_values(by=['年','月','TSE产业名','证券代码']).reset_index(drop=True) # 排序年月日 benchmark = benchmark[['年月日','TSE产业名','成份股','证券代码','年','月','前日市值比重','当月报酬率']] benchmark['前日市值比重'] = benchmark['前日市值比重'] * 0.01 benchmark['产业权重'] = benchmark.groupby(['TSE产业名','年','月'])['前日市值比重'].transform('sum') benchmark['实际当月报酬率'] = benchmark['前日市值比重'] * benchmark['当月报酬率'] benchmark['产业当月报酬率'] = benchmark.groupby(['TSE产业名','年','月'])['实际当月报酬率'].transform('sum')      / benchmark['产业权重'] benchmark['实际产业当月报酬率'] = benchmark['产业当月报酬率'] * benchmark['产业权重'] benchmark['ETF 当月报酬率'] = benchmark.groupby(['年','月'])['实际当月报酬率'].transform('sum') benchmark.head(5)
资料表(五)

绩效归因

透过下表所示我们可以将主动报酬分拆成配置效果(Q2-Q1)、选择效果(Q3-Q1)与交互效果(Q4-Q3+Q2-Q1)。配置效果主要衡量资产类别、国家、产业偏移对绩效的影响;选择效果主要衡量每项类别下「选择不同标的证券」对绩效所造成的影响;而交互效果实务上常常并入配置效果或选择效果。

图(一)
#%% 绩效归因表 table = pd.merge(etf ,benchmark ,how = 'left' , on=['年','月','TSE产业名']) table['配置效果'] = (table['投组权重'] - table['标竿权重']) *      (table['标竿当月报酬率'] - sum(table[:7]['标竿权重'] * table[:7]['标竿当月报酬率'])) table['选择效果'] = table['标竿权重'] * (table['投组当月报酬率'] - table['标竿当月报酬率']) table['交互效果'] = (table['投组权重'] - table['标竿权重']) * (table['投组当月报酬率'] - table['标竿当月报酬率']) table['主动报酬'] = table['配置效果'] + table['选择效果'] + table['交互效果'] table.loc['合计',:] = table.sum(axis=0) table.loc['合计','投组当月报酬率'] = sum(table[:7]['投组权重'] * table[:7]['投组当月报酬率']) table.loc['合计','标竿当月报酬率'] = sum(table[:7]['标竿权重'] * table[:7]['标竿当月报酬率']) table = (table * 100).round(2) table

我们计算出11月绩效归因表,发现 00881 ETF有更精准的选股能力,其选择效果达 1.61%,而因为 00881 ETF主要是投资半导体、网通、电动车个股,00881 ETF与台湾50指数皆有近 60%的权重在半导体产业,产业重叠性高,所以配置效果仅有 0.47%。

资料表(六)

视觉化

绘制三个月各产业的主动报酬雷达图,可以分析每月各产业贡献的主动报酬,发现产业对投组主动报酬的贡献会随著时间而轮动。轮动是常态,但要注意整体投组主动报酬是否能大于0。

#%%  fig = go.Figure() for date in etf['月'].unique():     table = pd.merge(etf ,benchmark ,how = 'left' , on=['年','月','TSE产业名'])     table = table[table['月'] == date]     table = table.drop(['年','月'], axis=1)     table = table.set_index(['TSE产业名'])     table = table.sort_values(by=['投组权重'], ascending=False)     table = table.fillna(0)     table['配置效果'] = (table['投组权重'] - table['标竿权重']) *          (table['标竿当月报酬率'] - sum(table[:7]['标竿权重'] * table[:7]['标竿当月报酬率']))     table['选择效果'] = table['标竿权重'] * (table['投组当月报酬率'] - table['标竿当月报酬率'])     table['交互效果'] = (table['投组权重'] - table['标竿权重']) * (table['投组当月报酬率'] - table['标竿当月报酬率'])     table['主动报酬'] = table['配置效果'] + table['选择效果'] + table['交互效果']     table.loc['合计',:] = table.sum(axis=0)     table.loc['合计','投组当月报酬率'] = sum(table[:7]['投组权重'] * table[:7]['投组当月报酬率'])     table.loc['合计','标竿当月报酬率'] = sum(table[:7]['标竿权重'] * table[:7]['标竿当月报酬率'])     table = (table * 100).round(2)     fig.add_trace(go.Scatterpolar(r= table.loc['半导体':'其他','主动报酬'].tolist(),                                   theta= table.drop(['合计']).index,                                   fill='toself',                                   name=str(date) + '月'))
fig.show()
图(二)

结论

绩效归因可帮助我们厘清投组的选股与产业配置是否得宜,可以作为日后投资分析与投资决定时的参照。最后推荐读者使用 TEJ资料库提供的基金与指数成份股,让我们可以获得不同期间指数的成份股,方便我们配对成份股所属的产业,分析不同投资组合的绩效归因。

本文供参考之用,因为本文仅用三个月的资料分析 ETF的绩效归因,并无法代表未来 ETF的绩效归因。因此本文不构成要约、招揽或邀请、诱使、任何不论种类或形式之申述或订立任何建议及推荐,读者务请运用个人独立思考能力,自行作出投资决定,如因相关建议招致损失,概与作者无涉。

完整程式码

延伸阅读

相关连结

返回总览页