
“Universe: NYSE, AMEX, Nasdaq stocks with Sales > $10M. Sorted by market cap, each half divided into ROA deciles. Long top 3, short bottom 3. Monthly rebalancing, equal weighting.”
ASSET CLASS: stocks | REGION: United States | FREQUENCY: Monthly | MARKET:
equities | KEYWORD: ROA
I. STRATEGY IN A NUTSHELL
The investment universe contains all stocks on NYSE and AMEX and Nasdaq with Sales greater than 10 million USD. Stocks are then sorted into two halves based on market capitalization. Each half is then divided into deciles based on Return on assets (ROA) calculated as quarterly earnings (Compustat quarterly item IBQ – income before extraordinary items) divided by one-quarter-lagged assets (item ATQ – total assets). The investor then goes long the top three deciles from each market capitalization group and goes short bottom three deciles. The strategy is rebalanced monthly, and stocks are equally weighted.
II. ECONOMIC RATIONALE
Research explains that firms with productive assets should yield higher average returns than firms with unproductive assets. Productive firms for which investors demand high average returns should be priced similarly to less productive firms for which investors demand lower returns. Variation in productivity, therefore, helps identify variation in investors’ required rates of return. Therefore profitable firms generate higher average returns than unprofitable firms (as productivity helps identify this variation – with higher profitability indicating higher required rates). This fact motivates the return-on-asset factor.
III. SOURCE PAPER
An Alternative Three-Factor Model [Click to Open PDF]
- Long Chen, Cheung Kong Graduate School of Business; Luohan Academy
- Robert Novy-Marx, Simon Business School, University of Rochester; National Bureau of Economic Research (NBER)
- Lu Zhang, Ohio State University – Fisher College of Business; National Bureau of Economic Research (NBER)
<Abstract>
A new factor model consisting of the market factor, an investment factor, and a return-on-equity factor is a good start to understanding the cross-section of expected stock returns. Firms will invest a lot when their profitability is high and the cost of capital is low. As such, controlling for profitability, investment should be negatively correlated with expected returns, and controlling for investment, profitability should be positively correlated with expected returns. The new three-factor model reduces the magnitude of the abnormal returns of a wide range of anomalies-based trading strategies, often to insignificance. The model’s performance, combined with its economic intuition, suggests that it can be used to obtain expected return estimates in practice.

IV. BACKTEST PERFORMANCE
| Annualised Return | 12.15% |
| Volatility | 13.36% |
| Beta | -0.176 |
| Sharpe Ratio | -0.05 |
| Sortino Ratio | -0.054 |
| Maximum Drawdown | 47.6% |
| Win Rate | 51% |
V. FULL PYTHON CODE
from AlgoLib import *
class ROAEffectWithinStocks(XXX):
def Initialize(self):
self.SetStartDate(2000, 1, 1)
self.SetCash(100000)
market:Symbol = self.AddEquity('SPY', Resolution.Daily).Symbol
self.quantile:int = 10
self.leverage:int = 5
self.sales_threshold:float = 1e7
self.exchange_codes:List[str] = ['NYS', 'NAS', 'ASE']
self.long:List[Symbol] = []
self.short:List[Symbol] = []
self.fundamental_count:int = 500
self.fundamental_sorting_key = lambda x: x.DollarVolume
self.selection_flag:bool = False
self.UniverseSettings.Resolution = Resolution.Daily
self.AddUniverse(self.FundamentalSelectionFunction)
self.Settings.MinimumOrderMarginPortfolioPercentage = 0.
self.Schedule.On(self.DateRules.MonthEnd(market), self.TimeRules.AfterMarketOpen(market), 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.SecurityReference.ExchangeId in self.exchange_codes and \
x.ValuationRatios.SalesPerShare * x.EarningReports.DilutedAverageShares.Value > self.sales_threshold and \
not np.isnan(x.OperationRatios.ROA.ThreeMonths) and x.OperationRatios.ROA.ThreeMonths != 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 market cap.
sorted_by_market_cap = sorted(selected, key = lambda x: x.MarketCap, reverse=True)
half:int = int(len(sorted_by_market_cap) / 2)
top_mc = [x for x in sorted_by_market_cap[:half]]
bottom_mc = [x for x in sorted_by_market_cap[half:]]
if len(top_mc) >= self.quantile and len(bottom_mc) >= self.quantile:
# Sorting by ROA.
sorted_top_by_roa:List[Fundamental] = sorted(top_mc, key = lambda x:(x.OperationRatios.ROA.Value), reverse=True)
quantile:int = int(len(sorted_top_by_roa) / self.quantile)
long_top:List[Symbol] = [x.Symbol for x in sorted_top_by_roa[:quantile*3]]
short_top:List[Symbol] = [x.Symbol for x in sorted_top_by_roa[-(quantile*3):]]
sorted_bottom_by_roa:List[Fundamental] = sorted(bottom_mc, key = lambda x:(x.OperationRatios.ROA.Value), reverse=True)
quantile = int(len(sorted_bottom_by_roa) / self.quantile)
long_bottom:List[Symbol] = [x.Symbol for x in sorted_bottom_by_roa[:quantile*3]]
short_bottom:List[Symbol] = [x.Symbol for x in sorted_bottom_by_roa[-(quantile*3):]]
self.long = long_top + long_bottom
self.short = short_top + short_bottom
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:
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"))