
The strategy invests in NYSE, AMEX, and NASDAQ stocks, sorting by Patent-to-Market ratio, going long on high-ratio firms, short on low-ratio ones, with annual value-weighted rebalancing.
ASSET CLASS: stocks | REGION: United States | FREQUENCY:
Yearly | MARKET: equities | KEYWORD: Patent, Market
I. STRATEGY IN A NUTSHELL
Targets U.S. stocks with granted patents, ranking firms by their patent-to-market (PTM) ratios. Goes long on the top decile and short on the bottom decile, using value-weighted portfolios rebalanced annually to capture returns linked to innovation.
II. ECONOMIC RATIONALE
Patents are key drivers of firm value and growth. The PTM ratio provides a practical, bias-free measure of a firm’s market value attributable to patents, allowing investors to exploit innovation-driven mispricing for stock returns.
III. SOURCE PAPER
Patent-to-Market Premium [Click to Open PDF]
Jiaping Qiu — McMaster University – Michael G. DeGroote School of Business; Kevin Tseng — The Chinese University of Hong Kong (CUHK) – CUHK Business School; National Taiwan University – Department of Finance; National Taiwan University – Center for Research in Econometric Theory and Applications; Chao Zhang — Shanghai University of Finance and Economics.
<Abstract>
A firm’s patent-to-market (PTM) ratio refers to the percentage of a firm’s market value that is attributable to its patent market value. A hedging portfolio based on PTM ratio generates a monthly return of 71 basis points. The CAPM cannot be rejected for firms with low PTM ratios, but is rejected for firms with high PTM ratios. PTM ratio is a priced factor distinct from known factors in the cross-section of stock returns. PTM ratio is positively associated with future profitability. Our analysis suggests that real option is the channel through which PTM ratio predicts future stock returns.


IV. BACKTEST PERFORMANCE
| Annualised Return | 5.91% |
| Volatility | 11.7% |
| Beta | 0.175 |
| Sharpe Ratio | 0.16 |
| Sortino Ratio | 0.103 |
| Maximum Drawdown | N/A |
| Win Rate | 52% |
V. FULL PYTHON CODE
from AlgorithmImports import *
from enum import Enum
from dateutil.relativedelta import relativedelta
from pandas.tseries.offsets import BDay
from collections import deque
from typing import List, Dict
#endregion
class PortfolioWeighting(Enum):
EQUALLY_WEIGHTED = 1
VALUE_WEIGHTED = 2
INVERSE_VOLATILITY_WEIGHTED = 3
class PatentToMarketEquityFactor(QCAlgorithm):
def Initialize(self) -> None:
self.SetStartDate(2005, 1, 1)
self.SetCash(100_000)
# parameters
self.reaction_period_after_patent: int = 2 # check for reaction of n days after patent grant
self.d_period_after_patent: int = self.reaction_period_after_patent + 1 # n of needed daily prices for performance after patent grant calculation
self.d_volatility_period: int = 60 # daily volatility calculation period
self.m_cumulative_period: int = 12 # calculate CPM value using n-month cumulative patent performance history
self.m_rebalance_period: int = 12 # rebalance once a n months
self.quantile: int = 10 # portfolio percentile selection (3-tercile; 4-quartile; 5-quintile; 10-decile and so on)
self.leverage: int = 20
self.portfolio_weighting: PortfolioWeighting = PortfolioWeighting.EQUALLY_WEIGHTED
# assign larger daily period if volatility weighting is set
if self.portfolio_weighting == PortfolioWeighting.INVERSE_VOLATILITY_WEIGHTED:
self.max_period: int = max(self.d_volatility_period, self.d_period_after_patent)
else:
self.max_period: int = self.d_period_after_patent
self.required_exchanges: List[str] = ['NYS', 'NAS', 'ASE']
self.CMPs: Dict[str, float] = {} # recent CPM value storage
self.weights: Dict[Symbol, float] = {} # recent portfolio selection traded weights
self.patent_dates: Dict[datetime.datetime, list[str]] = {} # storing list of stocks keyed by their patent date
self.market_moves: Dict[str, list[tuple(float, datetime.datetime.date)]] = {} # storing all market moves in one year keyed by stock's ticker
# Source: https://companyprofiles.justia.com/companies
csv_string_file: str = self.Download('data.quantpedia.com/backtesting_data/economic/patents.csv')
lines: List[str] = csv_string_file.split('\r\n')
# select header, then exclude 'date'
tickers: List[str] = lines[0].split(';')[1:]
# store RollingWindow object keyed by stock ticker
self.prices: Dict[str, deque] = { ticker : deque(maxlen=self.max_period) for ticker in tickers }
for line in lines[1:]:
if line == '':
continue
line_split: List[str] = line.split(';')
date: datetime.date = datetime.strptime(line_split[0], "%d.%m.%Y").date()
# initialize empty list for stock's tickers, which have patent in current date
self.patent_dates[date] = []
length: int = len(line_split)
for index in range(1, length):
# store stock's ticker into list, when stock has patent in current date
if line_split[index] != '0.0' and line_split[index] != '0':
self.patent_dates[date].append(tickers[index - 1])
self.market: Symbol = self.AddEquity('SPY', Resolution.Daily).Symbol
# add market to prices dictionary
self.prices[self.market.Value] = deque(maxlen=self.max_period)
self.symbol_by_ticker:dict[str, Symbol] = {}
self.month_counter: int = 0
self.selection_flag: bool = False
self.settings.daily_precise_end_time = False
self.Settings.MinimumOrderMarginPortfolioPercentage = 0.
self.UniverseSettings.Resolution = Resolution.Daily
self.AddUniverse(self.FundamentalSelectionFunction)
self.Schedule.On(self.DateRules.MonthEnd(self.market), self.TimeRules.BeforeMarketClose(self.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]:
# update daily prices
for stock in fundamental:
ticker:str = stock.Symbol.Value
if ticker in self.prices:
self.symbol_by_ticker[ticker] = stock.Symbol
if stock.AdjustedPrice != 0:
self.prices[ticker].append((self.Time.date(), stock.AdjustedPrice))
days_before: datetime.datetime = (self.Time - BDay(self.reaction_period_after_patent)).date()
# check if there was any patent granted in d_period_after_patent days before todays date
# market has to have price data ready
if days_before in self.patent_dates and len(self.prices[self.market.Value]) == self.prices[self.market.Value].maxlen:
if self.prices[self.market.Value][-self.d_period_after_patent][0] == days_before:
# calculate market's return for last d_period_after_patent days
market_return: float = self.prices[self.market.Value][-1][1] / self.prices[self.market.Value][-self.d_period_after_patent][1] - 1
tickers: List[str] = self.patent_dates[days_before]
# calc market moves
for ticker in tickers:
# if not self.prices[ticker].IsReady:
if len(self.prices[ticker]) != self.prices[ticker].maxlen:
continue
if self.prices[ticker][-self.d_period_after_patent][0] == days_before:
# calc stock's return for last d_period_after_patent days
stock_return: float = self.prices[ticker][-1][1] / self.prices[ticker][-self.d_period_after_patent][1] - 1
# calc excess market move value
market_move_value: float = stock_return - market_return
if ticker not in self.market_moves:
self.market_moves[ticker] = []
self.market_moves[ticker].append((days_before, market_move_value))
# rebalance yearly
if not self.selection_flag:
return Universe.Unchanged
# select stocks, which has at least one market move value
selected: List[Fundamental] = [
x for x in fundamental
if x.MarketCap != 0
and x.SecurityReference.ExchangeId in self.required_exchanges
and x.CompanyReference.IsREIT != 1
and x.Symbol.Value in self.market_moves
]
PMT:dict[Fundamental, float] = {} # stores stock's PMT value keyed by stock's object
volatility:dict[Symbol, float] = {} # stores volatility values for each symbol in current selection
for stock in selected:
symbol: Symbol = stock.Symbol
ticker: str = symbol.Value
market_cap: float = stock.MarketCap
# fetch only market moves stored within cumulative period window
sum_market_move: float = sum([x[1] for x in self.market_moves[ticker] if x[0] >= (self.Time - relativedelta(months=self.m_cumulative_period)).date()])
# in case there isn't last_CMP use formula: CMP = MP / (g + gama), otherwise use formula: # CMP = (1 - gama) * last_CMP + MP
curr_CMP_value: float = 0.85 * self.CMPs[ticker] + sum_market_move if ticker in self.CMPs else sum_market_move / (0.20 + 0.15)
# store new current CMP value keyed by stock's ticker
self.CMPs[ticker] = curr_CMP_value
# calc stock's PMT value
PMT_value: float = curr_CMP_value / market_cap
# store stock's PMT value keyed by stock's object
PMT[stock] = PMT_value
# volatility calculation - self.d_volatility_period
daily_prices: np.ndarray = np.array([x[1] for x in self.prices[ticker]][-self.d_volatility_period:])
daily_returns: np.ndarray = daily_prices[1:] / daily_prices[:-1] - 1
volatility[symbol] = np.std(daily_returns) * np.sqrt(252) # annualized volatility
# make sure, there are enough stocks for selection
if len(PMT) < self.quantile:
return Universe.Unchanged
# make percentile selection
quantile: int = int(len(PMT) / self.quantile)
sorted_by_PMT: List[Fundamental] = [x[0] for x in sorted(PMT.items(), key=lambda item: item[1])]
# long highest decile
long: List[Fundamental] = sorted_by_PMT[-quantile:]
# short lowest decile
short: List[Fundamental] = sorted_by_PMT[:quantile]
# portfolio weighting
# calculate weights for long and short portfolio part
if self.portfolio_weighting == PortfolioWeighting.EQUALLY_WEIGHTED:
for i, portfolio in enumerate([long, short]):
for stock in portfolio:
self.weights[stock.Symbol] = ((-1) ** i) / len(portfolio)
elif self.portfolio_weighting == PortfolioWeighting.VALUE_WEIGHTED:
for i, portfolio in enumerate([long, short]):
mc_sum: float = sum(list(map(lambda stock: stock.MarketCap, portfolio)))
for stock in portfolio:
self.weights[stock.Symbol] = ((-1)**i) * stock.MarketCap / mc_sum
elif self.portfolio_weighting == PortfolioWeighting.INVERSE_VOLATILITY_WEIGHTED:
for i, portfolio in enumerate([long, short]):
inv_vol_sum: float = sum(list(map(lambda stock: 1 / volatility[stock.Symbol], portfolio)))
for stock in portfolio:
self.weights[stock.Symbol] = ((-1)**i) * volatility[stock.Symbol] / inv_vol_sum
# return stocks symbols
return list(self.weights.keys())
def OnData(self, data: Slice) -> None:
# wait for selection flag to be set
if not self.selection_flag:
return
self.selection_flag = False
# trade execution
portfolio: List[PortfolioTarget] = [PortfolioTarget(symbol, w) for symbol, w in self.weights.items() if symbol in data and data[symbol]]
self.SetHoldings(portfolio, True)
self.weights.clear()
def Selection(self) -> None:
# wait for self.m_cumulative_period months to elapse from the start of the algorithm before first selection. It gives the chance to self.market_moves to potentially fill up.
if self.Time.date() < (self.StartDate + relativedelta(months=self.m_cumulative_period)).date():
return
# rebalance once a rebalance period
if self.month_counter % self.m_rebalance_period == 0:
self.selection_flag = True
self.month_counter += 1
# Custom fee model.
class CustomFeeModel(FeeModel):
def GetOrderFee(self, parameters: OrderFeeParameters) -> OrderFee:
fee: float = parameters.Security.Price * parameters.Order.AbsoluteQuantity * 0.00005
return OrderFee(CashAmount(fee, "USD"))
VI. Backtest Performance