Quant BuffetRelax, Not Over Thinking

Size and Investment Intersection Strategy

Log in to collect

Academic paper

A Four-Factor Model for the Size, Value, and Profitability Patterns in Stock Returns

AuthorsEugene F. Fama; Kenneth R. French

Institute
  • University of Chicago
  • ?University of Chicago - Finance
  • Dartmouth College
  • National Bureau of Economic Research
  • ?Dartmouth College - Tuck School of Business
  • ?National Bureau of Economic Research (NBER)

Strategy in a nutshell

The investment universe consists of all NYSE, Amex, and NASDAQ stocks. Firstly, stocks are allocated to five Size groups (Small to Big) at the end of each June using NYSE market cap breakpoints. Stocks are allocated independently to five Investment (Inv) groups (Low to High) still using NYSE breakpoints. The intersections of the two sorts produce 25 Size-Inv portfolios. For portfolios formed in June of year t, Inv is the growth of total assets for the fiscal year ending in t-1 divided by total assets at the end of t-1. Long portfolio with the highest Size and simultaneously with the lowest Investment. Short portfolio with the highest Size and simultaneously with the highest Investment. The portfolios are value-weighted.

Economic rationale

Past data and research proved that a conservative investment portfolio is connected with greater returns compared to an aggressive investment portfolio. To explain it briefly, aggressive investments do not improve returns in the near future, and yet it is questioned if those investments would improve returns in the future. Moreover, looking at the Size-Inv portfolios, estimates suggest that the five-factor model leaves only around 28% of the cross-section variance of expected returns unexplained. This is far less than the variance ratios produced by the Fama-French three-factor model, which are mostly greater than 50% for the Size-Inv portfolio. As a result, the investment factor could and should be used by investors.

Backtest performance

Annualised return3.54%
Beta0.034
Sharpe ratio0.363
Sortino ratio0.385
Maximum drawdown18.9%
Win rate52%

Full Python code

from AlgoLib import *

class InvestmentFactor(XXX):

def Initialize(self):
self.SetStartDate(2000, 1, 1)
self.SetCash(100000)

self.symbol:Symbol = self.AddEquity('SPY', Resolution.Daily).Symbol

self.fundamental_count:int = 3000
self.fundamental_sorting_key = lambda x: x.MarketCap

self.long:List[Symbol] = []
self.short:List[Symbol] = []

self.quantile:int = 5
self.leverage:int = 3
self.rebalance_month:int = 6
self.min_share_price:float = 1.

self.weight:Dict[Symbol, float] = {}

self.selection_flag:bool = False
self.UniverseSettings.Resolution = Resolution.Daily
self.AddUniverse(self.FundamentalSelectionFunction)
self.Settings.MinimumOrderMarginPortfolioPercentage = 0
self.Schedule.On(self.DateRules.MonthEnd(self.symbol), self.TimeRules.AfterMarketOpen(self.symbol), self.Selection)

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.MarketCap != 0 and x.AdjustedPrice >= self.min_share_price and \
    ((x.SecurityReference.ExchangeId == "NYS") or (x.SecurityReference.ExchangeId == "NAS") or (x.SecurityReference.ExchangeId == "ASE")) and \
    not np.isnan(x.FinancialStatements.BalanceSheet.TotalAssets.TwelveMonths) and not np.isnan(x.OperationRatios.TotalAssetsGrowth.OneYear) and \
    x.FinancialStatements.BalanceSheet.TotalAssets.TwelveMonths != 0 and x.OperationRatios.TotalAssetsGrowth.OneYear != 0
    ]

if len(selected) > self.fundamental_count:
    selected = [x for x in sorted(selected, key=self.fundamental_sorting_key, reverse=True)[:self.fundamental_count]]

# Sorting by investment factor.
sorted_by_inv_factor:List[Fundamental] = sorted(selected, key = lambda x: (x.OperationRatios.TotalAssetsGrowth.OneYear / x.FinancialStatements.BalanceSheet.TotalAssets.TwelveMonths), reverse=True)

if len(sorted_by_inv_factor) >= self.quantile:
    quintile:int = int(len(sorted_by_inv_factor) / self.quantile)
    self.long = [x.Symbol for x in sorted_by_inv_factor[-quintile:]]
    self.short = [x.Symbol for x in sorted_by_inv_factor[:quintile]]

return self.long + self.short

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

# order execution
targets:List[PortfolioTarget] = []
for i, portfolio in enumerate([self.long, self.short]):
    for symbol in portfolio:
        if symbol in data and data[symbol]:
            targets.append(PortfolioTarget(symbol, ((-1) ** i) / len(portfolio)))

self.SetHoldings(targets, True)

self.long.clear()
self.short.clear()

def Selection(self) -> None:
if self.Time.month == self.rebalance_month:
    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"))