
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.
ASSET CLASS: stocks | REGION: Global | FREQUENCY:
Monthly | MARKET: equities | KEYWORD: Beta
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 Return | 22.42% |
| Volatility | 19.87% |
| Beta | -0.033 |
| Sharpe Ratio | 1.13 |
| Sortino Ratio | N/A |
| Maximum Drawdown | N/A |
| Win Rate | 55% |
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