Article
Sustainable Portfolio Selection - An Approach To Impact Modelling
In our article Sustainable Portfolio Selection -- Markowitz goes ESG we describe the importance of ecological, social and governance (ESG) aspects of financial services and give a first approach, how ESG measures can be included into portfolio selection strategies. Basically, ESG-ratings of financial instruments, like for instance Refinitiv's ESG Scores, can be mathematically treated similar to mean historical returns. As a result, one can balance estimates for risk and/or return with ESG-ratings and compute portfolios of financial instruments with high average ESG Score as well as good risk measures.
In this follow-up article we adhere to the fact that high ESG-ratings can have a different impact in different industrial sectors or countries. We will show, how impact data can be integrated into portfolio selection models with ESG-awareness.
Overview
In this tutorial you can learn
- to get business sector information from Refinitiv and to plot the sector distribution of a weighted portfolio;
- how to balance volatility as a risk measure with portfolio returns and ESG-measures in portfolio selection;
- about the basics of impact modeling, and how sector impact information can be included into portfolio selection.
We build on the steps in Sustainable Portfolio Selection -- Markowitz goes ESG, where basically a classical Markowitz model is employed, but returns are replaced by ESG-ratings. This tutorial is structured as follows:
- Step 1 Get data via eikon, prepare the basic data structures.
- Step 2 Build the minimum volatility portfolio (MVP) and analyze its business sector distribution.
- Step 3 Build a portfolio, where volatility is balanced with return and also the ESG-score.
- Step 4 Build a model for impact-ESG and utilize it to select a portfolio, where volatility is balanced with return and the impact-ESG-score.
Technical prerequisites
- Refinitiv Eikon / Refinitiv Workspace with access to Eikon Data APIs (Free Trial Available) or Refinitiv Data Platform API
- Python 3.x
- Required Python Packages: refinitiv.dataplatform, pandas 0.17.0 or higher, numpy, , random, scipy, matplotlib
import pandas
import numpy
import random
import matplotlib.pyplot as plt
import scipy.optimize as sco
import os
import refinitiv.dataplatform.eikon as ek
ek.set_app_key('YOUR APPKEY HERE')
Data acquisition and universe selection
Our portfolio will be built from a universe (or a pre-selection) of shares. We will work based on the ETF iShares Core MSCI World UCITS ETF USD which refers to the MSCI World index. In difference to our earlier article, we enforce a lower bound on the ESG rating of financial instruments in the portfolio universe just from the beginning.
Reading the universe from Eikon
We use Eikon to get the constituents of the index. For simplicity we will reduce the list of entries here to 350. Besides the instrument identifer RIC, we load company names, their TR ESG Scores as well as the NACE Classification data which refers to the "Statistical Classification of Economic Activities in the European Community". Lateron, this data will be used to constitute an sector-related ESG impact.
N = 350
constituents, err = ek.get_data(['IWDA.L'], ['TR.ETPConstituentRIC', 'TR.ETPConstituentName'])
constituents.rename(columns={'Constituent RIC': 'ric', 'Constituent Name': 'name'}, inplace=True)
constituents = constituents[['ric','name']][0:N].drop_duplicates(subset=['ric'])
ric_list = list(constituents.ric[constituents.ric.astype(bool)])
df_esg, err = ek.get_data(ric_list, ['TR.CommonName', 'TR.TRESGScore','TR.BusinessSummary','TR.NACEClassification'])
df_esg = df_esg.rename(columns={'Company Common Name':'name', 'Instrument':'ric', 'ESG Score':'esg', 'NACE Classification':'nace'})
df_esg = df_esg.drop_duplicates(subset=['ric']).set_index('ric')
Constructing industrial sector information
There exist different classification systems mapping the economic activities of a company to a business sector. Refinitiv offers a bunch of these business sector information. One can, amongst others, download the sector with respect to the North American Industry Classification System (NAICS), the The Refinitiv Business Classification (TRBC), or -- as it is done here -- the NACE. The data item TR.NACEClassification is composed as follows,
BUSINESS SECTOR DESCRIPTION (NACE) (XX.YY)
We will utilize the business division encoded in XX and thus extract the string positions [-6:-4]. This data is included to the instruments' pandas dataframe.
nace_code_list =[]
for instr in ric_list:
df_instr = df_esg.loc[instr]
nace_code_list.append(df_instr['nace'][-6:-4])
df_nace = pandas.DataFrame(data=[ric_list,nace_code_list],index=['ric','nace_code']).T.set_index('ric')
df_esg_nace = pandas.concat([df_esg,df_nace],axis=1)[df_nace['nace_code'].map(len)>0]
esg_bound = 70
df_esg_constraint = df_esg_nace[(df_esg_nace.esg >= esg_bound)].replace('','nan')
K = len(df_esg_constraint.index)
print('Selected {K} out of {N} instruments by filtering ESG Score >= {esg}\n'.format(K=K,N=N,esg=esg_bound))
df_esg_constraint
Selected 113 out of 350 instruments by filtering ESG Score >= 70
ric |
name | esg | Business Description | nace | nace_code |
---|---|---|---|---|---|
FITB.OQ | Fifth Third Bancorp | 74.45776 | Fifth Third Bancorp is a bank holding company.... | Other monetary intermediation (NACE) (64.19) | 64 |
HOLX.OQ | Hologic Inc | 77.681436 | Hologic, Inc. is a developer, manufacturer and... | Manufacture of irradiation, electromedical and... | 26 |
8630.T | Sompo Holdings Inc | 77.448841 | Sompo Holdings, Inc. is a Japan-based company ... | Non-life insurance (NACE) (65.12) | 65 |
BAMa.TO | Brookfield Asset Management Inc | 77.97256 | Brookfield Asset Management Inc. is an alterna... | Renting and operating of own or leased real es... | 68 |
BCE.TO | BCE Inc | 71.008589 | BCE Inc. is a Canada-based communications comp... | Wired telecommunications activities (NACE) (61... | 61 |
... | ... | ... | ... | ... | ... |
LUMI.TA | Bank Leumi Le Israel BM | 76.909491 | Bank Leumi Le Israel BM (the Bank) is an Israe... | Other monetary intermediation (NACE) (64.19) | 64 |
LOW.N | Lowe's Companies Inc | 85.596615 | Lowe's Companies, Inc. (Lowe's) is a home impr... | Retail sale of hardware, paints and glass in s... | 47 |
3382.T | Seven & i Holdings Co Ltd | 86.572966 | Seven & i Holdings Co Ltd is a Japan-based com... | Retail sale in non-specialised stores with foo... | 47 |
VWS.CO | Vestas Wind Systems A/S | 77.767678 | Vestas Wind Systems A/S is a Denmark-based com... | Manufacture of engines and turbines, except ai... | 28 |
JNJ.N | Johnson & Johnson | 89.605162 | Johnson & Johnson is a holding company, which ... | Manufacture of pharmaceutical preparations (NA... | 21 |
print('Loading timeseries data from eikon')
start='2020-01-01'
end='2020-12-31'
instruments = df_esg_constraint.index
timeseries_data =pandas.DataFrame()
for r in instruments:
try:
ts1 = ek.get_timeseries(r,'CLOSE',start_date=start,end_date=end,interval='daily')
ts1.rename(columns = {'CLOSE' : r}, inplace=True)
timeseries_data =pandas.concat([timeseries_data, ts1], axis=1)
except:
continue
timeseries_data = timeseries_data.dropna()
timeseries_data
Loading timeseries data from eikon
Date | FITB.OQ | HOLX.OQ | 8630.T | BAMa.TO | BCE.TO | WBA.OQ | FTS.TO | HNKG_p.DE | RAND.AS | CONG.DE | ... | CRDA.L | EMR.N | MAP.MC | CATL.SI^I21 | 7974.T | LUMI.TA | LOW.N | 3382.T | VWS.CO | JNJ.N |
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
2020-01-07 | 29.85 | 52.49 | 4280.0 | 49.729029 | 60.48 | 59.29 | 54.13 | 91.58 | 54.86 | 102.944218 | ... | 5090 | 77.24 | 2.4 | 2.92704 | 42940 | 2491 | 119.63 | 3969 | 127.92 | 144.98 |
2020-01-08 | 29.92 | 52.29 | 4188.0 | 49.934193 | 60.64 | 55.83 | 54.34 | 91.74 | 55.32 | 104.929418 | ... | 5090 | 77.51 | 2.4 | 2.934662 | 42640 | 2484 | 121.53 | 3942 | 129.08 | 144.96 |
2020-01-09 | 30.25 | 53.275 | 4228.0 | 50.377611 | 60.29 | 54.68 | 54.42 | 93.04 | 55.9 | 107.415388 | ... | 5035 | 77.8 | 2.413 | 2.92704 | 43380 | 2483 | 122.05 | 4022 | 128.32 | 145.39 |
2020-01-14 | 29.74 | 53.53 | 4213.0 | 51.972594 | 60.89 | 54.62 | 54.92 | 93.12 | 55.58 | 105.179803 | ... | 5145 | 76.87 | 2.419 | 3.003265 | 43200 | 2472 | 120.07 | 4285 | 126.52 | 146.52 |
2020-01-15 | 29.03 | 53.75 | 4215.0 | 52.435866 | 61.11 | 54.43 | 55.24 | 93.18 | 55.42 | 104.839994 | ... | 5030 | 76.66 | 2.384 | 2.980398 | 43070 | 2446 | 119.52 | 4249 | 129.48 | 147.01 |
... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... |
2020-12-21 | 27.165 | 75.23 | 4038.0 | 50.609222 | 54.75 | 40.67 | 52.08 | 89.2 | 53.52 | 104.580666 | ... | 6354 | 80.57 | 1.599 | 2.492557 | 65250 | 1816 | 164.33 | 3528 | 270.0 | 153.02 |
2020-12-22 | 26.93 | 74.67 | 4034.0 | 50.738277 | 54.6 | 39.27 | 52.18 | 90.14 | 53.76 | 105.877306 | ... | 6422 | 79.33 | 1.63 | 2.4392 | 64400 | 1827 | 164.61 | 3503 | 280.6 | 152.72 |
2020-12-23 | 27.73 | 75.06 | 4068.0 | 51.433186 | 54.66 | 39.87 | 52.24 | 90.48 | 54.32 | 108.783566 | ... | 6416 | 80.02 | 1.632 | 2.462067 | 64480 | 1841 | 162.43 | 3509 | 278.2 | 151.94 |
2020-12-29 | 27.28 | 71.74 | 4186.0 | 52.495404 | 54.69 | 39.41 | 52.54 | 92.46 | 54.64 | 109.767224 | ... | 6594 | 79.24 | 1.614 | 2.507802 | 65860 | 1900 | 160.54 | 3710 | 291.8 | 154.14 |
2020-12-30 | 27.28 | 71.75 | 4173.0 | 52.336567 | 54.6 | 39.34 | 52.38 | 92.3 | 54.14 | 108.425872 | ... | 6536 | 79.82 | 1.608 | 2.50018 | 65830 | 1904 | 160.56 | 3659 | 287.9 | 156.05 |
Minimum Volatility Portfolio (MVP) on the ESG-constrained universe
We compute the classical MVP in the same way as in our previous article -- please consult that tutorial for details. Given the pre-filter on the constituents' ESG Score, the MVP satisfies a lower bound
ESG(MVP)≥70
returns = timeseries_data.pct_change().replace(numpy.inf, numpy.nan).dropna()
covMatrix = returns.cov()
def risk_measure(covMatrix, weights):
return numpy.dot(weights, numpy.dot(covMatrix, weights))
bounds = K * [(0, 1)]
constraints = {'type': 'eq', 'fun': lambda weights: weights.sum() - 1}
mvp = sco.minimize(lambda x: risk_measure(covMatrix, x), # function to be minimized
K * [1 / K], # initial guess
bounds=bounds, # box constraints
constraints =constraints, # equality constraints
)
print('''MVP in a universe with {K} instruments
Number of selected instruments: {n}
Minimum weight: {minw}
Maximum weight: {maxw}
Historical risk measure: {risk}
Historical return p.a.: {r}
ESG score: {esg}'''.format(K=K,
n=numpy.sum(mvp['x']>1e-4),
minw=numpy.min(mvp['x'][numpy.nonzero(mvp['x'])]),
maxw=numpy.max(mvp['x']),
risk=risk_measure(covMatrix, mvp['x']),
r=numpy.dot(mvp['x'],returns.sum()),
esg=numpy.dot(mvp['x'], df_esg_constraint['esg'])))
MVP in a universe with 113 instruments
Number of selected instruments: 45
Minimum weight: 7.503984557423134e-21
Maximum weight: 0.06185783693486125
Historical risk measure: 0.00015195017397695131
Historical return p.a.: 0.11247632176102468
ESG score: 78.49331458983875
You may use the the code in the previous article to get further insights into the structure of the resulting MVP. We now go further and plot the distribution of the investment on the NACE divisions:
Pie chart of the business sectors distribution of the MVP
Given the weight (portion) of each instrument within the (ESG-constrained) portfolio, the portion of the investment within each business sector is accumulated by the function calcWeightsForPieChart(df). Then, a pie chart illustrates, how the money is allotted to business sectors.
nace_dict ={'01': 'Crop and animal production, hunting and related service activities', '02': 'Forestry and logging', '03': 'Fishing and aquaculture', '05': 'Mining of coal and lignite', '06': 'Extraction of crude petroleum and natural gas', '07': 'Mining of metal ores', '08': 'Other mining and quarrying', '09': 'Mining support service activities', '10': 'Manufacture of food products', '11': 'Manufacture of beverages', '12': 'Manufacture of tobacco products', '13': 'Manufacture of textiles', '14': 'Manufacture of wearing apparel', '15': 'Manufacture of leather and related products', '16': 'Manufacture of wood and of products of wood and cork, except furniture; manufacture of articles of straw and plaiting materials', '17': 'Manufacture of paper and paper products', '18': 'Printing and reproduction of recorded media', '19': 'Manufacture of coke and refined petroleum products', '20': 'Manufacture of chemicals and chemical products', '21': 'Manufacture of basic pharmaceutical products and pharmaceutical preparations', '22': 'Manufacture of rubber and plastic products', '23': 'Manufacture of other non-metallic mineral products', '24': 'Manufacture of basic metals', '25': 'Manufacture of fabricated metal products, except machinery and equipment', '26': 'Manufacture of computer, electronic and optical products', '27': 'Manufacture of electrical equipment', '28': 'Manufacture of machinery and equipment n.e.c.', '29': 'Manufacture of motor vehicles, trailers and semi-trailers', '30': 'Manufacture of other transport equipment', '31': 'Manufacture of furniture', '32': 'Other manufacturing', '33': 'Repair and installation of machinery and equipment', '35': 'Electricity, gas, steam and air conditioning supply', '36': 'Water collection, treatment and supply', '37': 'Sewerage', '38': 'Waste collection, treatment and disposal activities; materials recovery', '39': 'Remediation activities and other waste management services', '41': 'Construction of buildings', '42': 'Civil engineering', '43': 'Specialised construction activities', '45': 'Wholesale and retail trade and repair of motor vehicles and motorcycles', '46': 'Wholesale trade, except of motor vehicles and motorcycles', '47': 'Retail trade, except of motor vehicles and motorcycles', '49': 'Land transport and transport via pipelines', '50': 'Water transport', '51': 'Air transport', '52': 'Warehousing and support activities for transportation', '53': 'Postal and courier activities', '55': 'Accommodation', '56': 'Food and beverage service activities', '58': 'Publishing activities', '59': 'Motion picture, video and television programme production, sound recording and music publishing activities', '60': 'Programming and broadcasting activities', '61': 'Telecommunications', '62': 'Computer programming, consultancy and related activities', '63': 'Information service activities', '64': 'Financial service activities, except insurance and pension funding', '65': 'Insurance, reinsurance and pension funding, except compulsory social security', '66': 'Activities auxiliary to financial services and insurance activities', '68': 'Real estate activities', '69': 'Legal and accounting activities', '70': 'Activities of head offices; management consultancy activities', '71': 'Architectural and engineering activities; technical testing and analysis', '72': 'Scientific research and development ', '73': 'Advertising and market research', '74': 'Other professional, scientific and technical activities', '75': 'Veterinary activities', '77': 'Rental and leasing activities', '78': 'Employment activities', '79': 'Travel agency, tour operator and other reservation service and related activities', '80': 'Security and investigation activities', '81': 'Services to buildings and landscape activities', '82': 'Office administrative, office support and other business support activities', '84': 'Public administration and defence; compulsory social security', '85': 'Education', '86': 'Human health activities', '87': 'Residential care activities', '88': 'Social work activities without accommodation', '90': 'Creative, arts and entertainment activities', '91': 'Libraries, archives, museums and other cultural activities', '92': 'Gambling and betting activities', '93': 'Sports activities and amusement and recreation activities', '94': 'Activities of membership organisations', '95': 'Repair of computers and personal and household goods', '96': 'Other personal service activities', '97': 'Activities of households as employers of domestic personnel', '98': 'Undifferentiated goods- and services-producing activities of private households for own use', '99': 'Activities of extraterritorial organisations and bodies', 'nan': 'Others'}
def calcWeightsForPieChart(df):
#calculate weights for pie chart
df_nan = df.replace('','nan')
sections = {item for item in list(df_nan['nace_code']) if len(item)>0}
piesizes = {}
for s in sections:
piesizes[str(s)] = df_nan[df_nan['nace_code']==s]['weight'].values.sum()
return piesizes
def plotPieChartWithNACELabels(nace_dict,piesizes):
## piechart for nace codes
labels = []
sizes = []
for x, y in piesizes.items():
labels.append('{d} ({c})'.format(d=nace_dict[x],c=str(x)))
sizes.append(y)
plt.figure(figsize=(10, 10), dpi=80)
plt.pie(sizes, labels=labels)
plt.axis('equal')
plt.show()
df_esg_constraint['weight'] = list(mvp['x'])
piesizes = calcWeightsForPieChart(df_esg_constraint)
plotPieChartWithNACELabels(nace_dict, piesizes)
The minimum volatility portfolio is nicely diversified and covers a large number of business sectors. Note that using e.g. address or currency data from Refinitiv, you can also plot the distribution over countries or currencies.
Balancing volatility with historical return and ESG-score
In addition to a small risk, previously estimated by a small historical volatility, one may aim for a high return and high ESG ratings in addition to the lower bound of 70 that was imposed on the optimization universe. The following code minimizes historical volatility and in addition, minimizes - 0.0002 * historical return and - 0.00002* ESG-rating of a portfolio. There is thus a trade-of between a higher return or higher ESG Score with low volatility. The factors 0.0002 and 0.00002 mirror the importance of each of the criteria. With a higher factor, e.g. with the return term, the portfolio is selected focusing more on the return, and less on the the other measures.
esg_ret_mvp = sco.minimize(lambda x: risk_measure(covMatrix, x) - 0.0002*numpy.dot(x, returns.sum()) - 0.00002*numpy.dot(x, df_esg_constraint['esg']), # function to be minimized
K * [1 / K], # initial guess
bounds=bounds, # box constraints
constraints =constraints, # equality constraints
)
print('''Solution to balanced volatility, return, ESG problem in a universe with {K} instruments
Number of selected instruments: {n}
Minimum weight: {minw}
Maximum weight: {maxw}
Historical risk measure: {risk}
Historical return p.a.: {r}
ESG score: {esg}'''.format(K=K,
n=numpy.sum(esg_ret_mvp['x']>1e-4),
minw=numpy.min(esg_ret_mvp['x'][numpy.nonzero(esg_ret_mvp['x'])]),
maxw=numpy.max(esg_ret_mvp['x']),
risk=risk_measure(covMatrix, esg_ret_mvp['x']),
r=numpy.dot(esg_ret_mvp['x'],returns.sum()),
esg=numpy.dot(esg_ret_mvp['x'], df_esg_constraint['esg'])))
Solution to balanced volatility, return, ESG problem in a universe with 113 instruments
Number of selected instruments: 28
Minimum weight: 3.3825682612790066e-20
Maximum weight: 0.10165960999890983
Historical risk measure: 0.00018671090425783444
Historical return p.a.: 0.17080291589692786
ESG score: 85.61815983112916
We gain some insight into the business sector diversification from the according pie chart: Notice, that we now have a trade-of between volatility and returns as well as ESG-scores. Since the latter terms do not diversify, but rather select high weights for companies with high return and/or ESG-score, the current portfolio is less distributed over the business sectors than the MVP from before.
df_esg_constraint['weight'] = list(esg_ret_mvp['x'])
piesizes = calcWeightsForPieChart(df_esg_constraint)
plotPieChartWithNACELabels(nace_dict, piesizes)
An approach to impact modeling
The idea behind impact investment is to invest specifically in companies that have a positive impact on social or environmental goals. Depending on the location or on the sector of economic activities, a high ESG Score can have a different impact -- for instance, a company that cares for clean drinking water has more impact in countries where clean water is actually an issue. Or using only renewable energies has much more impact in energy intense business sectors. We leave an evaluation of this kind of impact to experts in the field, and show here, how impact expertise can be included into our portfolio selection model.
Organization of impact information
We assume that wise people provide a score between 0 and 10 for each of the sectors covered by our universe. The score 0 means that an ESG-Rating exceeding the lower bound of 70 has no impact in this sector, whereas the score 10 implies a high impact of extra ESG points. To keep our example simple and illustrative, we just select one sector (construction of buildings, no. 41) which has the impact 10, and assume that all others have the impact 0 (for whatever reason). This impact information is assumed to be given by experts in a dictionary nace_dict with 2 digit NACE codes as keys and the respective impact score as values. You may use the commented line with random numbers for a more realistic setting of nace_dict. The impact scores are included into the instruments dataframe in the last line of the following code block.
nace_impact_dict ={key: 0 for key in nace_dict.keys()}
nace_impact_dict['41']=10
#nace_impact_dict ={key: random.randint(0, 10) for key in nace_dict.keys()}
df_esg_constraint['nace_impact'] = df_esg_constraint['nace_code'].apply(lambda x: nace_impact_dict[x])
Computation of the impact-ESG score of a portfolio
The impact-ESG score of a company now combines the ESG-Score with the sector impact score as follows:
𝑖𝑚𝑝𝑎𝑐𝑡𝐸𝑆𝐺=𝑖𝑚𝑝𝑎𝑐𝑡⋅(𝐸𝑆𝐺−𝑒𝑠𝑔𝐵𝑜𝑢𝑛𝑑)impactESG=impact⋅(ESG−esgBound)
This formula is applied to each company in a dataframe of all companies in the optimization universe in the following line,
impact_esg_score = lambda df: df['nace_impact']*(df['esg']-esg_bound)
and we compute some impact-ESG-scores of weighted portfolios in a scalar product as follows,
mvp_impact_esg = numpy.dot(mvp['x'], impact_esg_score(df_esg_constraint))
mvp_ret_impact_esg = numpy.dot(esg_ret_mvp['x'], impact_esg_score(df_esg_constraint))
print('ESG of MVP: {mvpesg}\nImpact-ESG of MVP: {mvpiesg}\n'.format(mvpesg=numpy.dot(mvp['x'], df_esg_constraint['esg']), mvpiesg=mvp_impact_esg))
print('ESG of balanced: {balesg}\nImpact-ESG of balanced: {baliesg}'.format(balesg=numpy.dot(esg_ret_mvp['x'], df_esg_constraint['esg']), baliesg=mvp_ret_impact_esg))
ESG of MVP: 78.49331458983875
Impact-ESG of MVP: 2.87839973147472
ESG of balanced: 85.61815983112916
Impact-ESG of balanced: 3.0997556428568394
Remember that the ESG-aware portfolio has a higher ESG-score than the basic MVP. But nevertheless, the impact-ESG score (which, in our example, sais that ESG-ratings higher than 70 make sense only for companies constructing buildings) is even decreased compared to the MVP.
Impact-ESG-aware portfolio selection
In order to build an impact-ESG-aware portfolio, we exchange the simple ESG-term with the impact-ESG term in the minimization problem.
impact_esg_mvp = sco.minimize(lambda x: risk_measure(covMatrix, x) - 0.0002*numpy.dot(x, returns.sum()) - 0.000004*numpy.dot(x, impact_esg_score(df_esg_constraint)), # function to be minimized
K * [1 / K], # initial guess
bounds=bounds, # boundary conditions
constraints =constraints, # equality constraints
)
print('''Solution to weighted impact ESG problem in a universe with {K} instruments
Number of selected instruments: {n}
Minimum weight: {minw}
Maximum weight: {maxw}
Historical risk measure: {risk}
Historical return p.a.: {r}
ESG score: {esg}'''.format(K=K,
n=numpy.sum(impact_esg_mvp['x']>1e-4),
minw=numpy.min(impact_esg_mvp['x'][numpy.nonzero(impact_esg_mvp['x'])]),
maxw=numpy.max(impact_esg_mvp['x']),
risk=risk_measure(covMatrix, impact_esg_mvp['x']),
r=numpy.dot(impact_esg_mvp['x'],returns.sum()),
esg=numpy.dot(impact_esg_mvp['x'], df_esg_constraint['esg'])))
Solution to weighted impact ESG problem in a universe with 113 instruments
Number of selected instruments: 14
Minimum weight: 1.9572764759356897e-21
Maximum weight: 0.433838334307341
Historical risk measure: 0.00020403642361264564
Historical return p.a.: 0.10575171137227271
ESG score: 80.22443849877946
We compare the ESG- and impact-ESG-ratings of the three portfolios:
mvp_impact_esg = numpy.dot(mvp['x'], impact_esg_score(df_esg_constraint))
mvp_ret_impact_esg = numpy.dot(esg_ret_mvp['x'], impact_esg_score(df_esg_constraint))
print('ESG of MVP: {esg}\nImpact-ESG of MVP: {iesg}\n'.format(
esg=numpy.dot(mvp['x'], df_esg_constraint['esg']),
iesg=mvp_impact_esg))
print('ESG of return and ESG balanced: {esg}\nImpact-ESG of return and ESG balanced: {iesg}\n'.format(
esg=numpy.dot(esg_ret_mvp['x'], df_esg_constraint['esg']),
iesg=mvp_ret_impact_esg))
print('ESG of return and impact-ESG balanced: {esg}\nImpact-ESG of return and impact-ESG balanced: {iesg}\n'.format(
esg=numpy.dot(impact_esg_mvp['x'], df_esg_constraint['esg']),
iesg=numpy.dot(impact_esg_mvp['x'], impact_esg_score(df_esg_constraint))))
ESG of MVP: 78.49331458983875
Impact-ESG of MVP: 2.87839973147472
ESG of return and ESG balanced: 85.61815983112916
Impact-ESG of return and ESG balanced: 3.0997556428568394
ESG of return and impact-ESG balanced: 80.22443849877946
Impact-ESG of return and impact-ESG balanced: 56.944799960584405
In fact, the impact-ESG optimization was successful: The respective portfolio achieves a much higher impact-ESG-score than the competing portfolios.
We also plot the business sectors pie chart and of course, the impact-ESG-aware portfolio has a special focus on the construction of buildings:
df_esg_constraint['weight'] = impact_esg_mvp['x']
piesizes = calcWeightsForPieChart(df_esg_constraint)
plotPieChartWithNACELabels(nace_dict, piesizes)
Summary and outlook
We hope that we could show, how different aspects and criteria can be considered in a portfolio selection strategy. We have constructed 3 portfolios: All of them satisfy a lower bound on the ESG-score in every constituent. By using specific objective functions in the minimization problem, we could steer the distribution of the investment to have low volatility in the first one, and a trade-of between low volatility but also high returns and high ESG-scores in the second one. For the third portfolio, we showed a model for impact-ESG scores and included it into the optimization goal.
This tutorial aims to show some key techniques and thus scratches only the surface of ESG- and risk-aware portfolio selection. One can refine the models in many aspects, and one could go for more details in the analysis of the results. We refer to the literature and to the large pool of Refinitiv tutorials in the developers' community.
For more information about the authors, check out https://goldmarie-finanzen.de!
Further Resources for Eikon Data API
For Content Navigation in Eikon - please use the Data Item Browser Application: Type 'DIB' into Eikon Search Bar.