Quant BuffetRelax, Not Over Thinking

Technology Momentum

Log in to collect

Academic paper

Technological Links and Predictable Returns

AuthorsCharles M.C. Lee; Stephen Teng Sun; Rongfei Wang; Ran Zhang

Institute
  • University of Washington
  • Stanford University
  • ?Foster School of Business, University of Washington
  • ?Stanford University - Graduate School of Business
  • HKCity University of Hong Kong
  • ?City University of Hong Kong (CityU) - Department of Accountancy
  • State Power Investment Corporation (China)
  • ?China Investment Corporation (CIC)
  • Renmin University of China
  • ?Renmin University of China - School of Business

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.

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

Backtest performance

Annualised return8.6%
Volatility18.17%
Beta0.066
Sharpe ratio0.25
Sortino ratio0.016
Win rate49%

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