Overall Statistics |
Total Orders
13669
Average Win
0.07%
Average Loss
-0.04%
Compounding Annual Return
13.700%
Drawdown
25.900%
Expectancy
0.733
Start Equity
100000
End Equity
715437.58
Net Profit
615.438%
Sharpe Ratio
0.691
Sortino Ratio
0.632
Probabilistic Sharpe Ratio
15.657%
Loss Rate
36%
Win Rate
64%
Profit-Loss Ratio
1.70
Alpha
0.023
Beta
0.658
Annual Standard Deviation
0.123
Annual Variance
0.015
Information Ratio
-0.091
Tracking Error
0.093
Treynor Ratio
0.129
Total Fees
$1174.64
Estimated Strategy Capacity
$70000000.00
Lowest Capacity Asset
ALG R735QTJ8XC9X
Portfolio Turnover
1.47%
|
# https://quantpedia.com/strategies/esg-factor-momentum-strategy/ # # The investment universe consists of stocks in the MSCI World Index. Paper uses MSCI ESG Ratings as the ESG database. # The ESG Momentum strategy is built by overweighting, relative to the MSCI World Index, companies that increased their # ESG ratings most during the recent past and underweight those with decreased ESG ratings, where the increases and decreases # are based on a 12-month ESG momentum. The paper uses the Barra Global Equity Model (GEM3) for portfolio construction with # constraints that can be found in Appendix 2. Therefore, this strategy is very specific, but we aim to present the idea, not # the portfolio construction. The strategy is rebalanced monthly. # # QC implementation changes: # - Universe consists of ~700 stocks with ESG score data. #region imports from AlgorithmImports import * from numpy import floor from collections import deque from typing import List, Dict, Tuple from dataclasses import dataclass from decimal import * #endregion class ESGFactorMomentumStrategy(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.esg_data: Security = self.AddData(ESGData, 'ESG', Resolution.Daily) self.tickers: List[str] = [] self.holding_period: int = 3 self.managed_queue: List[RebalanceQueueItem] = [] self.quantile: int = 10 self.leverage: int = 10 # Monthly ESG decile data. self.esg: Dict[str, RollingWindow[float]] = {} self.period: int = 14 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 momentum: Dict[Fundamental, float] = {} # Momentum calc. for stock in selected: symbol: Symbol = stock.Symbol ticker: str = symbol.Value # ESG data for 14 months is ready. if ticker in self.esg and self.esg[ticker].IsReady: esg_data: List[float] = [x for x in self.esg[ticker]] esg_decile_2_months_ago: float = esg_data[1] esg_decile_14_months_ago: float = esg_data[13] if esg_decile_14_months_ago != 0 and esg_decile_2_months_ago != 0: # Momentum as difference. # momentum_ = esg_decile_2_months_ago - esg_decile_14_months_ago # Momentum as ratio. momentum_: float = (esg_decile_2_months_ago / esg_decile_14_months_ago) - 1 # Store momentum/market cap pair. momentum[stock] = momentum_ if len(momentum) <= self.quantile: return Universe.Unchanged # Momentum sorting. sorted_by_momentum: List[Tuple[Fundamental, float]] = sorted(momentum.items(), key = lambda x: x[1], reverse = True) quantile: int = int(len(sorted_by_momentum) / self.quantile) long: List[Fundamental] = [x[0] for x in sorted_by_momentum[:quantile]] short: List[Fundamental] = [x[0] for x in sorted_by_momentum[-quantile:]] if len(long) == 0 or len(short) == 0: return Universe.Unchanged 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)) 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.esg: self.esg[ticker_u] = RollingWindow[float](self.period) decile: float = self.esg_data.GetLastData()[ticker] self.esg[ticker_u].Add(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 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"))