“该策略投资于纽约证券交易所、美国证券交易所和纳斯达克股票,按专利市场比率排序,做多高比率公司,做空低比率公司,并每年进行价值加权重新平衡。”

I. 策略概要

该策略的目标是具有有效会计和回报数据的纽约证券交易所、美国证券交易所和纳斯达克股票,不包括金融公司、基金、信托、美国存托凭证(ADR)、房地产投资信托基金(REIT)和账面价值为负的公司。它仅包括至少一项已授权专利的公司。

新授权专利的市场价值(MTMT)是根据专利授权日两天内的股票市场反应估算的,基于市值超额变化。专利的累计市场价值(CMPCMP)是每年为每家公司递归计算的。专利市场比率(PTM)计算为CMP/MVCMP / MV,其中MVMV是公司的市场价值。

公司按其PTM比率分为十分位数。多空策略投资于最高PTM十分位数并卖空最低十分位数,采用每年重新平衡的价值加权投资组合。这种方法利用专利估值与股票表现之间的关系来捕捉由创新价值驱动的回报。

II. 策略合理性

专利作为无形资产,对公司的增长、生产力和绩效至关重要。虽然衡量无形资产具有挑战性,但专利市场比率(PTM)提供了一种直接的方法来评估公司归因于其专利库存的市场价值。专利在资产定价中的重要性显而易见,平均PTM比率从1965年的6.91%上升到2010年的13.59%,反映出它们日益增长的重要性。与捕获私人和社会发明价值并依赖未来数据的基于引用的衡量标准不同,PTM比率使用历史股价信息,避免了前瞻性偏差。它提供了按市场价值标准化的货币价值,使公司之间易于比较,并提供了专利对公司估值影响的实用且可靠的衡量标准。

III. 来源论文

Patent-to-Market Premium [点击查看论文]

<摘要>

公司的专利市场比率(PTM)是指公司市场价值中归因于其专利市场价值的百分比。基于PTM比率的对冲投资组合每月产生71个基点的回报。对于PTM比率较低的公司,资本资产定价模型(CAPM)不能被拒绝,但对于PTM比率较高的公司,CAPM被拒绝。PTM比率是一个定价因子,与股票回报横截面中的已知因子不同。PTM比率与未来盈利能力呈正相关。我们的分析表明,实物期权是通过PTM比率预测未来股票回报的渠道。

IV. 回测表现

年化回报5.91%
波动率11.7%
β值0.175
夏普比率0.16
索提诺比率0.103
最大回撤N/A
胜率52%

V. 完整的 Python 代码

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"))

发表评论

了解 Quant Buffet 的更多信息

立即订阅以继续阅读并访问完整档案。

继续阅读