Patent Intensity Factor in Equities
Log in to collectAcademic paper
Pricing Technological Innovators: Patent Intensity and Life-Cycle Dynamics
Jan Bena; Adlai J. Fisher; Jiří Knesl; Julian Vahl
- CAUniversity of British Columbia
- ?University of British Columbia - Sauder School of Business
- ?University of British Columbia (UBC) - Sauder School of Business
- University of Oxford
- ?Said Business School, University of Oxford
Strategy in a nutshell
The strategy focuses on NYSE-listed firms and uses patent intensity (ratio of patents over the last 12 months to market capitalization) to gauge innovation levels. Stocks are sorted by patent intensity, split into quartiles, and the portfolio goes long on high-innovation firms (top quartiles) and short on low-innovation firms (bottom quartile). All positions are equally weighted, and the portfolio is rebalanced annually on June 30.
Economic rationale
Innovative firms often exhibit high growth potential but low profitability due to heavy reinvestment. Traditional valuation models misprice them. By targeting patent intensity, this strategy exploits market mispricing, favoring firms with higher innovation and growth prospects.
Backtest performance
Full Python code
from AlgorithmImports import *
from data_tools import CustomFeeModel
# endregion
class PatentIntensityFactorInEquities(QCAlgorithm):
def Initialize(self):
self.SetStartDate(2000, 1, 1)
self.SetCash(100000)
self.leverage:int = 5
self.quantile:int = 4
self.rebalance_month:int = 6
self.weights:Dict[Symbol, float] = {}
self.patents_granted:Dict[str, int] = {}
self.patents:Dict[str, Dict[str, Dict[str, int]]] = {}
self.market_symbol:Symbol = self.AddEquity('SPY', Resolution.Daily).Symbol
patents_csv:str = self.Download('data.quantpedia.com/backtesting_data/economic/patents.csv')
lines:List[str] = patents_csv.split('\r\n')
header:str = lines.pop(0)
self.tickers:List[str] = header.split(';')[1:]
for line in lines:
if line == '':
continue
split:List[str] = line.split(';')
date:str = split.pop(0)
date_split:List[str] = date.split('.')
month:int = int(date_split[1])
year:int = int(date_split[-1])
if year not in self.patents:
self.patents[year] = {}
if month not in self.patents[year]:
self.patents[year][month] = {}
for i, total_patents in enumerate(split):
if total_patents == '0.0':
continue
ticker:str = self.tickers[i]
total_patents = int(float(total_patents))
if ticker not in self.patents[year][month]:
self.patents[year][month][ticker] = 0
self.patents[year][month][ticker] += total_patents
self.selection_flag:bool = False
self.UniverseSettings.Resolution = Resolution.Daily
self.AddUniverse(self.CoarseSelectionFunction, self.FineSelectionFunction)
self.Schedule.On(self.DateRules.MonthEnd(self.market_symbol), self.TimeRules.BeforeMarketClose(self.market_symbol, 0), self.Selection)
def OnSecuritiesChanged(self, changes:SecurityChanges) -> None:
for security in changes.AddedSecurities:
security.SetFeeModel(CustomFeeModel())
security.SetLeverage(self.leverage)
def CoarseSelectionFunction(self, coarse:List[CoarseFundamental]) -> List[Symbol]:
if not self.selection_flag:
return Universe.Unchanged
selected_symbols:List[Symbol] = [x.Symbol for x in coarse if x.Symbol.Value in self.patents_granted]
return selected_symbols
def FineSelectionFunction(self, fine:List[FineFundamental]) -> List[Symbol]:
PI_values:Dict[Symbol, int] = {}
for stock in fine:
market_cap:float = stock.MarketCap
if market_cap == 0:
continue
symbol:Symbol = stock.Symbol
total_patents:int = self.patents_granted[symbol.Value]
PI_values[symbol] = total_patents / market_cap
self.patents_granted.clear()
if len(PI_values) < self.quantile:
return Universe.Unchanged
quantile:int = int(len(PI_values) / self.quantile)
sorted_by_values:List[Symbol] = [x[0] for x in sorted(PI_values.items(), key=lambda item: item[1])]
long_leg:List[Symbol] = sorted_by_values[-quantile:]
short_leg:List[Symbol] = sorted_by_values[:quantile]
for symbol in long_leg:
self.weights[symbol] = 1 / quantile
for symbol in short_leg:
self.weights[symbol] = -1 / quantile
return long_leg + short_leg
def OnData(self, data):
curr_year:int = self.Time.year
curr_month:int = self.Time.month
if curr_year in self.patents and curr_month in self.patents[curr_year]:
patents_granted:Dict[str, int] = self.patents[curr_year][curr_month]
for ticker, total_patents in patents_granted.items():
if ticker not in self.patents_granted:
self.patents_granted[ticker] = 0
self.patents_granted[ticker] += total_patents
del self.patents[curr_year][curr_month]
if not self.selection_flag:
return
self.selection_flag = False
# trade execution
invested:list[Symbol] = [x.Key for x in self.Portfolio if x.Value.Invested]
for symbol in invested:
if symbol not in self.weights:
self.Liquidate(symbol)
for symbol, w in self.weights.items():
self.SetHoldings(symbol, w)
self.weights.clear()
def Selection(self):
if self.Time.month == self.rebalance_month:
self.selection_flag = True