Quant BuffetRelax, Not Over Thinking

ESG Exclusion Premium Factor

Log in to collect

Academic paper

The Expected Returns of ESG Excluded Stocks. The Case of Exclusions from Norway's Oil Fund

AuthorsErika Christie Berle; Wangwei He; Bernt Arne Ødegaard

Institute
  • NOUniversity of Stavanger
  • ?University of Stavanger - Business School
  • ?University of Stavanger - Business School, Students

Strategy in a nutshell

The strategy invests in “sin” or low-ESG-quality stocks excluded from ESG-focused portfolios, based on GPFG Ethical Council announcements. A long-only value-weighted portfolio is constructed, with stock weights proportional to market capitalization. Stocks are added the month after exclusion and removed if the exclusion is revoked. Performance is measured via alpha, hedged using Fama-French international factors, ensuring comparability with traditional benchmarks.

Economic rationale

The rationale is that low-ESG firms can deliver higher long-term returns. Exclusions by ESG-focused funds create shifts in expected returns, not merely short-term price pressure. The strategy captures this alpha by systematically holding these underappreciated companies.

Backtest performance

Annualised return9%
Volatility11.67%
Beta0.109
Sharpe ratio0.77
Win rate80%

Full Python code

from AlgorithmImports import *
from datetime import datetime
from data_tools import CustomFeeModel, QuantpediaESGExclusion, SymbolData
from scipy import stats
# endregion

class ESGExclusionPremiumFactor(QCAlgorithm):

def Initialize(self):
self.SetStartDate(2005, 1, 1) # ESG exclusion starts in 2005
self.SetCash(100000)

self.leverage:int = 5

self.period:int = 12 * 21

self.max_missing_days:int = 15

self.data:dict[Symbol, SymbolData] = {}
self.weights:dict[Symbol, float] = {}
self.excluded_stocks:list[str] = []

self.esg_exclusion_symbol:Symbol = self.AddData(QuantpediaESGExclusion, 'esg_exclusion', Resolution.Daily).Symbol

security = self.AddEquity('ACWI', Resolution.Daily)
security.SetFeeModel(CustomFeeModel())
security.SetLeverage(self.leverage)
self.market:Symbol = security.Symbol

self.data[self.market] = SymbolData(self.period)
history = self.History(self.market, self.period, Resolution.Daily)
if not history.empty:
    closes = history.loc[symbol].close

    for time, close in closes.iteritems():
        self.data[symbol].update(time.date(), close)

self.rebalance_flag:bool = False
self.selection_flag:bool = 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(self.leverage)

def CoarseSelectionFunction(self, coarse):
curr_date:datetime.date = self.Time.date()

for equity in coarse:
    symbol:Symbol = equity.Symbol

    if symbol in self.data:
        self.data[symbol].update(curr_date, equity.AdjustedPrice)

if not self.selection_flag or not self.data[self.market].is_ready():
    return Universe.Unchanged

if not self.data[self.market].data_still_coming(curr_date, self.max_missing_days):
    self.data[self.market].reset_data()
self.selection_flag = False

warmed_up_symbols:list[Symbol] = []
for equity in coarse:
    if not equity.Symbol.Value in self.excluded_stocks:
        continue

    symbol:Symbol = equity.Symbol

    if symbol not in self.data:                
        self.data[symbol] = SymbolData(self.period)
        history = self.History(symbol, self.period, Resolution.Daily)
        if history.empty:
            continue

        closes = history.loc[symbol].close

        for time, close in closes.iteritems():
            self.data[symbol].update(time.date(), close)

    if self.data[symbol].is_ready():
        warmed_up_symbols.append(symbol)

self.rebalance_flag = True

return warmed_up_symbols

def FineSelectionFunction(self, fine):
fine:list[FineFundamental] = list(filter(lambda stock: stock.MarketCap != 0, fine))

total_cap:float = sum(list(map(lambda stock: stock.MarketCap, fine)))

market_daily_returns:list[float] = self.data[self.market].get_daily_returns()
beta_values:list[float] = []

for stock in fine:
    symbol:Symbol = stock.Symbol

    stock_daily_returns:list[float] = self.data[stock.Symbol].get_daily_returns()

    slope, intercept, r_value, p_value, std_err = stats.linregress(market_daily_returns, stock_daily_returns)

    beta_values.append(slope)

    self.weights[symbol] = stock.MarketCap / total_cap

self.weights[self.market] = -np.average(beta_values)

return list(self.weights.keys())

def OnData(self, data: Slice):
if self.esg_exclusion_symbol in data and data[self.esg_exclusion_symbol]:
    self.selection_flag = True
    excluded_tickers:list[str] = [x for x in data[self.esg_exclusion_symbol].GetProperty('tickers')]
    self.excluded_stocks += excluded_tickers

if not self.rebalance_flag:
    return
self.rebalance_flag = False

invested:list[Symbol] = [x.Key for x in self.Portfolio if x.Value.Invested]
for symbol in invested:
    if symbol not in self.weights:
        self.Liquidate(symbol)
        
for symbol, w in self.weights.items():
    if symbol in data and data[symbol]:
        self.SetHoldings(symbol, w)

self.weights.clear()