“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.”

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]

<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 Return12.15%
Volatility13.36%
Beta-0.176
Sharpe Ratio-0.05
Sortino Ratio-0.054
Maximum Drawdown47.6%
Win Rate51%

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"))

Leave a Reply

Discover more from Quant Buffet

Subscribe now to keep reading and get access to the full archive.

Continue reading