Quant BuffetRelax, Not Over Thinking

R&D Expenditures and Stock Returns

Log in to collect

Academic paper

Strategy in a nutshell

The investment universe consists of stocks that are listed on NYSE NASDAQ or AMEX. At the end of April, for each stock in the universe, calculate a measure of total R&D expenditures in the past 5 years scaled by the firm’s Market cap (defined on page 7, eq. 1). Go long (short) on the quintile of firms with the highest (lowest) R&D expenditures relative to their Market Cap. Weight the portfolio equally and rebalance next year. The backtested performance of the paper is substituted by our more recent backtest in Quantconnect.

Economic rationale

Under the efficient market hypothesis, the investor should be able to recognize the value of less-tangible assets. However, in conditions of an inefficient market, the presence of such intangible assets could possibly lead to mispricing. One of the reasons for possible mispricing lies in the US GAAP and IFRS accounting standards. Under these standards, the costs of R&D must be expensed in the same fiscal year as they occur and therefore could significantly influence the reported earnings of a company in the current year. However, the R&D expenditures usually represent a long-term investment that implies a possible future revenue and cash flow.

Backtest performance

Annualised return4.67%
Volatility8.23%
Beta0.041
Sharpe ratio0.34
Sortino ratio0.33
Maximum drawdown49.88%
Win rate45%

Full Python code

from AlgoLib import *
from numpy import log, average
from scipy import stats
import numpy as np
#endregion

class RDExpendituresandStockReturns(XXX):

def Initialize(self) -> None:
self.SetStartDate(1998, 1, 1)
self.SetCash(100000)

self.fundamental_count:int = 500
self.fundamental_sorting_key = lambda x: x.DollarVolume
self.rebalance_month:int = 4
self.quantile:int = 5
self.leverage:int = 5
self.min_share_price:float = 5.
self.exchange_codes:List[str] = ['NYS', 'NAS', 'ASE']

# R&D history.
self.RD:Dict[Symbol, float] = {}
self.rd_period:int = 5

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

data:Equity = self.AddEquity('XLK', Resolution.Daily)
data.SetLeverage(self.leverage)
self.technology_sector:Symbol = data.Symbol
  
market:Symbol = self.AddEquity('SPY', Resolution.Daily).Symbol

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.AdjustedPrice > self.min_share_price and x.SecurityReference.ExchangeId in self.exchange_codes and x.MarketCap != 0 and \
    not np.isnan(x.FinancialStatements.IncomeStatement.ResearchAndDevelopment.TwelveMonths) and x.FinancialStatements.IncomeStatement.ResearchAndDevelopment.TwelveMonths != 0
    ]
if len(selected) > self.fundamental_count:
    selected = [x for x in sorted(selected, key=self.fundamental_sorting_key, reverse=True)[:self.fundamental_count]]

selected_symbols:List[Symbol] = list(map(lambda x: x.Symbol, selected))

ability:Dict[Fundamental, float] = {}
updated_flag:List[Symbol] = []  # updated this year already

for stock in selected:
    symbol:Symbol = stock.Symbol
    
    # prevent storing duplicated value for the same stock in one year
    if symbol not in updated_flag:
        # Update RD.
        if symbol not in self.RD:
            self.RD[symbol] = RollingWindow[float](self.rd_period)
        
        if self.RD[symbol].IsReady:
            coefs:np.ndarray = np.array([1, 0.8, 0.6, 0.4, 0.2])
            rds:np.ndarray = np.array([x for x in self.RD[symbol]])
            
            rdc:float = sum(coefs * rds)
            ability[stock] = rdc / stock.MarketCap
        
        rd:float = stock.FinancialStatements.IncomeStatement.ResearchAndDevelopment.TwelveMonths
        self.RD[symbol].Add(rd)
    
    # prevent storing duplicated value for the same stock in one year
    if selected_symbols.count(symbol) > 1:
        updated_flag.append(symbol)

# Remove not updated symbols
symbols_to_delete:List[Symbol] = []
for symbol in self.RD.keys():
    if symbol not in selected_symbols:
        symbols_to_delete.append(symbol)    
for symbol in symbols_to_delete:
    if symbol in self.RD:
        del self.RD[symbol]

# starts trading after data storing period
if len(ability) >= self.quantile:
    # Ability sorting.
    sorted_by_ability:List = sorted(ability.items(), key = lambda x: x[1], reverse = True)
    quantile:int = int(len(sorted_by_ability) / self.quantile)
    high_by_ability:List[Symbol] = [x[0].Symbol for x in sorted_by_ability[:quantile]]
    low_by_ability:List[Symbol] = [x[0].Symbol for x in sorted_by_ability[-quantile:]]
    
    self.long = high_by_ability
    self.short = low_by_ability

return self.long + self.short

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

class CustomFeeModel(FeeModel):
def GetOrderFee(self, parameters):
fee = parameters.Security.Price * parameters.Order.AbsoluteQuantity * 0.00005
return OrderFee(CashAmount(fee, "USD"))