Trade mutual funds by climate beta, going long on the lowest-beta quintile and short on the highest, using equally-weighted portfolios rebalanced monthly, excluding funds under $15 million in assets.

I. STRATEGY IN A NUTSHELL

The strategy sorts mutual funds by their climate beta—sensitivity to climate news innovations from Crimson Hexagon over 24 months—and constructs equally-weighted quintile portfolios. It goes long the lowest climate beta funds and short the highest, rebalancing monthly.

II. ECONOMIC RATIONALE

High climate beta funds contain stocks that benefit from rising climate awareness. These stocks earn higher returns due to better fundamentals and investor demand for climate-hedging exposure. The premium remains significant even after controlling for fund characteristics and style, showing that climate beta captures a unique return driver linked to climate-risk sensitivity.

III. SOURCE PAPER

Climate sensitivity and mutual fund performance [Click to Open PDF]

Thang Ho, University of Bradford School of Management

<Abstract>

In the presence of rising concern about climate change that potentially affects risk and return of investors’ portfolio companies, active investors might have dispersed climate risk exposures. We compute mutual fund covariance with market-wide climate change news index and find that high (positive) climate beta funds outperform low (negative) climate beta funds by 0.24% per month on a risk-adjusted basis. High climate beta funds tilt their holdings toward stocks with high potential to hedge against climate change. In the cross section, such stocks yield higher excess returns, which are driven by greater pricing pressure and superior financial performance over our sample period.

IV. BACKTEST PERFORMANCE

Annualised Return2.55%
Volatility2.27%
Beta-0.028
Sharpe Ratio1.12
Sortino RatioN/A
Maximum DrawdownN/A
Win Rate49%

V. FULL PYTHON CODE

import statsmodels.api as sm
from AlgorithmImports import *
from data_tools import CustomFeeModel, MutualFund, SymbolData, ClimateChangeData, ClimateChange
from typing import Dict, List
# endregion
class ClimateBetaAndMutualFunds(QCAlgorithm):
    def Initialize(self):
        self.SetStartDate(2004, 1, 1)
        self.SetCash(100000)
        
        self.leverage:int = 5
        self.period:int = 24
        self.quantile:int = 5
        self.max_missing_days:int = 5
        self.max_missing_days_climate_change:int = 40
        self.min_prices:int = 15
        self.long_only_flag:bool = False
        self.recent_month:int = -1
        self.data:Dict[Symbol, SymbolData] = {}
        self.climate_change:Symbol = self.AddData(ClimateChange, 'CLIMATE_CHANGE', Resolution.Daily).Symbol
        self.climate_change_data:ClimateChangeData = ClimateChangeData(self.period)
        self.symbol_count:int = 500
        ticker_file_str:str = self.Download('data.quantpedia.com/backtesting_data/equity/mutual_funds/500_mutual_funds_tickers.csv')
        ticker_file_str = ticker_file_str.replace('\r', '')
        self.tickers:List[str] = ticker_file_str.split('\n')[:self.symbol_count]
        for t in self.tickers:
            data = self.AddData(MutualFund, t, Resolution.Daily)
            data.SetFeeModel(CustomFeeModel())
            data.SetLeverage(self.leverage)
            self.data[data.Symbol] = SymbolData(self.period)
    def OnData(self, data: Slice) -> None:
        curr_date:datetime.date = self.Time.date()
        
        # store data
        for symbol, symbol_data in self.data.items():
            if symbol in data and data[symbol] and data[symbol].Value != 0:
                symbol_data.update_daily_prices(curr_date, data[symbol].Value)
        if self.climate_change in data and data[self.climate_change]:
            search_value:float = data[self.climate_change].Value
            self.climate_change_data.update(curr_date, search_value)
        # rebalance
        if self.recent_month == self.Time.month:
            return
        self.recent_month = self.Time.month
        # data is still comming in
        if not self.climate_change_data.data_still_coming(curr_date, self.max_missing_days_climate_change):
            self.climate_change_data.reset()
        x:List[float]|None = self.climate_change_data.get_monthly_changes() \
            if self.climate_change_data.is_ready() else None
        beta_values:Dict[Symbol, float] = {}
        # run regression
        for symbol, symbol_data in self.data.items():
            if not symbol_data.prices_still_coming(curr_date, self.max_missing_days):
                symbol_data.reset()
                continue
            if symbol_data.daily_prices_ready(self.min_prices):
                symbol_data.update_monthly_returns()
            if x != None and symbol_data.monthly_returns_ready() and symbol in data \
                and data[symbol] and data[symbol].Value != 0:
                monthly_returns:List[float] = symbol_data.get_monthly_returns()
                regression_model = self.MultipleLinearRegression(x, monthly_returns)
                beta:float = regression_model.params[1]
                beta_values[symbol] = beta
            symbol_data.reset_daily_prices()
        if len(beta_values) < self.quantile:
            self.Liquidate()
            return
        # sort by beta
        quantile:int = int(len(beta_values) / self.quantile)
        sorted_by_beta:List[Symbol] = [x[0] for x in sorted(beta_values.items(), key=lambda item: item[1])]
        short_leg:List[Symbol] = [] if self.long_only_flag else sorted_by_beta[-quantile:]
        long_leg:List[Symbol] = sorted_by_beta[:quantile]
        # trade execution
        invested:List[Symbol] = [x.Key for x in self.Portfolio if x.Value.Invested]
        for symbol in invested:
            if symbol not in long_leg+ short_leg:
                self.Liquidate(symbol)
        for symbol in long_leg:
            self.SetHoldings(symbol, 1 / quantile)
        for symbol in short_leg:
            self.SetHoldings(symbol, -1 / quantile)
    def MultipleLinearRegression(self, x:list, y:list):
        x:np.array = np.array(x).T
        x = sm.add_constant(x)
        result = sm.OLS(endog=y, exog=x).fit()
        return result

Leave a Reply

Discover more from Quant Buffet

Subscribe now to keep reading and get access to the full archive.

Continue reading