Overall Statistics
Total Trades
3585
Average Win
0.36%
Average Loss
-0.21%
Compounding Annual Return
3.726%
Drawdown
38.600%
Expectancy
0.281
Net Profit
148.342%
Sharpe Ratio
0.356
Probabilistic Sharpe Ratio
0.005%
Loss Rate
53%
Win Rate
47%
Profit-Loss Ratio
1.71
Alpha
0.024
Beta
0.07
Annual Standard Deviation
0.081
Annual Variance
0.007
Information Ratio
-0.211
Tracking Error
0.171
Treynor Ratio
0.413
Total Fees
$239.24
Estimated Strategy Capacity
$68000.00
Lowest Capacity Asset
FEI R735QTJ8XC9X
# https://quantpedia.com/strategies/rd-expenditures-and-stock-returns/
#
# 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.

#region imports
from AlgorithmImports import *
from numpy import log, average
from scipy import stats
import numpy as np
#endregion

class RDExpendituresandStockReturns(QCAlgorithm):

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

        self.weight = {}
        self.coarse_count = 3000
        
        # R&D history.
        self.RD = {}
        self.rd_period = 5
        self.quantile = 5
        
        self.long = []
        self.short = []
        
        data = self.AddEquity('XLK', Resolution.Daily)
        data.SetLeverage(10)
        self.technology_sector = data.Symbol
      
        self.symbol = self.AddEquity('SPY', Resolution.Daily).Symbol
        
        self.selection_flag = True
        self.UniverseSettings.Resolution = Resolution.Daily
        self.AddUniverse(self.CoarseSelectionFunction, self.FineSelectionFunction)
        self.Schedule.On(self.DateRules.MonthEnd(self.symbol), self.TimeRules.AfterMarketOpen(self.symbol), self.Selection)
   
    def OnSecuritiesChanged(self, changes):
        for security in changes.AddedSecurities:
            security.SetLeverage(10)
            security.SetFeeModel(CustomFeeModel())
        
    def CoarseSelectionFunction(self, coarse):
        if not self.selection_flag:
            return Universe.Unchanged
        
        selected = [x.Symbol for x in coarse if x.HasFundamentalData and x.Price > 5]
        
        return selected

    def FineSelectionFunction(self, fine):
        fine = [x for x in fine if (x.FinancialStatements.IncomeStatement.ResearchAndDevelopment.TwelveMonths) and \
                                    (x.MarketCap != 0) and \
                                    ((x.SecurityReference.ExchangeId == "NYS") or (x.SecurityReference.ExchangeId == "NAS") or (x.SecurityReference.ExchangeId == "ASE"))]
                                    #and x.AssetClassification.MorningstarSectorCode == MorningstarSectorCode.Technology]
        
        top_by_market_cap = None
        if len(fine) > self.coarse_count:
            sorted_by_market_cap = sorted(fine, key = lambda x:x.MarketCap, reverse=True)
            top_by_market_cap = sorted_by_market_cap[:self.coarse_count]
        else:
            top_by_market_cap = fine
        
        fine_symbols = [x.Symbol for x in top_by_market_cap]
        ability = {}
        
        updated_flag = []  # updated this year already
        
        for stock in top_by_market_cap:
            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)
                #rd = stock.FinancialStatements.IncomeStatement.ResearchAndDevelopment.TwelveMonths
                #self.RD[symbol].Add(rd)
                
                if self.RD[symbol].IsReady:
                    coefs = np.array([1, 0.8, 0.6, 0.4, 0.2])
                    rds = np.array([x for x in self.RD[symbol]])
                    
                    rdc = sum(coefs * rds)
                    ability[stock] = rdc/stock.MarketCap
                
                rd = stock.FinancialStatements.IncomeStatement.ResearchAndDevelopment.TwelveMonths
                self.RD[symbol].Add(rd)
            
            # prevent storing duplicated value for the same stock in one year
            if fine_symbols.count(symbol) > 1:
                updated_flag.append(symbol)
        
        # Ability market cap weighting.
        #total_market_cap = sum([x.MarketCap for x in ability])
        #for stock, rdc in ability.items():
            #ability[stock] = rdc * (stock.MarketCap / total_market_cap)
        
        # Remove not updated symbols
        symbols_to_delete = []
        for symbol in self.RD.keys():
            if symbol not in fine_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 = sorted(ability.items(), key = lambda x: x[1], reverse = True)
            quantile = int(len(sorted_by_ability) / self.quantile)
            high_by_ability = [x[0].Symbol for x in sorted_by_ability[:quantile]]
            low_by_ability = [x[0].Symbol for x in sorted_by_ability[-quantile:]]
            
            self.long = high_by_ability
            self.short = low_by_ability
            #self.short = [self.technology_sector]
        
        return self.long + self.short
    
    def Selection(self):
        if self.Time.month == 4:
            self.selection_flag = True
            
    def OnData(self, data):
        if not self.selection_flag:
            return
        self.selection_flag = False
        
        # Trade execution.
        long_count = len(self.long)
        short_count = len(self.short)
        
        stocks_invested = [x.Key for x in self.Portfolio if x.Value.Invested]
        for symbol in stocks_invested:
            if symbol not in self.long + self.short:
                self.Liquidate(symbol)

        for symbol in self.long:
            if symbol in data and data[symbol]:
                self.SetHoldings(symbol, 1 / long_count)
        
        for symbol in self.short:
            if symbol in data and data[symbol]:
                self.SetHoldings(symbol, -1 / short_count)

        self.long.clear()
        self.short.clear()
        
class SymbolData():
    def __init__(self, tested_growth, period):
        self.TestedGrowth = tested_growth
        self.RD = RollingWindow[float](period)
            
    def update(self, window_value):
        self.RD.Add(window_value)
            
    def is_ready(self):
        return self.RD.IsReady
        
class CustomFeeModel(FeeModel):
    def GetOrderFee(self, parameters):
        fee = parameters.Security.Price * parameters.Order.AbsoluteQuantity * 0.00005
        return OrderFee(CashAmount(fee, "USD"))