该策略投资于所有在NYSE、AMEX和NASDAQ上市的股票。在万圣节效应期间(11月至4月),计算过去五年(排除最后一年)中某股票的收益率。将股票分为六个分位组,做多两个最低分位组,做空两个最高分位组。该策略按价值加权,并在冬季月份每月重新平衡。

策略概述

投资范围包括所有在NYSE、AMEX和NASDAQ上市的股票。每年在万圣节效应期间,即从11月至4月,计算过去五年中某一股票的收益率,排除最后一年(t-60到t-13)。将投资组合分为6个分位组,做多两个最低的分位组,做空两个最高的分位组。该策略为价值加权,并在冬季月份中每月重新平衡。

策略合理性

长期反转中的万圣节效应表明,长期反转策略在冬季月份表现更好。历史数据显示,这可能与交易者在夏季的假期有关,甚至有“5月卖出离场”的说法。然而,研究结果表明,该现象主要由小市值的“失败者”投资组合推动,而在大市值的“赢家”投资组合中则不存在这种效应。研究结果对公司规模效应、1月效应以及长期反转的其他定义表现出稳健性。

论文来源

The Halloween Effect in the Long-term Reversal Anomaly (2021) [点击浏览原文]

<摘要>

在这项研究中,我们调查了美国市场上长期反转异象中的万圣节效应。通过检验1931年至2021年期间根据先前收益形成的“失败者减赢家”投资组合的横截面收益,我们发现冬季月份的回报明显强于夏季月份。具体而言,小市值失败者投资组合的季节性显著,而大市值赢家投资组合中则缺乏万圣节效应。这项研究的结果对长期反转效应的不同衡量方法、不同子时期的考量、包括1月效应和异常值的考虑、小市值和大市值公司都表现出稳健性。

回测表现

年化收益率7.26%
波动率12.16%
Beta0.083
夏普比率0.6
索提诺比率-0.068
最大回撤N/A
胜率52%

完整python代码

from AlgorithmImports import *
# endregion

class TheHalloweenEffectWithinLongtermReversal(QCAlgorithm):

    def Initialize(self):
        self.SetStartDate(2000, 1, 1)
        self.SetCash(100000)

        self.quantile:int = 6
        self.liquidate_month:int = 4
        self.trading_months:List[int] = [10, 11, 12, 1, 2, 3]
        self.period:int = 60 * 21 # need five years of daily data
        self.leverage:int = 3
        
        self.prices:Dict[Symbol, RollingWindow] = {}
        self.weight:Dict[Symbol, float] = {}

        self.symbol:Symbol = self.AddEquity('SPY', Resolution.Daily).Symbol

        self.fundamental_count:int = 3000
        self.fundamental_sorting_key = lambda x: x.MarketCap

        self.selection_flag:bool = False
        self.UniverseSettings.Resolution = Resolution.Daily
        self.AddUniverse(self.FundamentalSelectionFunction)
        self.Settings.MinimumOrderMarginPortfolioPercentage = 0
        self.Schedule.On(self.DateRules.MonthEnd(self.symbol), self.TimeRules.BeforeMarketClose(self.symbol, 0), 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]) -> None:
        # update prices on daily basis
        for stock in fundamental:
            symbol:Symbol = stock.Symbol

            if symbol in self.prices:
                self.prices[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 and \
            ((x.SecurityReference.ExchangeId == "NYS") or (x.SecurityReference.ExchangeId == "NAS") or (x.SecurityReference.ExchangeId == "ASE"))
        ]

        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] = {}

        # warm up stock prices
        for stock in selected:
            symbol:Symbol = stock.Symbol
            
            if symbol not in self.prices:
                self.prices[symbol] = RollingWindow[float](self.period)
                history = self.History(symbol, self.period, Resolution.Daily)
                if history.empty:
                    continue

                closes:List = history.loc[symbol].close

                for time, close in closes.items():
                    self.prices[symbol].Add(close)
                
            if self.prices[symbol].IsReady:
                performance[stock] = self.prices[symbol][0] / self.prices[symbol][self.period - 1] - 1

        if len(performance) < 2 * self.quantile:
            return Universe.Unchanged        

        quantile:int = int(len(performance) / self.quantile)
        sorted_by_performance = [x[0] for x in sorted(performance.items(), key=lambda item: item[1])]
        
        # long the two lowest; short the two highest
        long = sorted_by_performance[:(2 * quantile)]
        short = sorted_by_performance[(2 * -quantile):]

        for i, portfolio in enumerate([long, short]):
            mc_sum:float = sum([x.MarketCap for x in 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:
        # 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.weight.items() if symbol in data and data[symbol]]
        self.SetHoldings(portfolio, True)

        self.weight.clear()
        
    def Selection(self) -> None:
        if self.Time.month in self.trading_months:
            self.selection_flag = True
        elif self.Time.month == self.liquidate_month:
            # liquidate portfolio at the end of the April
            self.Liquidate()
        
# Custom fee model
class CustomFeeModel(FeeModel):
    def GetOrderFee(self, parameters):
        fee = parameters.Security.Price * parameters.Order.AbsoluteQuantity * 0.00005
        return OrderFee(CashAmount(fee, "USD"))

Leave a Reply

Discover more from Quant Buffet

Subscribe now to keep reading and get access to the full archive.

Continue reading