| Overall Statistics |
|
Total Orders
23521
Average Win
0.03%
Average Loss
-0.02%
Compounding Annual Return
-0.562%
Drawdown
26.700%
Expectancy
-0.029
Start Equity
100000
End Equity
91726.43
Net Profit
-8.274%
Sharpe Ratio
-0.425
Sortino Ratio
-0.467
Probabilistic Sharpe Ratio
0.000%
Loss Rate
56%
Win Rate
44%
Profit-Loss Ratio
1.20
Alpha
-0.014
Beta
-0.051
Annual Standard Deviation
0.045
Annual Variance
0.002
Information Ratio
-0.721
Tracking Error
0.156
Treynor Ratio
0.37
Total Fees
$138.51
Estimated Strategy Capacity
$100000000.00
Lowest Capacity Asset
VLY R735QTJ8XC9X
Portfolio Turnover
0.56%
|
# 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 changes:
# - Universe consists of ~700 stocks with ESG score data.
#region imports
from AlgorithmImports import *
from numpy import floor
from typing import List, Dict
from dataclasses import dataclass
from decimal import *
#endregion
class ESGFactorInvestingStrategy(QCAlgorithm):
def Initialize(self) -> None:
self.SetStartDate(2009, 6, 1)
self.SetCash(100_000)
# Decile weighting.
# True - Value weighted
# False - Equally weighted
self.value_weighting: bool = True
# self.symbol: Symbol = self.AddEquity('SPY', Resolution.Daily).Symbol
self.esg_data: Data = self.AddData(ESGData, 'ESG', Resolution.Daily)
# All tickers from ESG database.
self.tickers: List[str] = []
self.ticker_deciles: Dict[str, float] = {}
self.holding_period: float = 12
self.leverage: int = 10
self.threshold: List[int] = [0.2, 0.8]
self.managed_queue: List[RebalanceQueueItem] = []
self.latest_price: Dict[Symbol, float] = {}
self.selection_flag: bool = False
self.UniverseSettings.Leverage = self.leverage
self.UniverseSettings.Resolution = Resolution.Daily
self.AddUniverse(self.FundamentalSelectionFunction)
self.settings.daily_precise_end_time = False
self.Settings.MinimumOrderMarginPortfolioPercentage = 0.
def OnSecuritiesChanged(self, changes: SecurityChanges) -> None:
for security in changes.AddedSecurities:
security.SetFeeModel(CustomFeeModel())
def FundamentalSelectionFunction(self, fundamental: List[Fundamental]) -> List[Symbol]:
if not self.selection_flag:
return Universe.Unchanged
self.latest_price.clear()
selected: List[Fundamental] = [
x for x in fundamental
if x.MarketCap != 0
and (x.Symbol.Value).lower() in self.tickers
]
for stock in selected:
symbol: Symbol = stock.Symbol
self.latest_price[symbol] = stock.AdjustedPrice
# Store symbol/market cap pair.
long: List[Fundamental] = [
x for x in selected 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] >= self.threshold[1])
]
short: List[Fundamental] = [
x for x in selected 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] <= self.threshold[0])
]
weights: List[Tuple[Symbol, float]] = []
# ew
if not self.value_weighting:
for i, portfolio in enumerate([long, short]):
for stock in portfolio:
w: float = self.Portfolio.TotalPortfolioValue / self.holding_period / len(portfolio)
weights.append((stock.Symbol, ((-1) ** i) * floor(w / self.latest_price[stock.Symbol])))
# vw
else:
for i, portfolio in enumerate([long, short]):
mc_sum: float = sum(list(map(lambda x: x.MarketCap, portfolio)))
for stock in portfolio:
w: float = self.Portfolio.TotalPortfolioValue / self.holding_period
weights.append((stock.Symbol, ((-1) ** i) * floor((w * (stock.MarketCap / mc_sum))) / self.latest_price[stock.Symbol]))
self.managed_queue.append(RebalanceQueueItem(weights))
self.ticker_deciles.clear()
return [x.Symbol for x in long + short]
def OnData(self, slice: Slice) -> None:
new_data_arrived: bool = False
custom_data_last_update_date: datetime.date = ESGData.get_last_update_date()
if self.esg_data.get_last_data() and self.time.date() > custom_data_last_update_date:
self.liquidate()
return
if slice.contains_key('ESG') and slice['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:-1]
# Store history for every ticker.
for ticker in self.tickers:
ticker_u: str = ticker.upper()
if ticker_u not in self.ticker_deciles:
self.ticker_deciles[ticker_u] = None
decile: float = 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: Union[None, RebalanceQueueItem] = 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: List[RebalanceQueueItem] = []
for symbol, quantity in item.symbol_q:
if abs(quantity) >= 1:
if slice.contains_key(symbol) and slice[symbol]:
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)
@dataclass
class RebalanceQueueItem():
# symbol/quantity collections
symbol_q: List[Tuple[Symbol, float]]
holding_period: int = 0
# ESG data.
class ESGData(PythonData):
_last_update_date:datetime.date = datetime(1,1,1).date()
@staticmethod
def get_last_update_date() -> datetime.date:
return ESGData._last_update_date
def __init__(self):
self.tickers = []
def GetSource(self, config: SubscriptionDataConfig, date: datetime, isLiveMode: bool) -> SubscriptionDataSource:
return SubscriptionDataSource("data.quantpedia.com/backtesting_data/economic/esg_deciles_data.csv", SubscriptionTransportMedium.RemoteFile, FileFormat.Csv)
def Reader(self, config: SubscriptionDataConfig, line: str, date: datetime, isLiveMode: bool) -> BaseData:
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])
if data.Time.date() > ESGData._last_update_date:
ESGData._last_update_date = data.Time.date()
return data
# Custom fee model.
class CustomFeeModel(FeeModel):
def GetOrderFee(self, parameters: OrderFeeParameters) -> OrderFee:
fee: float = parameters.Security.Price * parameters.Order.AbsoluteQuantity * 0.00005
return OrderFee(CashAmount(fee, "USD"))