
The strategy invests in patent-driven firms, leveraging technology closeness to calculate returns, taking long positions in top decile TECHRET firms and short positions in bottom decile, rebalancing monthly.
ASSET CLASS: stocks | REGION: United States | FREQUENCY:
Monthly | MARKET: equities | KEYWORD: Technology, Momentum
I. STRATEGY IN A NUTSHELL
This strategy targets U.S. stocks with recent patent activity, integrating CRSP, COMPUSTAT, and USPTO data. Eligible firms must have at least one patent granted in the past five years, be common stocks with positive book equity, and exclude financials, micro-caps, and stocks under $1.
Each firm’s technology-linked return (TECHRET) is calculated as a weighted average of returns from technology-related peers, with weights proportional to technology closeness (TECH)—the normalized overlap of patent vectors across 427 USPTO classes (0–1 scale). Stocks are sorted monthly into deciles based on prior-month TECHRET, with long positions in the top decile and short positions in the bottom decile. The portfolio is value-weighted and rebalanced monthly, leveraging innovation-related connections to predict stock performance.
II. ECONOMIC RATIONALE
The technology momentum effect reflects market inefficiencies driven by limited investor attention, processing constraints, valuation uncertainty, and arbitrage costs, particularly for overlooked innovative firms. Returns of technology-linked firms consistently predict focal firms’ future returns, even after controlling for size, book-to-market, profitability, R&D intensity, asset growth, and conventional momentum factors. This persistent effect highlights that innovation undervaluation, rather than short-term investor overreaction, underlies the strategy’s long-term reliability
III. SOURCE PAPER
Technological Links and Predictable Returns [Click to Open PDF]
Charles M. C. Lee, Foster School of Business, University of Washington; Stephen Teng Sun, Stanford University – Graduate School of Business; Rongfei Wang, City University of Hong Kong (CityU) – Department of Economics and Finance; Ran Zhang, City University of Hong Kong (CityU) – Department of Accountancy; [Next Author], China Investment Corporation (CIC); [Next Author], Renmin University of China – School of Business
<Abstract>
Employing a classic measure of technological closeness between firms, we show that the returns of technology-linked firms have strong predictive power for focal firm returns. A long-short strategy based on this effect yields monthly alpha of 117 basis points. This effect is distinct from industry momentum and is not easily attributable to risk-based explanations. It is more pronounced for focal firms that: (a) have a more intense and specific technology focus, (b) receive lower investor attention, and (c) are more difficult to arbitrage. Our results are broadly consistent with sluggish price adjustment to more nuanced technological news.


IV. BACKTEST PERFORMANCE
| Annualised Return | 8.6% |
| Volatility | 18.17% |
| Beta | 0.066 |
| Sharpe Ratio | 0.25 |
| Sortino Ratio | 0.016 |
| Maximum Drawdown | N/A |
| Win Rate | 49% |
V. FULL PYTHON CODE
from AlgorithmImports import *
from dateutil.relativedelta import relativedelta
import data_tools
from typing import List, Dict
# endregion
class TechnologyMomentum(QCAlgorithm):
def Initialize(self) -> None:
self.SetStartDate(2010, 1, 1)
self.SetCash(100_000)
self.international_data: bool = False
self.tickers: List[str] = [
'ABAX', 'ARAY', 'ACTU', 'ADBE', 'ADVS', 'AFFX', 'A', 'ALGN', 'AOSL', 'ALIN-A', 'AMZN', 'AMD', 'AAPL', 'AMAT',
'AMCC', 'ARBA', 'ARUN', 'T', 'ATML', 'AXTI', 'BRCD', 'CDNS', 'CALD', 'CAVM', 'CPHD', 'CSCO', 'C', 'CDXS', 'COHR',
'CPTS', 'CY', 'DEPO', 'DLGC', 'DIS', 'DLB', 'DWA', 'DSPG', 'EBAY', 'ELON', 'EA', 'EFII', 'EPOC', 'EQIX', 'EXAR',
'EXEL', 'EXTR', 'FB', 'FCS', 'FNSR', 'F', 'FORM', 'FTNT', 'GE', 'GM', 'GDHX', 'GILD', 'GOOGL', 'GSIT', 'HLIT',
'HPQ', 'HMC', 'IBM', 'IGTE', 'IKAN', 'IPXL', 'INFN', 'INFA', 'IDIT', 'ISSI', 'INTC', 'ISIL', 'IVAC', 'INTU',
'ISRG', 'INVN', 'IPA', 'IXYS', 'JDSU', 'JNPR', 'KEYN', 'KLAC', 'LRCX', 'LF', 'LLTC', 'LSI', 'LLC', 'MTSN',
'MXIM', 'MERU', 'MCRL', 'MSFT', 'MPWR', 'ONTO', 'NTUS', 'NPTN', 'NTAP', 'NFLX', 'NTGR', 'N', 'NSANY', 'NVLS',
'NVDA', 'OCLR', 'OCZ', 'OMCL', 'OVTI', 'ONXX', 'OPEN_old', 'OPWV', 'OPLK', 'OPXT', 'ORCL', 'P', 'PSEM', 'PLXT',
'PMCS', 'PLCM', 'POWI', 'QMCO', 'QNST', 'RMBS', 'MKTG', 'RVBD', 'ROVI', 'SABA', 'CRM', 'SNDK', 'SANM', 'SCLN',
'SREV', 'SHOR', 'SFLY', 'SIGM', 'SGI', 'SIMG', 'SLTM', 'CODE', 'SPWR', 'SMCI', 'NLOK', 'SYMM', 'SYNA', 'SNX',
'SNPS', 'TNAV', 'TSLA', 'XPER', 'THOR', 'TIBX', 'TIVO', 'TYO', 'TRID', 'TRMB', 'TWTR', 'UI', 'UCTT', 'ULTRACEMCO',
'VAR', 'PAY', 'VMW', 'VLTR', 'WMT', 'XLNX', 'YAHOO', 'DZSI', 'ZNGA'
]
self.years_period: int = 5
self.min_prices: int = 15
self.quantile: int = 10
self.leverage: int = 5
self.weights: Dict[Symbol, float] = {}
self.count_by_class_by_date: Dict[datetime.date, Dict[str, int]] = {}
self.data: Dict[str, data_tools.SymbolData] = {}
self.classification_symbol_by_ticker: Dict[str, Symbol] = {}
for ticker in self.tickers:
classification_symbol: Symbol = self.AddData(data_tools.QuantpediaPatentClassification, ticker, Resolution.Daily).Symbol
self.classification_symbol_by_ticker[ticker] = classification_symbol
self.data[ticker] = data_tools.SymbolData()
market: Symbol = self.AddEquity('SPY', Resolution.Daily).Symbol
self.selection_flag: bool = False
self.UniverseSettings.Leverage = self.leverage
self.UniverseSettings.Resolution = Resolution.Daily
self.AddUniverse(self.FundamentalSelectionFunction)
self.settings.daily_precise_end_time = False
self.Settings.MinimumOrderMarginPortfolioPercentage = 0.
self.Schedule.On(self.DateRules.MonthStart(market), self.TimeRules.BeforeMarketClose(market), self.Selection)
def OnSecuritiesChanged(self, changes: SecurityChanges) -> None:
for security in changes.AddedSecurities:
security.SetFeeModel(data_tools.CustomFeeModel())
def FundamentalSelectionFunction(self, fundamental: List[Fundamental]) -> List[Symbol]:
# daily update prices required in stock's monthly return calculation
for equity in fundamental:
ticker: str = equity.Symbol.Value
if ticker in self.data:
self.data[ticker].update_prices(equity.AdjustedPrice)
if not self.selection_flag:
return Universe.Unchanged
start_boundry_date: datetime.date = self.Time.date() - relativedelta(years=self.years_period)
for _, symbol_obj in self.data.items():
# reset of prices and update of monthly return, which is required for each stock on monthly basis, to keep valid data
if symbol_obj.prices_ready(self.min_prices):
symbol_obj.update_monthly_return()
else:
symbol_obj.reset_monthly_return()
symbol_obj.reset_prices()
# remove stock's patents classifications, which won't be needed anymore
symbol_obj.remove_needless_classifications(start_boundry_date)
selected: List[Fundamental] = [
x for x in fundamental
if x.Symbol.Value in self.data
and x.MarketCap != 0
and self.data[x.Symbol.Value].monthly_return_ready()
and self.data[x.Symbol.Value].classifications_ready()]
if len(selected) == 0:
return Universe.Unchanged
dates_to_remove: datetime.date = []
companies_patents_classes: List[str] = []
start_boundry_date: datetime.date = self.Time.date() - relativedelta(years=self.years_period)
for date, count_by_class in self.count_by_class_by_date.items():
if date < start_boundry_date:
dates_to_remove.append(date)
continue
for patent_class, count in count_by_class.items():
if patent_class not in companies_patents_classes:
companies_patents_classes.append(patent_class)
# these days won't be needed anymore
for date in dates_to_remove:
del self.count_by_class_by_date[date]
monthly_return_by_stock: Dict[Fundamental, float] = {}
vector_by_stock: Dict[Fundamental, np.ndarray] = {}
for stock in selected:
ticker: str = stock.Symbol.Value
proportional_vector: List[int] = self.data[ticker].get_proportional_vector(companies_patents_classes)
vector_by_stock[stock] = np.array(proportional_vector)
monthly_return: float = self.data[ticker].get_monthly_return()
monthly_return_by_stock[stock] = monthly_return
TECH_RET: Dict[Fundamental, float] = {}
for stock1, vector1 in vector_by_stock.items():
TECH: float = 0
RET: float = 0
for stock2, vector2 in vector_by_stock.items():
if stock1 != stock2:
scala_product: float = vector1.dot(vector2)
if scala_product == 0:
continue
TECH += scala_product / ((scala_product ** 0.5) * (scala_product ** 0.5))
RET += monthly_return_by_stock[stock2]
if TECH != 0 and RET != 0:
TECH_RET[stock1] = (TECH * RET) / TECH
if len(TECH_RET) < self.quantile:
return Universe.Unchanged
quantile: int = int(len(TECH_RET) / self.quantile)
sorted_by_TECH_RET: List[Fundamental] = [x[0] for x in sorted(TECH_RET.items(), key=lambda item: item[1])]
long_leg: List[Fundamental] = sorted_by_TECH_RET[-quantile:]
short_leg: List[Fundamental] = sorted_by_TECH_RET[:quantile]
for i, portfolio in enumerate([long_leg, short_leg]):
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
return list(self.weights.keys())
def OnData(self, data: Slice) -> None:
curr_date: datetime.date = self.Time.date()
custom_data_last_update_date: Dict[Symbol, datetime.date] = data_tools.QuantpediaPatentClassification.get_last_update_date()
latest_update_date: datetime.date = max(date for date in custom_data_last_update_date.values())
stock_to_delete: List[str] = []
# if self.time.date() > latest_update_date:
# self.liquidate()
# return
for stock_ticker, classification_symbol in self.classification_symbol_by_ticker.items():
if self.Securities[stock_ticker].GetLastData() and self.Time.date() > custom_data_last_update_date[stock_ticker]:
self.Liquidate(stock_ticker)
stock_to_delete.append(stock_ticker)
continue
if data.contains_key(classification_symbol) and data[classification_symbol]:
if curr_date not in self.count_by_class_by_date:
self.count_by_class_by_date[curr_date] = {}
symbol_obj: SymbolData = self.data[stock_ticker]
wanted_property: str = 'international' if self.international_data else 'u.s.'
patent_classes: List[str] = list(data[classification_symbol].GetProperty(wanted_property))
for patent_class in patent_classes:
if patent_class not in self.count_by_class_by_date[curr_date]:
self.count_by_class_by_date[curr_date][patent_class] = 0
self.count_by_class_by_date[curr_date][patent_class] += 1
symbol_obj.update_patents(curr_date, patent_class)
for ticker in stock_to_delete:
self.classification_symbol_by_ticker.pop(ticker)
# rebalance monthly
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 data.contains_key(symbol) and data[symbol]]
self.SetHoldings(portfolio, True)
self.weights.clear()
def Selection(self) -> None:
self.selection_flag = True