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中的方案,具有齐全的资料库,就能轻易的完成各种检定。

完整程式码

延伸阅读

相关连结

返回总览页