1. Introduction
2. Data Feeds
3. Step I: Incident score
4. Step II: Raw Investment Score by company
5. Step III: Scaling of raw Investment Scores
6. Investment scores
7. Investment sub-scores
8. UNGC investment scores
9. SASB investment scores
10. Trading strategy example
1. Introduction¶
This is a straightforward guide on how to calculate and customize Investment scores as a basis for business conduct ratings and trading signals for the equity and bond portfolio construction.
After subscribing to RepRisk’s Data Feeds, the Python code below can be used for building customized Investment scores and ratings.
Go to the interactive examples
Typical client use cases:
- Systematic Trader – add new alpha sources long/short
- Portfolio Manager – add portfolio construction signals
- Fund Manager – rate equity or bond funds
- Equity Index – example SPDR S&P 500 RepRisk UCITS ETF
- Sin-Portfolios – driven by business conduct (not by exclusion)
With this tool, clients can:
- increase the investment universe by re-weighting the different components;
- build business conduct-tilted products (e.g., ETFs, funds);
- construct and monitor portfolios in compliance with client requests and specifications;
- generate any custom factors (e.g., factors based on the SDG 5, i.e., Gender equality, or the UNGC Labour Principles 3 to 6);
- create trading signals.
2. Data Feeds¶
Clients have access to the incident-level data through:
- RepRisk's daily Data Feeds (FTP)
- API (e.g., Factset, Crux Informatics, Battlefin and others)
Read RepRisk’s full methodology.
Each incident in the RepRisk Dataset is rated based on severity, reach, and novelty:
- Severity [1, 2, 3] reflects the harshness of the risk incident or criticism; where [3] denotes a very severe risk incident or criticism and [1] denotes low severity.
- Reach [1, 2, 3] of the information source (influence based on readership/circulation as well as by its importance in a specific country); where [3] denotes high reach and [1] denotes low reach.
- Novelty [1, 2] reflects the newness of the issues addressed for the company, i.e., whether it is the first time a company/project is exposed to a specific RepRisk Issue in a certain location; where [2] denotes the first time a company is exposed to this issue and [1] denotes repeated occurrences.
The code section below loads all risk incidents, calculates their incident scores, and outputs the data example in the Incident Level Data table.
# load risk incidents from database
import pandas as pd
import numpy as np
import warnings
import plotly.io as pio
pio.renderers.default = "notebook_connected"
warnings.filterwarnings("ignore")
pd.options.mode.chained_assignment = None
# load Incident File
incident_data = pd.read_csv('221228101343_IncidentFileV2_Dec22_LBr.zip', sep=";")
incident_data = incident_data.drop_duplicates(subset=['Company ID', 'Story ID'])
incident_data['Incident Date'] = pd.to_datetime(incident_data['Incident Date'])
incident_data['Severity'].replace({0: 1}, inplace=True)
incident_data['Reach'].replace({0: 1}, inplace=True)
incident_data['Novelty'].replace({0: 1}, inplace=True)
# calculate incident scores
severity_weights = {1: 1,
2: 10,
3: 100}
reach_weights = {1: 1,
2: 2,
3: 3}
novelty_weights = {1: 1,
2: 2}
incident_data["Incident Score"] = (incident_data['Severity'].replace(severity_weights)
*incident_data['Reach'].replace(reach_weights)
*incident_data['Novelty'].replace(novelty_weights))
# prepare for displaying data feeds
incident_v2 = incident_data
cols = incident_v2.columns.tolist()
incident_v2 = incident_v2[cols[0:3] + cols[4:9] + cols[-1:] + cols[9:30] + cols[32:-1]]
pd.options.display.max_columns = None
displayDF = incident_v2[pd.notnull(incident_v2['Related Countries']) &
incident_v2['Related Countries'].str.contains(';')].sample(2).copy()
displayDF = pd.concat([displayDF, incident_v2.sample(4)], ignore_index=True)
incident_v2 = []
display(displayDF.sample(6))
3. Step I: Incident score¶
We estimate that each incident reflects a company's business conduct risk exposure, and that its risk increases with the rising incident level. The incident score is thus calculated as follows:
\begin{align} \text{Incident score} &= \text{Severity weight} \times \text{Reach weight} \times \text{Novelty weight}\\ \end{align}
| Severity level | Severity weight |
|---|---|
| 3 (Very severe incident) | 100 |
| 2 (Severe incident) | 10 |
| 1 (Less severe incident) | 1 |
| Reach level | Reach weight |
|---|---|
| 3 (High reach source) | 3 |
| 2 (Medium reach source) | 2 |
| 1 (Limited reach source) | 1 |
| Novelty level | Novelty weight |
|---|---|
| 2 (Novel) | 2 |
| 1 (Repeated) | 1 |
3.1. Incident score example¶
- Severity = 3
- Reach = 2
- Novelty = 2
\begin{align} \text{Incident score} &= \text{Severity weight} \times \text{Reach weight} \times \text{Novelty weight}\\ &= 100 \times 2 \times 2 \\ &=400 \end{align}
Alternatively, the initial incident score can be calculated using an arithmetic or geometric mean:
\begin{align} \text{Arithmetic Incident score} &= \frac{1}{3}( \text{exp(3)} \times \text{Severity level} + \text{exp(2)} \times \text{Reach level} + \text{exp(1)} \times \text{Novelty level})\\ \end{align}
\begin{align} \text{Geometric Incident score} &= \sqrt[3]{( \text{exp(Severity level)} \times \text{exp(Reach level)} \times \text{exp(Novelty level)} }\\ \end{align}
4. Step II: Raw Investment Score by company¶
We estimate that internal business processes causing business conduct violations need an average time of two years to be mitigated.
To calculate the current Investment Score of a company, the past two years of incident scores are time-weighted:
\begin{align} \text{Raw Investment score} &= \sum_{i} \text{Time weight}_{i} \times \text{Incident score}_{i}\\ \end{align}
# calculate and plot time weights
import plotly.express as px
n_years = 2
n_days = 365*n_years
curvature_ = 1
weights = np.arange(1,n_days + 1)
weights = weights/weights.max()
weights = ((2**weights) - 1)**curvature_
time_weights = pd.Series(weights)
fig = px.line(x=time_weights.index, y=time_weights)
fig.update_layout(font=dict(color='#231F20', family='UnitWeb, UnitOT, Verdana'),
xaxis=dict(title="Day in a two-year window"),
yaxis=dict(title="Time weight"),
template="plotly_white",
margin=dict(l=200, r=200),
height=400)
fig.show()
4.1. Raw Investment Score (calculated for selected companies)¶
This graph shows the raw scores calculated for a selected set of listed companies.
# plot raw Investment score for sample companies
import plotly.graph_objects as go
companies_table = pd.read_excel("ticker_data/company_list.xlsx")
companies_table['Name'] = companies_table['Platform Name'].str.split("(").str[0].str.strip()
companies_table.set_index("Name", inplace=True)
companies = companies_table["Company ID"]
start = pd.Timestamp("2007-01-01")
todate = max(incident_data['Incident Date']).strftime('%Y-%m-%d')
end = pd.Timestamp(todate)
date_range_extended = pd.date_range(start=start-pd.Timedelta(days=n_days), end=end)
date_range = pd.date_range(start=start, end=end)
incident_data.set_index("Company ID", inplace=True)
def get_raw_scores(id_, incident_data, col="Incident Score"):
try:
company_incidents = incident_data.reset_index().set_index('Incident Date')
company_incidents = company_incidents[company_incidents['Company ID']==id_]
incident_scores = company_incidents[col].groupby(level=0).sum().reindex(date_range_extended).fillna(0)
stacked_incident_score = incident_scores.rolling(n_days).apply(lambda scores: np.dot(scores, weights)).fillna(0)
stacked_incident_score = stacked_incident_score.reindex(date_range)
except KeyError:
stacked_incident_score = pd.Series(0, index=date_range)
stacked_incident_score.name = "Incident Score"
return stacked_incident_score
colors = px.colors.qualitative.Dark24*2
fig = go.Figure()
for name, id_ in companies.items():
series = get_raw_scores(id_, incident_data)
fig.add_trace(go.Scatter(x=series.index, y=series.values, name=name,
line=dict(color=colors.pop())))
fig.update_layout(font=dict(color='#231F20', family='UnitWeb, UnitOT, Verdana'),
xaxis_rangeslider_visible=True,
legend=dict(title=dict(text=" ", side='top'), x=0, y=0.99,
orientation="h", bgcolor='rgba(0,0,0,0)'),
template="plotly_white",
margin=dict(l=20, r=20, b=10),
height=600,
title=dict(text='<b>Raw Investment Scores</b>',
y=0.99, x=0,
xanchor='left',
yanchor='top'))
fig.show()
5. Step III: Scaling of raw Investment Scores to [0, 100]¶
The graph showing the raw Investment scores reflects notably that:
- large companies tend to be linked to more incidents and get more media attention.
- there seems to be a general increase in incidents or reporting over time.
To make the Investment scores comparable and also aggregate them on a sector, industry, or country level, the raw Investment scores have to be scaled to [0, 100].
There are many ways to scale or map the raw Investment scores, however, in line with the incident and raw Investment score calculation, an exponential cumulative distribution function is used for scaling, as shown below.
In addition, the decay of the score is limited to a maximum of 50% per year (corresponding to a minimum half-life period of one year).
# exponential cumulative distribution function
def scale(series, lambda_=0.000105, curvature=5.3):
return 100*((1 - np.exp(-lambda_*series))**(1/curvature))
scaling = scale(pd.Series(range(750)))
fig = px.line(x=scaling.index, y=scaling)
fig.update_layout(font=dict(color='#231F20', family='UnitWeb, UnitOT, Verdana'),
xaxis=dict(title="Raw Investment Score"),
yaxis=dict(title="Investment Score"),
template="plotly_white",
margin=dict(l=200, r=200),
height=400)
fig.show()
def max_decay(series, decay=0.5**(1/365)):
new_series = pd.Series(index=series.index, dtype="float")
previous = 0
for date, value in series.items():
if previous > 0:
if value/previous < decay:
new_series[date] = previous*decay
else:
new_series[date] = value
else:
new_series[date] = value
previous = new_series[date]
return new_series
6. Investment scores¶
As a result of the previous step, the scaled Investment scores are more regular and more comparable with respect to size over time:
- Companies with a high number of incidents have been compressed.
- Companies with a smaller number of incidents have been scaled up.
- Incidents are more comparable over time.
The Investment scores can be extended with a systematic adjustment of size / market cap and location bias (see RepRisk Due Diligence Score). For example, larger companies are typically more in the spotlight and receive more coverage than smaller ones. In addition, companies in countries with high business conduct risk awareness and countries where freedom of the media is guaranteed are more likely to be scrutinized. To prevent the count from being biased for large companies and those located in specific countries, the RepRisk Due Diligence Score applies a normalization factor to the count of less severe (severity 1) incidents.
# plot outside-in score for sample companies
colors = px.colors.qualitative.Dark24*2
fig = go.Figure()
for name, id_ in companies.items():
series = get_raw_scores(id_, incident_data)
series = max_decay(scale(series))
fig.add_trace(go.Scatter(x=series.index, y=series.values, name=name,
line=dict(color=colors.pop())))
fig.update_traces(hovertemplate='Investment score: %{y}')
fig.update_yaxes(tickfont=dict(color='#231F20'))
fig.update_xaxes(tickfont=dict(color='#231F20'))
fig.update_layout(font=dict(color='#231F20', family='UnitWeb, UnitOT, Verdana'),
xaxis_rangeslider_visible=True,
legend=dict(title=dict(text=" ", side='top'), x=0, y=0.99,
orientation="h", bgcolor='rgba(0,0,0,0)'),
legend_font_color="#231F20",
template="plotly_white",
margin=dict(l=20, r=20, b=10),
autosize=True,
title=dict(text='<b>Investment Scores</b>',
y=0.99, x=0,
xanchor='left',
yanchor='top'),
title_font_family="UnitWeb, UnitOT, Verdana",
title_font_color="#231F20",
title_font_size=18)
fig.show()
fig.write_html('images/investment_score_plot.html', config={'displayModeBar': False})
7. Investment sub-scores¶
Each incident is distinctly mapped to one or more characteristics such as:
- Environmental, Social, Governance, or Cross-cutting
- 28 RepRisk Issues
- over 73 topics
- UNGC, SASB, and SDGs
Therefore, it is possible to build customized risk incident sub-scores based on individual or a combination of characteristics.
The example below uses Volkswagen for showing the Investment scores for the All Issues, Environmental, Social, Governance, and Cross-cutting characteristics.
Notes:
- Since the incidents are not exclusively related to one category, the sub-scores may not add up to the overall score.
- The overall Investment score represents an upper bound (i.e., the sub-scores cannot exceed the overall score).
# calculate and plot Investment sub-scores for Volkswagen AG
categ = ["ENVIRONMENT", "SOCIAL", "GOVERNANCE", "CROSS_CUTTING"]
# Investment sub-scores
name, id_ = "All Issues", 370
colors = ['#B8B8B8', "#003755", "#2E91E5", "#FBC82D", "#449847"]
fig = go.Figure()
series = get_raw_scores(id_, incident_data)
series = max_decay(scale(series))
fig.add_trace(go.Scatter(x=series.index, y=series.values, name=name,
line=dict(color='#EB0C0C')))
for subscore in categ:
series = get_raw_scores(id_, incident_data[incident_data[subscore] == 'T'])
series = max_decay(scale(series))
fig.add_trace(go.Scatter(x=series.index, y=series.values, name=subscore,
line=dict(color=colors.pop())))
fig.update_traces(hovertemplate='Investment score: %{y}')
fig.update_layout(font=dict(color='#231F20', family='UnitWeb, UnitOT, Verdana'),
xaxis_rangeslider_visible=True,
legend=dict(title=dict(text=" ", side='top'), x=0, y=1.1,
orientation="h", bgcolor='rgba(0,0,0,0)'),
legend_font_color="#231F20",
template="plotly_white",
margin=dict(l=20, r=20, b=10),
height=500,
title=dict(text='<b>Volkswagen AG: Investment Scores</b>',
y=0.99,
x=0,
xanchor='left',
yanchor='top'),
title_font_family="UnitWeb, UnitOT, Verdana",
title_font_color="#231F20",
title_font_size=18)
fig.show()
8. UNGC investment scores¶
The example below uses Volkswagen for showing the Investment scores for all ten UNGC Principles both overall and individually.
Notes:
- Since the incidents are not exclusively related to one of the UNGC Principles, the sub-scores may not add up to the overall score.
- The overall Investment score (All Issues) represents an upper bound (i.e., the sub-scores cannot exceed the overall score).
# calculate and plot UNGC sub-scores for Volkswagen AG
ungcprinciples = ['UNGC Principle 1 (Human Rights)', 'UNGC Principle 2 (Human Rights)',
'UNGC Principle 3 (Labour)', 'UNGC Principle 4 (Labour)',
'UNGC Principle 5 (Labour)', 'UNGC Principle 6 (Labour)',
'UNGC Principle 7 (Environment)', 'UNGC Principle 8 (Environment)',
'UNGC Principle 9 (Environment)', 'UNGC Principle 10 (Anti-corruption)']
# UNGC sub-scores
name, id_ = "UNGC (ALL)", 370
colors = px.colors.qualitative.Dark24*2
fig = go.Figure()
# UNGC (ALL)
series = get_raw_scores(id_, incident_data[incident_data.filter(regex='UNGC').replace({'T': 1, 'F': 0}).any(axis=1)])
series = max_decay(scale(series))
fig.add_trace(go.Scatter(x=series.index, y=series.values, name=name,
line=dict(color='#A60808')))
# All Issues score
series = get_raw_scores(id_, incident_data)
series = max_decay(scale(series))
fig.add_trace(go.Scatter(x=series.index, y=series.values, name="All Issues",
line=dict(color='#EB0C0C')))
for subscore in ungcprinciples:
series = get_raw_scores(id_, incident_data[incident_data[subscore] == 'T'])
series = max_decay(scale(series))
fig.add_trace(go.Scatter(x=series.index, y=series.values, name=subscore,
line=dict(color=colors.pop())))
fig.update_traces(hovertemplate='Investment score: %{y}')
fig.update_layout(font=dict(color='#231F20', family='UnitWeb, UnitOT, Verdana'),
xaxis_rangeslider_visible=True,
legend=dict(title=dict(text=" ", side='top'), x=0, y=0.99,
orientation="h", bgcolor='rgba(0,0,0,0)'),
template="plotly_white",
margin=dict(l=20, r=20, b=10),
height=530,
title=dict(text='<b>Volkswagen AG: UNGC Scores</b>',
y=0.99, x=0,
xanchor='left',
yanchor='top',
font_family="UnitWeb, UnitOT, Verdana",
font_color="#231F20",
font_size=18))
fig.show()
9. SASB investment scores¶
The example below uses Volkswagen for showing the Investment scores for all 21 SASB Issues both overall and individually.
Notes:
- Since the incidents are not exclusively related to one of the SASB Issues, the sub-scores may not add up to the overall score.
- The overall Investment score (All Issues) represents an upper bound (i.e., the sub-scores cannot exceed the overall score).
# calculate and plot SASB sub-scores for Volkswagen AG
sasbissues = ['SASB Issue (GHG Emissions)', 'SASB Issue (Air Quality)',
'SASB Issue (Energy Management)', 'SASB Issue (Water & Wastewater Management)',
'SASB Issue (Waste & Hazardous Materials Management)', 'SASB Issue (Ecological Impacts)',
'SASB Issue (Human Rights & Community Relations)', 'SASB Issue (Customer Privacy)',
'SASB Issue (Data Security)', 'SASB Issue (Access & Affordability)',
'SASB Issue (Product Quality & Safety)', 'SASB Issue (Customer Welfare)',
'SASB Issue (Selling Practices & Product Labeling)', 'SASB Issue (Labor Practices)',
'SASB Issue (Employee Health & Safety)', 'SASB Issue (Employee Engagement, Diversity & Inclusion)',
'SASB Issue (Product Design & Lifecycle Management)', 'SASB Issue (Supply Chain Management)',
'SASB Issue (Business Ethics)', 'SASB Issue (Competitive Behavior)',
'SASB Issue (Management of the Legal & Regulatory Environment)']
# SASB sub-scores
name, id_ = "SASB (ALL)", 370
colors = px.colors.qualitative.Dark24*2
fig = go.Figure()
# SASB (ALL)
series = get_raw_scores(id_, incident_data[incident_data.filter(regex='SASB I').replace({'T': 1, 'F': 0}).any(axis=1)])
series = max_decay(scale(series))
fig.add_trace(go.Scatter(x=series.index, y=series.values, name=name,
line=dict(color='#A60808')))
# All Issues score
series = get_raw_scores(id_, incident_data)
series = max_decay(scale(series))
fig.add_trace(go.Scatter(x=series.index, y=series.values, name="All Issues",
line=dict(color='#EB0C0C')))
for subscore in sasbissues:
series = get_raw_scores(id_, incident_data[incident_data[subscore] == 'T'])
series = max_decay(scale(series))
fig.add_trace(go.Scatter(x=series.index, y=series.values, name=subscore,
line=dict(color=colors.pop())))
fig.update_traces(hovertemplate='Investment score: %{y}')
fig.update_layout(font=dict(color='#231F20', family='UnitWeb, UnitOT, Verdana'),
xaxis_rangeslider_visible=True,
legend=dict(title=dict(text=" ", side='top'), x=0, y=0.99,
orientation="h", bgcolor='rgba(0,0,0,0)'),
template="plotly_white",
margin=dict(l=20, r=20, b=10),
height=600,
title=dict(text='<b>Volkswagen AG: SASB Scores</b>',
y=0.99, x=0,
xanchor='left',
yanchor='top',
font_family="UnitWeb, UnitOT, Verdana",
font_color="#231F20",
font_size=18))
fig.show()
10. A simple trading strategy based on crossovers of Moving Averages (MA)¶
Trading strategy based on reputational risk incidents¶
Asset managers use several factors to slice and dice market and company information. The Investment scores can provide an additional dimension.
The code below calculates the MA for the scaled Investment scores.
from plotly.subplots import make_subplots
def adjust_trading_signal(score_step, date_range):
signal_adj = dict()
prev_value = False
date = score_step.index.min()
while date <= score_step.index.max():
value = (score_step == score_step.max())[date]
if value != prev_value:
# trading signal: roll date over to a business day
date = pd.offsets.BusinessDay().rollforward(date)
if value == True:
# buy signal
# shift by one day (to when the daily return will count/be considered)
date += pd.Timedelta(days=1)
# roll date over to a business day
date = pd.offsets.BusinessDay().rollforward(date)
# log the signal
signal_adj[date], prev_value = True, True
prev_value = True
date += pd.Timedelta(days=1)
else:
# sell signal
# shift by one day
date += pd.Timedelta(days=1)
# roll date over to a business day
date = pd.offsets.BusinessDay().rollforward(date)
# log the signal
signal_adj[date], prev_value = False, False
date += pd.Timedelta(days=1)
else:
# no trading signal
signal_adj[date] = prev_value
date += pd.Timedelta(days=1)
return pd.Series(signal_adj).reindex(date_range).ffill().fillna(False)
def get_highlow(pulse):
margin = 0.40
high = pulse.max()
low = pulse.min()
range_ = high - low
high += -margin*range_
low += margin*range_
return {"high": high, "low": low}
def getbinarysignal(fastmoving, slowmoving, pulse):
step = fastmoving - slowmoving
step = step.where(step <= 0, get_highlow(pulse)["low"])
step = step.where(step > 0, get_highlow(pulse)["high"])
return step
def button(i, name, annotation, n_traces, n_dropdowns):
return dict(label=name,
method="update",
args=[{"visible": [False]*n_traces*i + [True]*n_traces + [False]*n_traces*(n_dropdowns-1-i)},
{"title": None,
"annotations": [annotation]}])
def calculate_sharpe_ratio(daily_returns):
daily_returns = daily_returns.dropna()
try:
return (daily_returns.mean()/daily_returns.std())*(252**0.5)
except ZeroDivisionError:
return 0
def sharpe_ratio_annotation(name, sr_price, sr_invstrategy):
return dict(
x=1,
y=1.188,
showarrow=False,
text="<b>Sharpe Ratios</b><br>Stock Price: {:.2f}<br>{strategy_name}Strategy: {:.2f}".format(
sr_price, sr_invstrategy, strategy_name=name),
xref="paper",
yref="paper",
align="left",
font=dict(size=14))
line_specs = {"Stock Price": dict(
color='black'),
"All Issues Investment Score": dict(
color='#EB0C0C', width=1),
"G Investment Score": dict(
color='#EB0C0C', width=1),
"Moving Average Short": dict(
color='grey', dash='dot'),
"Moving Average Long": dict(
color='grey'),
"Trading Signal": dict(
color='black', width=0.5),
"All Issues Strategy": dict(
color="#0A9C02"),
"G Strategy": dict(
color="#0A9C02"),
"UNGC Strategy": dict(
color="#0A9C02"),
"SASB Strategy": dict(
color="#0A9C02"),
"UNGC Investment Score (ALL)": dict(
color="#EB0C0C", width=1),
"SASB Investment Score (ALL)": dict(
color="#EB0C0C", width=1)}
tickers = companies_table["Tickers"]
inv_options = {"All Issues Strategy": ["ENVIRONMENT", "SOCIAL", "GOVERNANCE", "CROSS_CUTTING"],
"G Strategy": ["GOVERNANCE"]}
ungcsasb_options = {"UNGC Strategy": ungcprinciples,
"SASB Strategy": sasbissues}
truefalse = {'T': 1, 'F': 0}
incident_data[["ENVIRONMENT", "SOCIAL", "GOVERNANCE", "CROSS_CUTTING"]] = incident_data[["ENVIRONMENT", "SOCIAL",
"GOVERNANCE", "CROSS_CUTTING"]].replace(truefalse)
incident_data[ungcprinciples] = incident_data[ungcprinciples].replace(truefalse).astype('bool')
incident_data[sasbissues] = incident_data[sasbissues].replace(truefalse).astype('bool')
# plot 1
ma_strategy, day_tuple = "12m/24m", (365, 2*365)
fig = make_subplots(rows=2, cols=1, start_cell="top-left",
shared_xaxes=True, shared_yaxes=False,
vertical_spacing=0.03)
visible = True
counter = 0
sharpe_ratio_annotations = []
dropdown_options = dict()
for issue in categ:
incident_data[issue] = incident_data[issue].astype('bool')
for name, id_ in companies.items():
counter += 1
if counter > 100:
break
# fetch stock prices
ticker = tickers[name]
try:
prices = pd.read_csv("ticker_data/{}_stockprice.csv".format(name), index_col=0)
prices = prices['Adj Close']
prices.index = pd.to_datetime(prices.index)
prices.name = "Stock Price"
daily_returns = prices.pct_change()
except Exception:
continue
for strategy_name, inv_list in inv_options.items():
series = get_raw_scores(id_, incident_data[incident_data[inv_list].any(axis=1)])
series = max_decay(scale(series))
series.name = strategy_name.replace('Strategy', 'Investment Score')
ma_short = series.rolling(day_tuple[0], min_periods=0).mean()
ma_short.name = "Moving Average Short"
ma_long = series.rolling(day_tuple[1], min_periods=0).mean()
ma_long.name = "Moving Average Long"
score_step = getbinarysignal(ma_short, ma_long, series)
score_step.name = "Trading Signal"
cumreturns_stock = (daily_returns + 1).cumprod()
# sharpe ratios
sharpe_ratio_price = calculate_sharpe_ratio(daily_returns)
adj_tradingsignal = adjust_trading_signal(score_step, date_range)
strategy_daily_returns = daily_returns.where(adj_tradingsignal, 0)
sharpe_ratio_invstrategy = calculate_sharpe_ratio(strategy_daily_returns)
sharpe_ratio_annotations.append(sharpe_ratio_annotation(strategy_name.replace('Strategy', ''),
sharpe_ratio_price, sharpe_ratio_invstrategy))
dropdown_options[name + " - " + ma_strategy + " - " + strategy_name] = id_
# investment strategy
cumreturns = (strategy_daily_returns + 1)
cumreturns = cumreturns.where(-adj_tradingsignal.diff().dropna().astype(bool),
cumreturns-0.0005) #transaction costs: 5bps
cumreturns = cumreturns.cumprod()
cumreturns.name = strategy_name
traces = [cumreturns_stock, cumreturns, score_step, ma_long, ma_short, series]
for trace in traces:
if trace.name in ["Stock Price", "All Issues Strategy", "G Strategy"]:
row = 1
legendgrp = "Cumulative returns"
else:
row = 2
legendgrp = "Signal"
fig.add_trace(
go.Scatter(x=trace.index, y=trace, name=trace.name, line=line_specs[trace.name], visible=visible,
legendgroup=legendgrp),
row=row, col=1
)
visible = False
buttons = []
for i, name in enumerate(dropdown_options.keys()):
buttons.append(button(i, name, sharpe_ratio_annotations[i], n_traces=6, n_dropdowns=len(sharpe_ratio_annotations)))
fig.update_layout(
updatemenus=[
dict(
y=1.3,
x=0,
yanchor="top",
xanchor="left",
active=0,
buttons=list(buttons)
)
])
fig.update_yaxes(type="log", row=1, col=1, secondary_y=False)
fig.update_layout(font=dict(color='#231F20', family='UnitWeb, UnitOT, Verdana'),
title_text=None,
xaxis2_rangeslider_visible=True,
legend=dict(x=0, y=1.2),
legend_orientation="h",
template="plotly_white",
margin=dict(l=20, r=20, t=90, b=0),
height=550)
fig.update_traces(hovertemplate="<b>%{y}</b><br>" +
"Date: %{x|%Y-%m-%d}")
fig1 = fig
# plot 2
ma_strategy, day_tuple = "12m/24m", (365, 2*365)
fig = make_subplots(rows=2, cols=1, start_cell="top-left",
shared_xaxes=True, shared_yaxes=False,
vertical_spacing=0.03)
visible = True
counter = 0
sharpe_ratio_annotations = []
dropdown_options = dict()
for name, id_ in companies.items():
counter += 1
if counter > 100:
break
# fetch stock prices
ticker = tickers[name]
try:
prices = pd.read_csv("ticker_data/{}_stockprice.csv".format(name), index_col=0)
prices = prices['Adj Close']
prices.index = pd.to_datetime(prices.index)
prices.name = "Stock Price"
daily_returns = prices.pct_change()
except Exception:
continue
for strategy_name, inv_list in ungcsasb_options.items():
series = get_raw_scores(id_, incident_data[incident_data[inv_list].any(axis=1)])
series = max_decay(scale(series))
series.name = strategy_name.replace("Strategy", "Investment Score (ALL)")
ma_short = series.rolling(day_tuple[0], min_periods=0).mean()
ma_short.name = "Moving Average Short"
ma_long = series.rolling(day_tuple[1], min_periods=0).mean()
ma_long.name = "Moving Average Long"
score_step = getbinarysignal(ma_short, ma_long, series)
score_step.name = "Trading Signal"
cumreturns_stock = (daily_returns + 1).cumprod()
# sharpe ratios
sharpe_ratio_price = calculate_sharpe_ratio(daily_returns)
adj_tradingsignal = adjust_trading_signal(score_step, date_range)
strategy_daily_returns = daily_returns.where(adj_tradingsignal, 0)
sharpe_ratio_invstrategy = calculate_sharpe_ratio(strategy_daily_returns)
sharpe_ratio_annotations.append(sharpe_ratio_annotation(strategy_name.replace('Strategy', ''),
sharpe_ratio_price, sharpe_ratio_invstrategy))
dropdown_options[name + " - " + ma_strategy + " - " + strategy_name] = id_
# investment strategy
cumreturns = (strategy_daily_returns + 1)
cumreturns = cumreturns.where(-adj_tradingsignal.diff().dropna().astype(bool),
cumreturns-0.0005) #transaction costs: 5bps
cumreturns = cumreturns.cumprod()
cumreturns.name = strategy_name
traces = [cumreturns_stock, cumreturns, score_step, ma_long, ma_short, series]
for trace in traces:
if trace.name in ["Stock Price", "UNGC Strategy", "SASB Strategy"]:
row = 1
legendgrp = "Cumulative returns"
else:
row = 2
legendgrp = "Signal"
fig.add_trace(
go.Scatter(x=trace.index, y=trace, name=trace.name, line=line_specs[trace.name], visible=visible,
legendgroup=legendgrp),
row=row, col=1
)
visible = False
buttons = []
for i, name in enumerate(dropdown_options.keys()):
buttons.append(button(i, name, sharpe_ratio_annotations[i], n_traces=6, n_dropdowns=len(sharpe_ratio_annotations)))
fig.update_layout(
updatemenus=[
dict(
y=1.3,
x=0,
yanchor="top",
xanchor="left",
active=0,
buttons=list(buttons)
)
])
fig.update_yaxes(type="log", row=1, col=1, secondary_y=False)
fig.update_layout(font=dict(color='#231F20', family='UnitWeb, UnitOT, Verdana'),
title_text=None,
xaxis2_rangeslider_visible=True,
legend=dict(x=0, y=1.2),
legend_orientation="h",
template="plotly_white",
margin=dict(l=20, r=20, t=90, b=0),
height=550)
fig.update_traces(hovertemplate="<b>%{y}</b><br>" +
"Date: %{x|%Y-%m-%d}")
fig2 = fig
10.1. Displayed graphs¶
For a simple demonstration, the crossover of MA can be used as a very basic trading signal. The graphs below show the following:
- Upper panel:
- the stock price of the selected company [black]
- the resulting Investment strategy when trading the signals of the lower panel [green]
- Lower panel:
- the incident scores of the selected company [green]
- the Long-term versus Short-term MA [bold versus dotted in grey]
- the trading signal to invest at the close of the next business day (transaction costs 5bps) [step function in grey]
fig1.show()