ESG投資組合(下)

使用TESG資料庫建構專屬的ESG投資組合

Photo by Luke Chesser on Unsplash

本文重點概要

文章難度:★★☆☆☆

閱讀建議:本文分為上下兩篇,上篇先介紹TEJ中的TESG評等在國內熱門ESG ETF的成分股中佔比,下篇將進一步運用TESG的評等,建構一檔具備成長性和永續經營的投資組合。建議讀者可以先閱讀【實戰應用】ESG投資組合(上),可以對本文有更好的理解。

前言

在上篇中我們詳細介紹了TESG的評分機制,以及它在國內熱門ETF上成分股的評等占比,那這篇要來教讀者怎麼進一步運用TESG提供的ESG評等,使用Python來建構一檔兼具永續經營和財務成長性的投資組合。本文使用的篩選標準如下:

公司該年TESG等級為B-、B、B+、A、A+

近一年常續性稅後淨利CAGR達到20%(含)以上

當季稅後ROE大於當季產業ROE中位數

當季市值至少大於10億元

篩選完後使用近三年股利殖利率*80%+近一年股利殖利率*20%計算出該股股利分數,並選擇分數最大20支個股作為成分股,同時依此分數進行權重分配。每年財報公布日(3月底、5月8月11月中)進行再平衡
*註:本文中出現指數一詞只是投組代名詞,請不用在意。

編輯環境及模組需求

本文使用Windows OS並以Jupyter作為編輯器

import tejapi as tej
import pandas as pd
import numpy as np
import datetime
import matplotlib.pyplot as plt
import ffn

tej.ApiConfig.api_key = 'Your Key'

plt.rcParams['font.family'] = 'Noto Sans TC'

import warnings
warnings.filterwarnings("ignore")

%matplotlib inline

資料庫使用

證券屬性資料庫(TWN/ANPRCSTD)

上市(櫃)調整股價(日)-除權息調整(TWN/APRCD1)

IFRS以合併為主財務(單季)-全部產業Ⅳ(TWN/AIFINQ)

資料載入

下載從2010年1月到2022年11月所有上市櫃(包含下市櫃)個股的股價,欄位有調整後收盤價、市值和股利殖利率。

#上市櫃公司代號
code = tej.get('TWN/ANPRCSTD', mdate={'lt':'2022-11-18'}, chinese_column_name=True, paginate=True)
all_code = code[(code['證券種類名稱'].isin(['普通股', '外國企業來台掛牌', 'TDR'])) & (code['上市別'].isin(['TSE', 'OTC', 'DIST']))]['證券碼'].to_list() #已下市的也包含

m = pd.date_range('2010-01-01', '2022-11-18', freq='1M', inclusive='both').to_list()

price = pd.DataFrame()

for i in range(1, len(m)):
price = pd.concat([price, tej.get('TWN/APRCD1',
coid=all_code,
mdate={'gt': m[i-1], 'lt':m[i]+pd.Timedelta(days=1)},
opts={'columns': ['coid', 'mdate', 'close_adj', 'mv', 'div_yid']},
chinese_column_name=True,
paginate=True)])
print(f'目前週期:{m[i-1]}:{m[i]}')

price = price.reset_index(drop=True)
price = price.rename(columns={'證券代碼':'公司', '年月日':'年/月'})
price['公司'] = price['公司'].astype(int)
price = price.astype({'年/月':'datetime64[ns]'})
price['y'] = price['年/月'].dt.year

各家公司ESG等級請至TEJ PRO → TESG永續發展解決方案 → TESG永續發展指標 → TESG永續發展指標主表,下載至今最新版(2021年)的資料。

tesg = pd.read_csv('TESG_1118.csv')

tesg['證券代碼'] = tesg['證券代碼'].str.extract('(\d+)').astype(int)

tesg['年'] = tesg['年月'].map({201512:2015, 201612:2016, 201712:2017, 201812:2018, 201912:2019, 202012:2020, 202210:2021})

tesg = tesg[['證券代碼', '年', 'TESG等級']]

tesg = tesg[tesg['TESG等級'].isin(['A', 'A+', 'B+', 'B', 'B-'])]

tesg_rank = tesg.pivot(index='證券代碼', columns='年', values='TESG等級').fillna(0)

財報資料也可經由TEJ PRO或是Python API上下載,選擇以合併為主簡表(單季)-全產業,下載從2010年1月至2022年11月以來所有欄位的財報。

另外要注意的是,使用財報發布日的時間將每檔個股財報公布時間統一設為3月底、5月8月11月中旬,避免用不正確的財報時間來篩選個股,所以在此統一設在規定的最後一天。

fin_ind = pd.read_csv('上市櫃_財報指標_10_22.csv')

fin_ind = fin_ind.fillna(0)

fin_ind['公司'] = fin_ind['證券代碼'].str.extract('(\d+)')

fin_ind['年月'] = pd.to_datetime(fin_ind['年月'], format='%Y%m')

fin_ind['年月'] = fin_ind['年月'].apply(lambda x: pd.Period(x, freq='M').end_time.date())

fin_ind['年月'] = pd.to_datetime(fin_ind['年月'])

fin_ind['發布日'] = pd.to_datetime(fin_ind['財報發布日'], format='%Y/%m/%d')

fin_ind['年'] = fin_ind['年月'].dt.year

fin_ind['季'] = (fin_ind['年月'].dt.month/3).astype(int)

fin_ind = fin_ind.sort_values(['公司', '年月'])

# 轉換所有財報發布日至3月底、5,8,11月中
def date_change(x, y):
if x==1:
return pd.Timestamp(y.year, 5, 15)
elif x==2:
return pd.Timestamp(y.year, 8, 15)
elif x==3:
return pd.Timestamp(y.year, 11, 14)
elif x==4:
return pd.Timestamp(y.year, 3, 31)

fin_ind['年月'] = fin_ind.apply(lambda x: date_change(x['季'], x['發布日']), axis=1)

投組計算

首先抓出每一週期符合標準的個股與時間,計算這些個股在此時間內的累積報酬率,再將其用np.dot與對應的個股權重相乘,即得出每一週期的累計報酬率,我們以1000點作為初始值來觀察其成長。最早一批個股從2015/05/15篩選出並計算累計報酬。

def cul_index(price, df, init):
#將價格轉換為陣列型態
price_nd = price.pivot(columns='公司', index='年月', values='收盤價(元)')
#取出所有週期
period = df['年月'].drop_duplicates().to_list()
#取出所有公司
company = [list(df.groupby('年月'))[i][1]['公司'].to_list() for i in range(len(list(df.groupby(['年月']))))]
#取出所有權重分配
weights = [list(df.groupby('年月'))[i][1]['權重分配'].to_list() for i in range(len(list(df.groupby(['年月']))))]

init = init
index = []

for i in range(1, len(weights)):
index.append((price_nd.loc[period[i-1]: period[i], company[i-1]].pct_change()+1).cumprod().dot(init * np.array(weights[i-1])))
init = index[-1][-1]
print(f'第{i}次再平衡:{init:8.2f}')
index.append((price_nd.loc[period[-1]: pd.Timestamp(2022, 11, 18), company[-1]].pct_change()+1).cumprod().dot(init * np.array(weights[-1]))) #最後一期
#print(f'第{i+1}次再平衡:{init:8.2f}')

return pd.DataFrame(pd.concat(index).dropna(), columns=['指數'])
第一期個股

我們將所有的篩選標準以及計算function整合進下表的投組公式內。參數有項year_2022,是假設2022年所有個股的ESG分數和2021年相同,以方便我們觀察該投組最新的表現。

def constr_index(n1, n2, n3, n4, init, tesg, year_2022=False):
#假設2022 ESG分數和2021一樣
tmp = tesg.loc[tesg['年'] == 2021].copy()
tmp['年'] = 2022
tesg = pd.concat([tesg, tmp]).reset_index(drop=True)
#--------------------------------------------------------選擇性開啟

fin_ind['常續性稅後淨利'].fillna(1)
fin_ind['淨利CAGR_3'] = fin_ind.groupby('公司')['常續性稅後淨利'].transform(lambda x: (x.pct_change(n1) + 1)**(1/n1)-1 )

fin_ind['產業ROE中位數'] = fin_ind.groupby(['年月', 'TSE新產業名'])['ROE(A)-稅後'].transform(lambda x: x.median())

filter1 = pd.merge_asof(fin_ind.sort_values('年月'), price.sort_values('年月'), on='年月', by='公司').sort_values(['年月', '公司'])

filter1['近三年股利殖利率'] = filter1.groupby('公司')['股利殖利率-TSE'].transform(lambda x: x.rolling(n2).mean())

filter1 = filter1.merge(tesg, left_on=['年', '公司'], right_on=['年', '證券代碼'])

filter1 = filter1[['公司', '代碼', '年月', '淨利CAGR_3', '產業ROE中位數', 'ROE(A)-稅後', '市值(百萬元)', '股利殖利率-TSE', '年', 'TESG等級', '近三年股利殖利率', 'TSE新產業名']]

#多點ROE濾網
condition1 = filter1['淨利CAGR_3'] >= n3
condition2 = filter1['ROE(A)-稅後'] >= filter1['產業ROE中位數']
condition3 = filter1['市值(百萬元)']/100 >= 10

filter1 = filter1[condition1 & condition2 & condition3]

filter1['股利分數'] = filter1['近三年股利殖利率']*0.8 + filter1['股利殖利率-TSE']*0.2

filter1 = filter1.dropna(subset=['股利分數'])

div_30_Q = filter1.groupby('年月').apply(lambda x: x.nlargest(n4, '股利分數'))

div_30_Q = div_30_Q.drop(columns=['年月']).reset_index().drop(columns='level_1')

div_30_Q['權重分配'] = div_30_Q.reset_index().groupby('年月')['股利分數'].apply(lambda x: x/x.sum())
#每半年的3, 9月進行調整
if year_2022 == False:
div_30_Sem = div_30_Q[~(div_30_Q['年月'].dt.year == 2022)].sort_values(['年月', '公司']) #~(div_30_Q['年月'].dt.month.isin([3, 8])) &
else:
div_30_Sem = div_30_Q.sort_values(['年月', '公司'])

return cul_index(price, div_30_Sem, init), filter1, div_30_Sem
df, index_filter, div_30_Sem = constr_index(4, 12, 0.2, 20, 1000, tesg, year_2022=True)

參數分別為近4季常續性稅後淨利、近3年股利殖利率、常續稅後淨利CAGR >= 20%、20支成分股、起始值1000點和開啟2022年ESG分數假設。從下表可看出2015年5月到2022年11月總共進行30次再平衡,指數從1000點成長至7000點。

2015~2022

成果統計

我們下載同期間的大盤累計報酬指數-Y9997作為對照組,來觀察是否能勝過大盤。很明顯對比大盤累計205%的報酬,我們投組以716%累計報酬率勝過大盤。

base_index = tej.get('TWN/APRCD1', 
coid=['y9997'],
mdate={'gt': '2015-03-01', 'lt':'2022-12-01'},
opts={'columns': ['coid', 'mdate', 'close_adj']},
chinese_column_name=True,
paginate=True)

base_index['年月日'] = base_index['年月日'].astype('datetime64[ns]')

base_index = base_index.pivot(columns='證券代碼', index='年月日', values='收盤價(元)').rename_axis(None, axis=1).reset_index()

result = base_index.merge(df, left_on='年月日', right_on='年月')

result['997累計報酬'] = (result['Y9997'].pct_change()+1).cumprod()
result['指數累計報酬'] = (result['指數'].pct_change()+1).cumprod()
result

從下方圖表更能明顯看出投組從2016年以來就逐漸加大與大盤的差距,最大增幅來自2020年疫情V型反轉,基本當年度所累積的報酬率就接近100%。

績效指標

計算常見的各種績效指標如報酬率、夏普值和MDD等

stat = pd.DataFrame(index=['Y9997', '指數'],
columns=['年化標準差', '年化報酬率', '夏普值', 'MDD'],
data=[[cul_std(result['Y9997']), cul_ret(result['997累計報酬']), 0, mdd(result['997累計報酬'])],
[cul_std(result['指數']), cul_ret(result['指數累計報酬']), 0, mdd(result['指數累計報酬'])]])

stat['夏普值'] = stat['年化報酬率'] / stat['年化標準差']

stat['風報比'] = 0
stat['風報比'].iloc[0] = (result['997累計報酬'].iloc[-1]-1) / -stat['MDD'].iloc[0]
stat['風報比'].iloc[1] = (result['指數累計報酬'].iloc[-1]-1) / -stat['MDD'].iloc[1]

從實際的績效指標來看,可以看出在遠勝大盤的同時,年化標準差僅比大盤高0.18%,甚至最大回撤僅有26.45%比大盤還穩健,風報比更高達23.3。

績效指標

期間回撤幅度

圖形化呈現投組與大盤的期間回撤幅度

fig, ax = plt.subplots(figsize=(20, 6))

plt.rcParams['font.sans-serif'] = ['Taipei Sans TC Beta']

ax.plot(result['年月日'], result['997累計報酬'].to_drawdown_series().to_list(), color='black', linewidth=1.5)

ax.fill_between(result['年月日'], np.zeros(len(result['997累計報酬'])), result['997累計報酬'].to_drawdown_series().to_list(), label='大盤 DD', color='black', linewidth=1, alpha=0.5)

ax.plot(result['年月日'], result['指數累計報酬'].to_drawdown_series().to_list(), color='blue', linewidth=1.5)

ax.fill_between(result['年月日'], np.zeros(len(result['指數累計報酬'])), result['指數累計報酬'].to_drawdown_series().to_list(), label='獲利ESG DD', color='c', linewidth=1, alpha=0.7)

ax.grid()

ax.legend(loc='best', fontsize=16)

plt.xlabel('時間', fontsize=16)
plt.ylabel('下跌幅度', fontsize=16)

plt.title('大盤報酬&獲利ESG 回撤幅度', fontsize=20)

plt.show()

下表可以看出與大盤同期間的回撤幅度,可以看出我們藍色的投組雖然在前幾次大事件的股市下跌中比大盤還深以外,在2020年疫情以及2022美國大升息下回撤幅度皆比大盤來得小。

報酬分布

圖形化呈現每月的正負報酬分布與累積報酬率

fig = plt.figure(figsize=(16, 12))

ax = fig.subplots()

ax2 = ax.twinx()

ax.bar(index_m.index, [i if i >= 0 else 0 for i in index_m['單月報酬']], color='red', width=10, label='單月正報酬')

ax.bar(index_m.index, [i if i < 0 else 0 for i in index_m['單月報酬']], color='blue', width=10, label='單月負報酬')

ax2.plot(index_m.index, index_m['指數累計報酬']*100, label='累計報酬', linewidth=5)

ax.axhline(y = 0, color='black')

for i in range(2015, 2023):
plt.axvline(x = [pd.Timestamp(i,12,31)], color='black', linestyle="--", alpha=0.3)

ax.set_xlabel('時間', fontsize=16)

ax.set_ylabel('單月報酬率(%)', fontsize=16)

ax2.set_ylabel('指數累計報酬(%)', fontsize=16)

plt.title('單月報酬 vs. 累計報酬', fontsize=16)

fig.legend(loc='upper left', bbox_to_anchor=(0,1), bbox_transform=ax.transAxes, fontsize=16)

我們細看每個月的正負報酬分布,能看出除了2018中美貿易戰、2020新冠肺炎、2021台灣疫情和2022烏俄戰爭&FED大升息外,基本每月報酬多為正數,總體月勝率來到71.4%。

最新結果

最近一次投組篩選的成分股為下表20檔,可以看出自11/14以來近一個月的時間,篩選出至少4檔超過10%報酬率的股票,其中跌最多的也僅有-1.35%,當前報酬率來到4.133%。

結語

我們使用TESG提供的個股ESG評等作為基礎,結合要求較高的獲利成長指標,希望能在茫茫股海中找出兼具永續經營和成長潛力的標的,而從結果來看我們也成功達成了這一目標,在比大盤還穩健的情況下賺取超過大盤2倍以上的累積報酬率,展現出投資組合就算納入ESG指標,也還是能獲得顯著的超額報酬。文中所提供的投組程式碼以function的形式整合起來,方便讀者想進一步去調整參數,也可參考文中的做法自行選取其他財務指標,建構一專屬的ESG投資組合。

最後,還是要再次提醒本文所提及之標的僅供說明使用,不代表任何金融商品之推薦或建議。因此,若讀者對於建置策略、績效回測、研究實證等相關議題有興趣,歡迎選購 TEJ E Shop中的方案,具有齊全的資料庫,就能輕易的完成各種檢定。

完整程式碼

延伸閱讀

相關連結

返回總覽頁
Procesing