
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.
ASSET CLASS: stocks | REGION: United States | FREQUENCY:
Quarterly | MARKET: equities | KEYWORD: Growth Premium
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 Return | 4.14% |
| Volatility | 15.89% |
| Beta | -0.077 |
| Sharpe Ratio | 0.26 |
| Sortino Ratio | -0.12 |
| Maximum Drawdown | N/A |
| Win Rate | 53% |
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