Quant BuffetRelax, Not Over Thinking

Small Industry Premia

Log in to collect

Academic paper

Strategy in a nutshell

This strategy trades 936 local supersector indices across 51 countries, covering 19 industries. Indices are weighted based on cross-sectional rank relative to mean market value (MV) to form a long-short, zero-investment portfolio. Weights are scaled to maintain dollar-long and dollar-short balance, and the portfolio is rebalanced monthly. Returns are calculated as the weighted sum of individual indices, systematically leveraging cross-sectional rankings for risk-neutral, globally diversified exposure.

Economic rationale

Return premia in small sectors and markets arise from behavioral mispricing or non-market risk factors. Market size is decomposed into momentum, long-run reversal, equity issuance, and lagged market capitalization. These low-correlation components complement the size factor, improving expected return modeling and capturing variations across industries for robust raw and risk-adjusted returns.

Backtest performance

Annualised return14.03%
Volatility12.09%
Beta0.784
Sharpe ratio0.83
Sortino ratio0.14
Win rate70%

Full Python code

from AlgorithmImports import *
import data_tools
# endregion
class SmallIndustryPremia(QCAlgorithm):
def Initialize(self) -> None:
self.SetStartDate(2000, 1, 1)
self.SetCash(100000)

self.min_share_price:int = 5
self.period:int = 60
self.leverage:int = 5
self.weights:dict = {}
self.data:dict = {}
self.prev_industry_country_ids:list[str] = []
market_symbol:Symbol = self.AddEquity('SPY', Resolution.Daily).Symbol
self.selection_flag:bool = False
self.Settings.MinimumOrderMarginPortfolioPercentage = 0.
self.UniverseSettings.Resolution = Resolution.Daily
self.AddUniverse(self.FundamentalSelectionFunction)
self.Schedule.On(self.DateRules.MonthStart(market_symbol), self.TimeRules.BeforeMarketClose(market_symbol), self.Selection)
self.settings.daily_precise_end_time = False
def OnSecuritiesChanged(self, changes: SecurityChanges) -> None:
for security in changes.AddedSecurities:
    security.SetFeeModel(CustomFeeModel())
    security.SetLeverage(self.leverage)
def FundamentalSelectionFunction(self, fundamental: List[Fundamental]) -> List[Symbol]:
if not self.selection_flag:
    return Universe.Unchanged

selected:list[Fundamental] = [x for x in fundamental if x.HasFundamentalData and x.Price >= self.min_share_price]
stocks_by_industry_id:dict = {}
for stock in selected:
    market_cap:float = stock.MarketCap
    country_id:str = stock.CompanyReference.BusinessCountryID
    sector:str = str(stock.AssetClassification.MorningstarSectorCode)
    if not country_id or not sector or market_cap == 0:
        continue
    
    industry_id:str = country_id + sector
    if industry_id not in stocks_by_industry_id:
        stocks_by_industry_id[industry_id] = []
    stocks_by_industry_id[industry_id].append(stock)

# store not adjusted MV value
for industry_id, stocks in stocks_by_industry_id.items():
    MV_value:float = np.mean(list(map(lambda stock: stock.MarketCap, stocks)))
    if industry_id not in self.data or industry_id not in self.prev_industry_ids: # require consecutive data
        self.data[industry_id] = data_tools.IndustryData(self.period)
    self.data[industry_id].update_MV_values(MV_value)
MV_by_industry_id:dict = { industry_id: industry_obj.get_MV() \
     for industry_id, industry_obj in self.data.items() if industry_obj.is_ready() }
mean_MV:float = np.mean(list(MV_by_industry_id.values()))
# MV difference from mean
long_mean_diff:dict = { industry_id : MV - mean_MV for industry_id, MV in MV_by_industry_id.items() if MV > mean_MV }
short_mean_diff:dict = { industry_id : MV - mean_MV for industry_id, MV in MV_by_industry_id.items() if MV < mean_MV }
total_mean_diff_long:float = abs(sum(list(long_mean_diff.values())))
total_mean_diff_short:float = abs(sum(list(short_mean_diff.values())))

# calculate weight for each stock in long and short leg
for diff_dict in [long_mean_diff, short_mean_diff]:
    
    # calculate weights that are proportional to distance from the mean
    total_mean_diff:float = abs(sum(list(diff_dict.values())))
    
    for industry_id, MV_mean_diff in long_mean_diff.items():
        if industry_id in stocks_by_industry_id:
            ind_weight:float = MV_mean_diff / total_mean_diff
            ind_stocks:list = stocks_by_industry_id[industry_id]
            ind_stock_count:float = len(ind_stocks)
            for stock in ind_stocks:
                self.weights[stock.Symbol] = ind_weight / ind_stock_count
self.prev_industry_ids = list(stocks_by_industry_id.keys())
return list(self.weights.keys())

def OnData(self, data: Slice) -> None:
# rebalance monthly
if not self.selection_flag:
    return
self.selection_flag = False

# trade execution
portfolio:List[PortfolioTarget] = [PortfolioTarget(symbol, w) for symbol, w in self.weights.items() if symbol in data and data[symbol]]
self.SetHoldings(portfolio, True)
self.weights.clear()

def Selection(self) -> None:
self.selection_flag = True

# Custom fee model
class CustomFeeModel(FeeModel):
def GetOrderFee(self, parameters):
fee = parameters.Security.Price * parameters.Order.AbsoluteQuantity * 0.00005
return OrderFee(CashAmount(fee, "USD"))