| Overall Statistics |
|
Total Trades
14305
Average Win
0.02%
Average Loss
-0.01%
Compounding Annual Return
9.815%
Drawdown
21.900%
Expectancy
2.153
Net Profit
250.993%
Sharpe Ratio
0.746
Probabilistic Sharpe Ratio
11.282%
Loss Rate
16%
Win Rate
84%
Profit-Loss Ratio
2.76
Alpha
0.008
Beta
0.635
Annual Standard Deviation
0.096
Annual Variance
0.009
Information Ratio
-0.475
Tracking Error
0.06
Treynor Ratio
0.113
Total Fees
$168.91
Estimated Strategy Capacity
$800000000.00
Lowest Capacity Asset
WY R735QTJ8XC9X
|
# https://quantpedia.com/strategies/esg-factor-investing-strategy/
#
# As we have previously mentioned, the choice of the database of ESG scores can alter results. This paper uses for the assessments of
# environment, social, and governance performance of single firms database provided by Asset4. Scores are updated every year, therefore
# to obtain monthly ESG data, the scores remain unchanged until the next assessment.
# The investment universe consists of stocks of the North America region (Canada and the United States) that have ESG scores available.
# Stocks with a price of less than one USD are excluded. Paper examines the returns as abnormal returns according to the methodology of
# Daniel et al. (1997). Such methodology controls for risk factors such as size, book-to-market ratio, and momentum. The idea is to match
# a stock along with the mentioned factors to a benchmark portfolio that contains stocks with similar characteristics. Therefore, for the
# North America region, we have 4×4 benchmark portfolios. The abnormal return is calculated as the return of stock minus the return of
# stock´s matching benchmark portfolio return (equation 1, page 13).
# Finally, each month stocks are ranked according to their E, S and G scores. Long top 20% stocks of each score and short the bottom 20%
# stocks of each score. Therefore, we have one complex strategy that consists of three individual strategies (for representative purposes,
# the paper examines each strategy individually). The strategy is equally-weighted: both stocks in the quintiles and individual strategies.
# The strategy is rebalanced yearly.
#
# QC implementation:
# - Universe consists of ~700 stocks with ESG score data.
#region imports
from AlgorithmImports import *
from numpy import floor
#endregion
class ESGFactorInvestingStrategy(QCAlgorithm):
def Initialize(self):
self.SetStartDate(2009, 6, 1)
self.SetCash(100000)
# Decile weighting.
# True - Value weighted
# False - Equally weighted
self.value_weighting = True
self.symbol = self.AddEquity('SPY', Resolution.Daily).Symbol
self.esg_data = self.AddData(ESGData, 'ESG', Resolution.Daily)
# All tickers from ESG database.
self.tickers = []
self.ticker_deciles = {}
self.holding_period = 12
self.managed_queue = []
self.latest_price = {}
self.selection_flag = False
self.UniverseSettings.Resolution = Resolution.Daily
self.AddUniverse(self.CoarseSelectionFunction, self.FineSelectionFunction)
def OnSecuritiesChanged(self, changes):
for security in changes.AddedSecurities:
security.SetFeeModel(CustomFeeModel())
security.SetLeverage(10)
def CoarseSelectionFunction(self, coarse):
if not self.selection_flag:
return Universe.Unchanged
self.latest_price.clear()
selected = [x for x in coarse if (x.Symbol.Value).lower() in self.tickers]
for stock in selected:
symbol = stock.Symbol
self.latest_price[symbol] = stock.AdjustedPrice
return [x.Symbol for x in selected]
def FineSelectionFunction(self, fine):
fine = [x for x in fine if x.MarketCap != 0]
# Store symbol/market cap pair.
long = [x for x in fine if (x.Symbol.Value in self.ticker_deciles) and \
(self.ticker_deciles[x.Symbol.Value] is not None) and \
(self.ticker_deciles[x.Symbol.Value] >= 0.8)]
short = [x for x in fine if (x.Symbol.Value in self.ticker_deciles) and \
(self.ticker_deciles[x.Symbol.Value] is not None) and \
(self.ticker_deciles[x.Symbol.Value] <= 0.2)]
long_symbol_q = []
short_symbol_q = []
# ew
if not self.value_weighting:
if len(long) != 0:
long_w = self.Portfolio.TotalPortfolioValue / self.holding_period / len(long)
long_symbol_q = [(x.Symbol, floor(long_w / self.latest_price[x.Symbol])) for x in long]
if len(short) != 0:
short_w = self.Portfolio.TotalPortfolioValue / self.holding_period / len(short)
short_symbol_q = [(x.Symbol, -floor(short_w / self.latest_price[x.Symbol])) for x in short]
# vw
else:
if len(long) != 0:
total_market_cap_long = sum([x.MarketCap for x in long])
long_w = self.Portfolio.TotalPortfolioValue / self.holding_period
long_symbol_q = [(x.Symbol, floor((long_w * (x.MarketCap / total_market_cap_long))) / self.latest_price[x.Symbol]) for x in long]
short_symbol_q = []
if len(short) != 0:
total_market_cap_short = sum([x.MarketCap for x in short])
short_w = self.Portfolio.TotalPortfolioValue / self.holding_period
short_symbol_q = [(x.Symbol, -floor((short_w * (x.MarketCap / total_market_cap_short))) / self.latest_price[x.Symbol]) for x in short]
self.managed_queue.append(RebalanceQueueItem(long_symbol_q + short_symbol_q))
self.ticker_deciles.clear()
return [x.Symbol for x in long + short]
def OnData(self, data):
new_data_arrived = False
if 'ESG' in data and data['ESG']:
# Store universe tickers.
if len(self.tickers) == 0:
# TODO '_typename' in storage dictionary?
self.tickers = [x.Key for x in self.esg_data.GetLastData().GetStorageDictionary()][:-1]
# Store history for every ticker.
for ticker in self.tickers:
ticker_u = ticker.upper()
if ticker_u not in self.ticker_deciles:
self.ticker_deciles[ticker_u] = None
decile = self.esg_data.GetLastData()[ticker]
self.ticker_deciles[ticker_u] = decile
# trigger selection after new esg data arrived.
if not self.selection_flag:
new_data_arrived = True
if new_data_arrived:
self.selection_flag = True
return
if not self.selection_flag:
return
self.selection_flag = False
# Trade execution
remove_item = None
# Rebalance portfolio
for item in self.managed_queue:
if item.holding_period == self.holding_period:
for symbol, quantity in item.symbol_q:
self.MarketOrder(symbol, -quantity)
remove_item = item
elif item.holding_period == 0:
open_symbol_q = []
for symbol, quantity in item.symbol_q:
if symbol in data and data[symbol]:
if quantity >= 1:
self.MarketOrder(symbol, quantity)
open_symbol_q.append((symbol, quantity))
# Only opened orders will be closed
item.symbol_q = open_symbol_q
item.holding_period += 1
if remove_item:
self.managed_queue.remove(remove_item)
class RebalanceQueueItem():
def __init__(self, symbol_q):
# symbol/quantity collections
self.symbol_q = symbol_q
self.holding_period = 0
# ESG data.
class ESGData(PythonData):
def __init__(self):
self.tickers = []
def GetSource(self, config, date, isLiveMode):
return SubscriptionDataSource("data.quantpedia.com/backtesting_data/economic/esg_deciles_data.csv", SubscriptionTransportMedium.RemoteFile, FileFormat.Csv)
def Reader(self, config, line, date, isLiveMode):
data = ESGData()
data.Symbol = config.Symbol
if not line[0].isdigit():
self.tickers = [x for x in line.split(';')][1:]
return None
split = line.split(';')
data.Time = datetime.strptime(split[0], "%Y-%m-%d") + timedelta(days=1)
index = 1
for ticker in self.tickers:
data[ticker] = float(split[index])
index += 1
data.Value = float(split[1])
return data
# Custom fee model.
class CustomFeeModel(FeeModel):
def GetOrderFee(self, parameters):
fee = parameters.Security.Price * parameters.Order.AbsoluteQuantity * 0.00005
return OrderFee(CashAmount(fee, "USD"))