“该策略涉及根据大型股距离其52周高点和最大月度回报的接近程度进行排序,然后做多低NH和低MAX的股票,做空高MAX的股票。”

I. 策略概要

投资范围包括在纽约证券交易所、美国证券交易所或纳斯达克上市的普通股,股票代码为10或11,不包括股价低于5美元的股票。股票被分为三个市值组,其中最大组占前30%,且仅使用最大组。这些股票根据其与52周高点的接近程度(NH)和一个月内的最大日回报(MAX)进行双重排序。MAX计算为该月最高的日回报,NH是当前股价与52周高点的比率。在最低NH五分位中,采用一种策略,即做空MAX五分位最高的股票,做多MAX五分位最低的股票。该策略采用等权重,并每月重新平衡。

II. 策略合理性

过去的研究表明,投资者会高估具有正偏度的股票,特别是那些具有彩票特征的股票,因为他们被极端正回报的潜力所吸引。然而,接近52周高点的股票并未表现出这种异常现象,因为投资者将52周高点视为一个心理障碍,认为价格不会超过该水平。这种认知导致投资者对接近52周高点的彩票型股票缺乏兴趣。相反,远离52周高点的股票往往具有更强的彩票相关异常,因为投资者预期价格上涨并高估极端正回报的可能性。这种偏好在机构持股比例较低的股票中更为强烈,因为散户投资者更容易受到行为偏差的影响。该策略适用于各种股票特征,包括流动性、规模和价格,使其稳健且不限于非流动性、小盘股或低价股。

III. 来源论文

The Role of Psychological Barriers in Lottery-Related Anomalies [点击查看论文]

边锡俊(Byun, Suk-Joon),韩国科学技术院(KAIST)金融工程专业;高志勋(Goh, Jihoon),韩国科学技术院(KAIST)商学院

<摘要>

先前的研究发现,具有彩票特征的股票被高估。我们表明,由投资者彩票偏好引起的异常现象主要存在于远离其52周高价的股票中。结果表明,如果股票接近其52周高点,投资者不再偏好彩票股票,因为他们将52周高点视为心理障碍或价格上限。我们发现,彩票相关异常与接近52周高点之间的依赖性在机构持股比例较低的股票中尤为明显。套利限制和资本利得等替代解释无法解释我们的结果。

IV. 回测表现

年化回报18.58%
波动率19.25%
β值0.438
夏普比率0.76
索提诺比率-0.278
最大回撤N/A
胜率47%

V. 完整的 Python 代码

import numpy as np
from AlgorithmImports import *
from typing import List, Dict
class LotteryStocks52WeekHigh(QCAlgorithm):
    def Initialize(self):
        self.SetStartDate(2010, 1, 1)
        self.SetCash(100000)
        self.exchange_codes: List[str] = ['NYS', 'NAS', 'ASE']    
        self.period: int = 52 * 5
        self.month_period: int = 21
        self.leverage: int = 10
        self.min_share_price: int = 5
        self.quantile: int = 10
        
        self.data: Dict[Symbol, RollingWindow] = {}
        self.long: List[Symbol] = []
        self.short: List[Symbol] = []
        
        market: Symbol = self.AddEquity('SPY', Resolution.Daily).Symbol
        self.fundamental_count: int = 1_000
        self.fundamental_sorting_key = lambda x: x.DollarVolume
        self.selection_flag: bool = True
        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.MonthStart(market), self.TimeRules.AfterMarketOpen(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 the rolling window every day
        for stock in fundamental:
            symbol: Symbol = stock.Symbol
            if symbol in self.data:
                self.data[symbol].Add(stock.AdjustedPrice)
        if not self.selection_flag:
            return Universe.Unchanged
        selected: List[Fundamental] = [
            x for x in fundamental if x.HasFundamentalData and x.Price > self.min_share_price and x.Market == 'usa' \
            and x.MarketCap != 0 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]]
        MAX: Dict[Symbol, float] = {}
        NH: Dict[Symbol, float] = {}
        for stock in selected:
            symbol: Symbol = stock.Symbol
            # warmup price rolling windows
            if symbol not in self.data:
                self.data[symbol] = RollingWindow[float](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].Add(close)
            if not self.data[symbol].IsReady:
                continue
            
            closes: List[float] = list(self.data[symbol])
            last_month_closes: np.ndarray = np.array(closes[:self.month_period])
            last_close: float = closes[0]
            
            daily_returns: np.ndarray = (last_month_closes[:-1] - last_month_closes[1:]) / last_month_closes[1:]
            MAX[symbol] = max(daily_returns)
            
            # NH calc    
            if last_close != 0:
                local_highest_close: float = max(closes)
                NH[symbol] = last_close / local_highest_close
        if len(MAX) < self.quantile or len(NH) < self.quantile:
            return Universe.Unchanged
        # NH sorting
        sorted_by_NH: List[Tuple[Symbol, float]] = sorted(NH.items(), key = lambda x: x[1], reverse = True)
        quintile:int = int(len(sorted_by_NH) / self.quantile)
        low_NH: List[Symbol] = [x[0] for x in sorted_by_NH[-quintile:]]
        
        # MAX sorting
        sorted_by_MAX: List[Tuple[Symbol, float]] = sorted(low_NH, key = lambda x: MAX[x], reverse = True)
        quintile: int = int(len(sorted_by_MAX) / self.quantile)
        
        self.long = [x for x in sorted_by_MAX[:quintile]]
        self.short = [x for x in sorted_by_MAX[-quintile:]]
        return self.long + self.short
    
    def OnData(self, slice: 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 slice and slice[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 CustomFeeModel(FeeModel):
    def GetOrderFee(self, parameters):
        fee = parameters.Security.Price * parameters.Order.AbsoluteQuantity * 0.00005
        return OrderFee(CashAmount(fee, "USD"))

发表评论

了解 Quant Buffet 的更多信息

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

继续阅读