目录
投资组合的绩效表现受到许多因素影响,我们不容易清楚区分绩效表现是源于大盘上涨、交易员的选股能力,还是资产配置或产业配置得宜? 故我们可以透过 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的绩效归因。因此本文不构成要约、招揽或邀请、诱使、任何不论种类或形式之申述或订立任何建议及推荐,读者务请运用个人独立思考能力,自行作出投资决定,如因相关建议招致损失,概与作者无涉。