The strategy trades 24 developed markets, going long on high-SMB-return countries’ BAB strategies and short on low-SMB-return ones, using equal weighting and monthly rebalancing for performance optimization.

I. STRATEGY IN A NUTSHELL

Ranks 24 developed-market countries by past SMB returns and implements BAB (betting-against-beta) strategies: going long in high-SMB countries and short in low-SMB countries. Portfolios are equally weighted and rebalanced monthly to capture cross-country SMB differences.

II. ECONOMIC RATIONALE

Performance is driven by funding and asset liquidity: rising small-cap prices improve collateral for leveraged low-beta stocks, increasing demand and temporarily boosting returns. BAB profits are enhanced after positive SMB payoffs, while periods without SMB signals generate minimal alpha.

III. SOURCE PAPER

Small-Minus-Big Predicts Betting-Against-Beta: Implications for International Equity Allocation and Market Timing[Click to Open PDF]

Adam Zaremba, Montpellier Business School, Poznan University of Economics and Business, University of Cape Town (UCT)

<Abstract>

We demonstrate a strong relationship between short-term small-firm premium and future low-beta anomaly performance. Rises (declines) in small firm prices temporarily improve (deteriorate) funding conditions, benefiting (impairing) the short-run returns on the low-beta strategy. To investigate this phenomenon, we examine returns on betting-against-beta (BAB) and small-minus-big (SMB) factor portfolios in 24 developed markets for the years 1989–2018. A zero-investment strategy of going long (short) in BAB factors in the quintile of countries with the highest (lowest) three-month SMB return produces a mean return of 1.46% per month. The effect is robust to controlling for major risk factors in equity markets, alternative portfolio construction methods, and subperiod analysis. The predictability of BAB performance by SMB returns is also present in the time-series of individual country returns, forming the ground for effective timing in the low-beta strategies.

IV. BACKTEST PERFORMANCE

Annualised Return22.42%
Volatility19.87%
Beta-0.033
Sharpe Ratio1.13
Sortino RatioN/A
Maximum DrawdownN/A
Win Rate55%

V. FULL PYTHON CODE

from AlgorithmImports import *
import numpy as np
#endregion
class TimingBettingAgainstBetawithSmallStocks(QCAlgorithm):
    def Initialize(self):
        self.SetStartDate(2000, 1, 1)
        self.SetCash(100000)
        self.countries = [
                        "AUS", "AUT", "BEL", "CAN", "DNK", "FIN", "FRA", "DEU",
                        "GRC", "HKG", "IRL", "ISR","ITA","JPN","NLD","NZL","NOR",
                        "PRT","SGP","ESP","SWE","CHE","GBR","USA"
                    ]
        self.symbol:Symbol = self.AddEquity('SPY', Resolution.Daily).Symbol        
        self.smb_history:dict = {}
        self.quantile:int = 5
        self.max_missing_days:int = 5
        
        self.period:int = 3 * 21 # performance period.
        self.SetWarmUp(self.period, Resolution.Daily)
        self.smb_symbol:Symbol = self.AddData(SMB, 'SMB_percentage', Resolution.Daily).Symbol
        for country in self.countries:
            # BAB and SMB data.
            data = self.AddData(BAB, country + '_BAB', Resolution.Daily)
            data.SetLeverage(10)
            data.SetFeeModel(CustomFeeModel())
            
            self.smb_history[country] = RollingWindow[float](self.period)
        self.recent_month:int = -1
    def OnData(self, data:Slice) -> None:
        if self.smb_symbol in data and data[self.smb_symbol]:
            for country in self.countries:
                smb_value:float = data[self.smb_symbol].GetProperty(country)
                self.smb_history[country].Add(smb_value)
        # rebalance monthly
        if self.Time.month == self.recent_month:
            return
        self.recent_month = self.Time.month
        # SMB factor data is still comming in
        if self.Securities[self.smb_symbol].GetLastData() and (self.Time.date() - self.Securities[self.smb_symbol].GetLastData().Time.date()).days > self.max_missing_days:
            self.Liquidate()
            return
        # calculate average performance
        avg_perf:dict[str, float] = {}
        for country in self.countries:
            if self.smb_history[country].IsReady:
                # BAB factor data is still comming in
                if self.Securities[country + '_BAB'].GetLastData() and (self.Time.date() - self.Securities[country + '_BAB'].GetLastData().Time.date()).days > self.max_missing_days:
                    continue
                avg_perf[country] = np.average([x for x in self.smb_history[country]])
        
        if len(avg_perf) < self.quantile:
            self.Liquidate()
            return
        
        sorted_by_avg_perf:List = [x[0] for x in sorted(avg_perf.items(), key = lambda x: x[1], reverse = True)]
        quantile:int = int(len(sorted_by_avg_perf) / self.quantile)
        long_countries:List[str] = sorted_by_avg_perf[:quantile]
        short_countries:List[str] = sorted_by_avg_perf[-quantile:]
        
        invested:List[str] = [x.Key.Value for x in self.Portfolio if x.Value.Invested]
        for symbol in invested:
            if symbol not in long_countries + short_countries:
                self.Liquidate(self.traded_symbol(symbol))
        
        long_count:int = len(long_countries)
        short_count:int = len(short_countries)
        
        for country in long_countries:
            if self.traded_symbol(country) in data and data[self.traded_symbol(country)]:
                self.SetHoldings(self.traded_symbol(country), 1 / long_count)
        
        for country in short_countries:
            if self.traded_symbol(country) in data and data[self.traded_symbol(country)]:
                self.SetHoldings(self.traded_symbol(country), -1 / short_count)
    
    def traded_symbol(self, symbol:str) -> str:
        return symbol + '_BAB'
        
# Custom fee model
class CustomFeeModel(FeeModel):
    def GetOrderFee(self, parameters):
        fee = parameters.Security.Price * parameters.Order.AbsoluteQuantity * 0.00005
        return OrderFee(CashAmount(fee, "USD"))
        
# SMB factor.
# NOTE: IMPORTANT: Data order must be ascending (datewise).
# Data source: https://www.aqr.com/Insights/Datasets/Betting-Against-Beta-Equity-Factors-Daily
class SMB(PythonData):
    def GetSource(self, config, date, isLiveMode):
        return SubscriptionDataSource("data.quantpedia.com/backtesting_data/equity/smb_factor_percentage.csv", SubscriptionTransportMedium.RemoteFile, FileFormat.Csv)
    # File example.
    # DATE;AUS;AUT;BEL;CAN;CHE;DEU;DNK;ESP;FIN;FRA;GBR;GRC;HKG;IRL;ISR;ITA;JPN;NLD;NOR;NZL;PRT;SGP;SWE;USA
    # 09/30/2020;1.40;1.19;0.72;-0.22;0.84;1.05;0.31;1.26;0.67;0.83;1.12;-0.16;-0.47;0.16;-0.20;1.21;-0.07;0.26;0.23;-0.37;0.68;-0.84;0.43;-0.61
    def Reader(self, config, line, date, isLiveMode):
        data = SMB()
        data.Symbol = config.Symbol
        
        if not line[0].isdigit(): return None
        split = line.split(';')
        
        # Prevent look-ahead bias.
        data.Time = datetime.strptime(split[0], "%m/%d/%Y") + timedelta(days=1)
        
        data['AUS'] = float(split[1])
        data['AUT'] = float(split[2])
        data['BEL'] = float(split[3])
        data['CAN'] = float(split[4])
        data['CHE'] = float(split[5])
        data['DEU'] = float(split[6])
        data['DNK'] = float(split[7])
        data['ESP'] = float(split[8])
        data['FIN'] = float(split[9])
        data['FRA'] = float(split[10])
        data['GBR'] = float(split[11])
        data['GRC'] = float(split[12])
        data['HKG'] = float(split[13])
        data['IRL'] = float(split[14])
        data['ISR'] = float(split[15])
        data['ITA'] = float(split[16])
        data['JPN'] = float(split[17])
        data['NLD'] = float(split[18])
        data['NOR'] = float(split[19])
        data['NZL'] = float(split[20])
        data['PRT'] = float(split[21])
        data['SGP'] = float(split[22])
        data['SWE'] = float(split[23])
        data['USA'] = float(split[24])
        
        data.Value = float(split[1])
        return data
        
# BAB factor.
# NOTE: IMPORTANT: Data order must be ascending (datewise).
# Data source: https://www.aqr.com/Insights/Datasets/Betting-Against-Beta-Equity-Factors-Daily
class BAB(PythonData):
    def GetSource(self, config, date, isLiveMode):
        return SubscriptionDataSource(f"data.quantpedia.com/backtesting_data/equity/{config.Symbol.Value}.csv", SubscriptionTransportMedium.RemoteFile, FileFormat.Csv)
    # File example.
    # date;AUS
    # 09/30/2020;24.05738634
    def Reader(self, config, line, date, isLiveMode):
        data = BAB()
        data.Symbol = config.Symbol
        
        if not line[0].isdigit(): return None
        split = line.split(';')
        
        # Prevent look-ahead bias.
        data.Time = datetime.strptime(split[0], "%m/%d/%Y") + timedelta(days=1)
        data.Value = float(split[1])
        return data

VI. Backtest Performance

Leave a Reply

Discover more from Quant Buffet

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

Continue reading