The strategy trades NYSE, Amex, and NASDAQ stocks by sorting top size-quintile stocks by Book-to-Market intensity, going long on the lowest and short on the highest, rebalancing quarterly.

I. STRATEGY IN A NUTSHELL

This strategy trades NYSE, Amex, and NASDAQ stocks, excluding financials and stocks under $1. Stocks are grouped by size, focusing on the largest quintile, and then sorted by Book-to-Market (B/M) intensity. The portfolio goes long on stocks with the lowest B/M intensity and short on those with the highest. Portfolios are value-weighted and rebalanced quarterly, systematically leveraging B/M ratio analysis to optimize returns.

II. ECONOMIC RATIONALE

B/M intensity provides distinct informational content, with low correlation to standard B/M or its volatility. By scaling B/M changes by historical volatility, this measure predicts future returns, showing strong negative relationships with medium-to-large stock returns and high correlation with past cumulative returns, offering a robust forecasting tool.

III. SOURCE PAPER

 Growth Stocks Are More Risky: New Evidence on Cross-Sectional Stock Returns [Click to Open PDF]

Jia, Central University of Finance and Economics (CUFE) – Chinese Academy of Finance and Development; Yuecheng, Sun Yat-sen University (SYSU) – Lingnan (University) College

<Abstract>

The conventional wisdom argues that the growth stocks are riskier to earn a higher premium. However, the empirical evidence points out that the value stocks, which are classified based on the book-to-market ratio, tend to have a higher premium. To solve this tension, this paper decomposes the book-to-market ratio into two components, a trend component, and a temporary (innovation) component. Both economic interpretation and empirical results show that the temporary component has a strong negative relationship with future cross-sectional stock returns even after controlling for main return predictors including Book-to-Market ratio, while the trend component is positively associated with the value premium. Therefore, consistent with conventional wisdom, our results confirm that there is growth premium captured by the temporary component of the book-to-market ratio.

IV. BACKTEST PERFORMANCE

Annualised Return4.14%
Volatility15.89%
Beta-0.077
Sharpe Ratio 0.26
Sortino Ratio-0.12
Maximum DrawdownN/A
Win Rate53%

V. FULL PYTHON CODE

import numpy as np
from AlgorithmImports import *
from numpy import isnan
class UsingIntensityofBooktoMarkettoIdentifyGrowthPremium(QCAlgorithm):
    def Initialize(self):
        self.SetStartDate(2000, 1, 1)
        self.SetCash(100000)
        self.rebalance_month:int = 4
        self.quantile:int = 5
        self.leverage:int = 5
        self.exchange_codes:List[str] = ['NYS', 'NAS', 'ASE']
        
        market:Symbol = self.AddEquity('SPY', Resolution.Daily).Symbol
        self.weight:Dict[Symbol, float] = {}
        self.bm_data:Dict[Symbol, RollingWindow] = {}
        self.bm_period:int = 8
        self.fundamental_count:int = 3000
        self.fundamental_sorting_key = lambda x: x.MarketCap
        self.month:int = 12
        self.selection_flag:bool = False
        self.UniverseSettings.Resolution = Resolution.Daily
        self.Settings.MinimumOrderMarginPortfolioPercentage = 0.
        self.AddUniverse(self.FundamentalSelectionFunction)
        self.Schedule.On(self.DateRules.MonthStart(market), self.TimeRules.AfterMarketOpen(market), 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.Market == 'usa' and x.SecurityReference.ExchangeId in self.exchange_codes and \
            not isnan(x.ValuationRatios.PBRatio) and x.ValuationRatios.PBRatio != 0 and not isnan(x.MarketCap) and x.MarketCap != 0
            ]
        if len(selected) > self.fundamental_count:
            selected = [x for x in sorted(selected, key=self.fundamental_sorting_key, reverse=True)[:self.fundamental_count]]
        # BM intensity.
        bm_intensity:Dict[Fundamental, float] = {}
        for stock in selected:
            symbol:Symbol = stock.Symbol
            # BM ratio calc
            if symbol not in self.bm_data:
                self.bm_data[symbol] = RollingWindow[float](self.bm_period)
            
            bm:float = 1. / stock.ValuationRatios.PBRatio
            
            if self.bm_data[symbol].IsReady:
                # Intensity calc.
                bms:List[float] = list(self.bm_data[symbol])
                avg_bm:float = np.mean(bms)
                std_bm:float = np.std(bms)
                intensity:float = (bm - avg_bm) / std_bm
                
                bm_intensity[stock] = intensity
                
            self.bm_data[symbol].Add(bm)
        
        if len(bm_intensity) >= self.quantile ** 2:
            # Market cap sorting
            sorted_by_market_cap = sorted(bm_intensity.items(), key = lambda x: x[0].MarketCap, reverse = True)
            quantile:int = int(len(sorted_by_market_cap) / self.quantile)
            top_by_market_cap:List = [x for x in sorted_by_market_cap[:quantile]]
            # Intensity sorting
            sorted_by_intesity:List = sorted(top_by_market_cap, key = lambda x: x[1], reverse = True)
            quantile = int(len(sorted_by_intesity) / self.quantile)
            short:List[Fundamental] = [x[0] for x in sorted_by_intesity[:quantile]]
            long:List[Fundamental] = [x[0] for x in sorted_by_intesity[-quantile:]]
            # Market cap weighting.
            for i, portfolio in enumerate([long, short]):
                mc_sum:float = sum(map(lambda x: x.MarketCap, portfolio))
                for stock in portfolio:
                    self.weight[stock.Symbol] = ((-1) ** i) * stock.MarketCap / mc_sum
        return list(self.weight.keys())
    def OnData(self, data: Slice) -> None:
        if not self.selection_flag:
            return
        self.selection_flag = False
        
        # Trade execution.
        portfolio:List[PortfolioTarget] = [PortfolioTarget(symbol, w) for symbol, w in self.weight.items() if symbol in data and data[symbol]]
        self.SetHoldings(portfolio, True)
        self.weight.clear()
    def Selection(self) -> None:
        self.selection_flag = True
        if self.month % 3 == 0:
            self.selection_flag = True
    
        self.month += 1
        if self.month > 12:
            self.month = 1
# Custom fee model.
class CustomFeeModel(FeeModel):
    def GetOrderFee(self, parameters):
        fee = parameters.Security.Price * parameters.Order.AbsoluteQuantity * 0.00005
        return OrderFee(CashAmount(fee, "USD"))

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