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的績效歸因。因此本文不構成要約、招攬或邀請、誘使、任何不論種類或形式之申述或訂立任何建議及推薦,讀者務請運用個人獨立思考能力,自行作出投資決定,如因相關建議招致損失,概與作者無涉。

完整程式碼

延伸閱讀

相關連結

返回總覽頁
Procesing