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.

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 Return5.91%
Volatility11.7%
Beta0.175
Sharpe Ratio0.16
Sortino Ratio0.103
Maximum DrawdownN/A
Win Rate52%

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

Leave a Reply

Discover more from Quant Buffet

Subscribe now to keep reading and get access to the full archive.

Continue reading