“该策略根据过往表现和彩票代理(MAX)对美国股票进行排序,然后在亏损组合中做多最低彩票十分位数,做空最高彩票十分位数,每月重新平衡。”

I. 策略概要

该投资范围包括CRSP数据库中的美国股票(在纽约证券交易所、美国证券交易所或纳斯达克交易),不包括股价低于1美元的股票、封闭式基金和房地产投资信托基金。该策略涉及根据过去3至1个月的股票表现(PFM)进行条件双变量投资组合排序,创建三个投资组合:赢家、输家和中间。在输家投资组合中,股票根据彩票代理(MAX或LTRY)进一步分为五分位数。MAX代理是上个月的最大日回报。该策略涉及做多最低彩票十分位数,做空最高彩票十分位数。投资组合采用价值加权,每月重新平衡。

II. 策略合理性

彩票投资者受行为偏差影响,偏爱表现不佳的彩票型股票,认为它们有更高的反弹潜力。这导致此类股票定价过高,可以在交易策略中加以利用。这些投资者认为,表现良好的股票获得显著回报的机会较低,而表现不佳的股票则有更好的上涨机会。没有彩票型特征的股票可能会继续表现不佳,这进一步证明了对表现不佳的彩票型股票的偏好。这种偏差导致股票定价错误,这可以作为交易策略中的机会。

III. 来源论文

Gambling Preferences for Loser Stocks [点击查看论文]

<摘要>

我发现投资者对赌博的偏好主要集中在过去三个月表现不佳的股票上,因为表现不佳的彩票型股票比表现良好的彩票型股票更有可能产生巨额回报(61.53% vs. 40.17%)。此外,彩票投资者倾向于认为表现不佳的彩票型股票可能在短期内强劲反弹,而表现良好的彩票型股票由于价格高昂,产生高额正回报的可能性较小。因此,表现不佳的彩票型股票具有非常有效的彩票型外观,从而吸引了彩票投资者。另一方面,没有彩票型特征的亏损股票可能会继续表现不佳。对具有(不具有)彩票型特征的股票过于乐观(悲观)的信念导致亏损股票中出现显著的彩票溢价。

IV. 回测表现

年化回报28.78%
波动率28.38%
β值-0.814
夏普比率1.23
索提诺比率-0.183
最大回撤N/A
胜率55%

V. 完整的 Python 代码

import numpy as np
from AlgorithmImports import *
from pandas.core.frame import dataframe
class LotteryStocksandPastPerformance(QCAlgorithm):
    def Initialize(self):
        self.SetStartDate(2000, 1, 1)  
        self.SetCash(100000) 
        market:Symbol = self.AddEquity('SPY', Resolution.Daily).Symbol
        
        self.period:int = 3 * 21
        self.performance_quantile:int = 3
        self.lottery_quantile:int = 10
        self.leverage:int = 5
        self.fundamental_count:int = 1000
        self.fundamental_sorting_key = lambda x: x.DollarVolume
        
        self.weight:Dict[Symbol, float] = {}
        
        # Daily price data.
        self.data:Dict[Symbol, RollingWindow] = {}
        
        self.selection_flag:bool = False
        self.UniverseSettings.Resolution = Resolution.Daily
        self.AddUniverse(self.FundamentalSelectionFunction)
        self.Settings.MinimumOrderMarginPortfolioPercentage = 0.
        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].Add(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.MarketCap != 0]
        if len(selected) > self.fundamental_count:
            selected = [x for x in sorted(selected, key=self.fundamental_sorting_key, reverse=True)[:self.fundamental_count]]
        
        performance:Dict[Fundamental, float] = {}
        # Warmup price rolling windows.
        for stock in selected:
            symbol:Symbol = stock.Symbol
            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:pd.Series = history.loc[symbol].close
                for time, close in closes.items():
                    self.data[symbol].Add(close)
            
            if self.data[symbol].IsReady:
                performance[stock] = self.data[symbol][0] / self.data[symbol][self.period-1] - 1
        if len(performance) >= self.performance_quantile * self.lottery_quantile:
            # Performance sorting.
            sorted_by_performance:List[Fundamental] = sorted(performance, key = performance.get, reverse = True)
            quantile:int = int(len(sorted_by_performance) / self.performance_quantile)
            losers:List[Fundamental] = sorted_by_performance[-quantile:]
            
            # MAX calc.
            lottery:Dict[Fundamental, float] = {}
            for stock in losers:
                daily_closes:np.ndarray = np.array([x for x in self.data[stock.Symbol]][:21])
                daily_returns:np.ndarray = (daily_closes[:-1] - daily_closes[1:]) / daily_closes[1:]
                lottery[stock] = max(daily_returns)
            
            # Lottery sorting.
            sorted_by_lottery = sorted(lottery, key = lottery.get, reverse = True)
            quantile:int = int(len(lottery) / self.lottery_quantile)
            long:List[Fundamental] = sorted_by_lottery[-quantile:]
            short:List[Fundamental] = sorted_by_lottery[:quantile]
        
            # Market cap weighting.
            for i, portfolio in enumerate([long, short]):
                mc_sum:float = sum(map(lambda x: x.MarketCap, portfolio))
                for stock in portfolio:
                    self.weight[stock.Symbol] = ((-1) ** i) * stock.MarketCap / mc_sum
    
        return list(self.weight.keys())
    
    def OnData(self, data: Slice) -> None:
        if not self.selection_flag:
            return
        self.selection_flag = False
        
        # Trade execution.
        portfolio:List[PortfolioTarget] = [PortfolioTarget(symbol, w) for symbol, w in self.weight.items() if symbol in data and data[symbol]]
        self.SetHoldings(portfolio, True)
        self.weight.clear()
                        
    def Selection(self) -> None:
        self.selection_flag = True
# 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 的更多信息

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

继续阅读