Quant BuffetRelax, Not Over Thinking

Patent Intensity Factor in Equities

Log in to collect

Academic paper

Pricing Technological Innovators: Patent Intensity and Life-Cycle Dynamics

AuthorsJan Bena; Adlai J. Fisher; Jiří Knesl; Julian Vahl

Institute
  • 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

Annualised return6.97%
Volatility15.61%
Beta0.016
Sharpe ratio0.45
Win rate42%

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