“该策略涉及大盘股,按1个月回报和PTH比率对其进行排序。它创建了25个投资组合,做多低PTH过去输家,做空低PTH过去赢家,每月重新平衡。”

I. 策略概要

投资范围包括纽约证券交易所(NYSE)、美国证券交易所(AMEX)和纳斯达克(NASDAQ)的股票,不包括在t月末价格低于5美元的股票。使用纽约证券交易所的百分位数,将股票分为微盘股、小盘股和大盘股公司。重点关注大盘股,PTH比率计算为当前价格相对于过去52周最高价格的比率。股票根据1个月回报和PTH排名被分为五分位投资组合。这两个投资组合的交集创建了25种组合。该策略涉及做多低PTH过去输家和做空低PTH过去赢家,等权重投资组合每月重新平衡。

II. 策略合理性

52周最高股票价格作为Andrei和Cujean模型中会议强度的代理,影响动量和反转效应。达到52周高点的股票吸引媒体关注,增加投资者兴趣和信息交流。该论文表明,由于投资者会议频繁,动量在高PTH股票中占主导地位,而低PTH股票则经历短期反转。该策略显示PTH值与短期反转盈利能力之间存在强烈的负相关关系。结果在不同的回报类型、公司规模和子样本期间保持稳健。分别对PTH和反转排名进行计时,可以防止赢家和输家的投资组合不平衡。

III. 来源论文

Information Percolation, the 52-Week High, and Short-Term Reversal in Stock Returns [点击查看论文]

<摘要>

我们发现,价格锚定在理解1个月(1M)股票回报的短期反转中,与众所周知的流动性提供渠道相结合,发挥着作用。具体而言,我们确定,对于(a)相对于其52周高点价格较低的股票(George和Hwang)和(b)资本利得悬置较低的股票(Grinblatt和Han),1M反转策略表现更好。此外,我们发现过去赢家和过去输家之间的反转行为存在显著的非对称性,这取决于股票相对于价格参考点的价格。这些反转非对称性与假设的价格锚定偏差相符。

IV. 回测表现

年化回报19.6%
波动率30.61%
β值0.121
夏普比率0.64
索提诺比率0.396
最大回撤N/A
胜率52%

V. 完整的 Python 代码

from AlgorithmImports import *
class ReversalCombinedwithVolatility(QCAlgorithm):
    def Initialize(self):
        self.SetStartDate(2000, 1, 1)
        self.SetCash(100000)
        self.SetSecurityInitializer(lambda x: x.SetMarketPrice(self.GetLastKnownPrice(x)))
        self.leverage:int = 10
        self.quantile:int = 5
        self.period:int = 52 * 5
        self.exchange_codes:List[str] = ['NYS', 'NAS', 'ASE']
        
        market:Symbol = self.AddEquity('SPY', Resolution.Daily).Symbol
        
        self.long:List[Symbol] = []
        self.short:List[Symbol] = []
        self.data:Dict[Symbol, SymbolData] = {}
        self.fundamental_count:int = 500
        self.fundamental_sorting_key = lambda x: x.DollarVolume
        
        self.selection_flag:bool = False
        self.Settings.MinimumOrderMarginPortfolioPercentage = 0.
        self.UniverseSettings.Resolution = Resolution.Daily
        self.AddUniverse(self.FundamentalSelectionFunction)
        self.Schedule.On(self.DateRules.MonthEnd(market), self.TimeRules.AfterMarketOpen(market), self.Selection)
        self.settings.daily_precise_end_time = False
    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 the rolling window every day.
        for stock in fundamental:
            symbol:Symbol = stock.Symbol
            # Store monthly price.
            if symbol in self.data:
                self.data[symbol].update(stock.AdjustedPrice)
        if not self.selection_flag:
            return Universe.Unchanged
        selected:List[Fundamental] = [x for x in fundamental if x.HasFundamentalData and x.Market == 'usa' and x.SecurityReference.ExchangeId in self.exchange_codes]
        if len(selected) > self.fundamental_count:
            selected = [x for x in sorted(selected, key=self.fundamental_sorting_key, reverse=True)[:self.fundamental_count]]
        
        selected_ready:List[Fundamental] = []
        # Warmup price rolling windows.
        for stock in selected:
            symbol:Symbol = stock.Symbol
            if symbol not in self.data:
                self.data[symbol] = SymbolData(self.period)
                history:dataframe = self.History(symbol, self.period, Resolution.Daily)
                if history.empty:
                    self.Log(f"Not enough data for {symbol} yet.")
                    continue
                closes:Series = history.loc[symbol].close
                for time, close in closes.items():
                    self.data[symbol].update(close)
            
            if self.data[symbol].is_ready():
                selected_ready.append(stock)
        
        pth_performance:Dict[Symbol, Tuple[float]] = {x.Symbol : (self.data[x.Symbol].pth(), self.data[x.Symbol].performance()) for x in selected_ready}
        sorted_by_pth:List[Tuple[Symbol, float]] = sorted(pth_performance.items(), key = lambda x: x[1][0], reverse = True)
        sorted_by_pth:List[Symbol] = [x[0] for x in sorted_by_pth]
        
        sorted_by_ret:List[Tuple[Symbol, float]] = sorted(pth_performance.items(), key = lambda x: x[1][1], reverse = True)
        sorted_by_ret:List[Symbol] = [x[0] for x in sorted_by_ret]
        quintile:int  = int(len(sorted_by_ret) / self.quantile)
        
        low_pth:List[Symbol] = sorted_by_pth[-quintile:]
        top_ret:List[Symbol] = sorted_by_ret[:quintile]
        low_ret:List[Symbol] = sorted_by_ret[-quintile:]
        
        self.long = [x for x in low_pth if x in low_ret]
        self.short = [x for x in low_pth if x in top_ret]
        
        return self.long + self.short
    def OnData(self, data: Slice) -> None:
        if not self.selection_flag:
            return
        self.selection_flag = False
        
        # Trade execution.
        targets:List[PortfolioTarget] = []
        for i, portfolio in enumerate([self.long, self.short]):
            for symbol in portfolio:
                if symbol in data and data[symbol]:
                    targets.append(PortfolioTarget(symbol, ((-1) ** i) / len(portfolio)))
        
        self.SetHoldings(targets, True)
        self.long.clear()
        self.short.clear()
    def Selection(self) -> None:
        self.selection_flag = True
class SymbolData():
    def __init__(self, period:int):
        self._price:RollingWindow = RollingWindow[float](period)
    
    def update(self, value:float) -> None:
        self._price.Add(value)
    
    def is_ready(self) -> bool:
        return self._price.IsReady
    
    def pth(self) -> float:
        high_proxy = [x for x in self._price]
        symbol_price = high_proxy[0]
        return  symbol_price / max(high_proxy[21:])
    
    def performance(self) -> float:
        closes = [x for x in self._price][:21]
        return (closes[0] / closes[-1] - 1)
# Custom fee model.
class CustomFeeModel(FeeModel):
    def GetOrderFee(self, parameters):
        fee = parameters.Security.Price * parameters.Order.AbsoluteQuantity * 0.00005
        return OrderFee(CashAmount(fee, "USD"))

发表评论

了解 Quant Buffet 的更多信息

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

继续阅读