板块基于动量和季节性每月评分,依据阈值进行仓位分配:得分高于9时做多,低于3时做空,得分超过6时平仓。

策略概述

该策略将股票板块分类为周期性(材料、工业、非必需消费品)、防御性(必需消费品、医疗保健、电信、公用事业)或中性板块。每月根据以下三个因素为板块打分:

  1. 12个月动量:最强的板块得4分,次强得3分,以此类推。
  2. 1个月动量:评分方式同上。
  3. 季节性:冬季(11月至次年4月),周期性板块得4分;夏季(5月至10月),防御性板块得4分;中性板块始终得2分。

最终得分在0至12分之间。每月根据得分排名,按以下阈值分配仓位:

当得分超过6时平仓,以平衡表现与换手率。

得分高于9的板块做多。

得分低于3的板块做空。

经济基础

Kamstra、Kramer 和 Levi(2003)将股市收益的季节性归因于季节性情感障碍(SAD),这种情绪与秋冬季较短的白昼时间有关。Doeswijk(2004)提出,年末的乐观情绪源于市场参与者对经济和盈利增长的预期。动量的延续性主要受到行为偏差的驱动,包括投资者的羊群效应、过度反应、反应不足和确认偏差,这些心理因素会影响股票价格的波动,并维持收益趋势。这些心理和季节性因素共同解释了股市行为的模式及动量效应的延续。

论文来源

Global Tactical Sector Allocation: A Quantitative Approach [点击浏览原文]

<摘要>

本研究检验了全球战术板块配置(GTSA)中的七个变量。我们构建了1970年至2008年间的10个全球板块指数,以测试已记录的变量是否具有全球适用性,以及其在发表后是否持续有效。研究发现,动量(1个月与12-1个月)、盈利修正和“5月卖出”季节性效应在发表后仍能带来显著收益。相比之下,货币政策和估值指标(均值回归与股息收益率)无法预测全球板块收益。样本外测试显示,表现平均衰减约三分之一。结合动量与季节性的多空GTSA策略,年成功率达82%,交易成本后年复合收益率为9.9%。据我们所知,这项研究在如此长的样本期间和广泛的变量范围下对全球板块配置进行了独特分析。

回测表现

年化收益率12.9%
波动率17%
Beta-0.734
夏普比率0.52
索提诺比率N/A
最大回撤-29.9%
胜率37%

完整python代码



from AlgorithmImports import *
#endregion
class SeasonalityandMomentum(QCAlgorithm):
    def Initialize(self):
        self.SetStartDate(2007, 1, 1)
        self.SetCash(100000)
        self.cyclical = ["VAW", "XLI", "XLY"]
        self.defensive = ["XLP", "XLV", "VGT", "XLU"]
        self.neutral = ["XLK", "XLF", "XLE", "VNQ"]
        self.symbols = self.cyclical + self.defensive + self.neutral
        self.period = 21
        self.SetWarmUp(self.period)
        
        self.short_momentum = {}
        self.long_momentum = {}
        
        for symbol in self.symbols:
            data = self.AddEquity(symbol, Resolution.Daily)
            data.SetLeverage(10)
            data.SetFeeModel(CustomFeeModel())
            
            self.short_momentum[symbol] = self.ROC(symbol, self.period, Resolution.Daily)
            self.long_momentum[symbol] = self.ROC(symbol, 12*self.period, Resolution.Daily)
            
        self.recent_month = -1
    def OnData(self, data):
        if self.IsWarmingUp: return
        
        if self.Time.month == self.recent_month:
            return
        self.recent_month = self.Time.month
        
        returns_12M = { x : self.long_momentum[x].Current.Value for x in self.symbols if self.long_momentum[x].IsReady and x in data and data[x] }
        returns_1M = { x : self.short_momentum[x].Current.Value for x in self.symbols if self.short_momentum[x].IsReady and x in data and data[x] }
        
        if len(returns_12M) < 4 and len(returns_1M) < 4:
            self.Liquidate()
            return
        
        score = { x : 0 for x in self.symbols }
        
        # 12M Momentum Sorting
        count = 4
        sorted_by_12M = sorted(returns_12M.items(), key=lambda x: x[1], reverse = True)
        sorted_by_12M = [x[0] for x in sorted_by_12M][:count]
        points = count
        for symbol in sorted_by_12M:
            score[symbol] += points
            points -= 1
        # 1M Momentum Sorting
        sorted_by_1M = sorted(returns_1M.items(), key=lambda x: x[1], reverse = True)
        sorted_by_1M = [x[0] for x in sorted_by_1M][:count]
        points = count
        for symbol in sorted_by_1M:
            score[symbol] += points
            points -= 1
        
        # Seasonality score
        for symbol in self.neutral:
            score[symbol] += 2
        if self.Time.month <= 4 or self.Time.month >= 11 :
            for symbol in self.cyclical:
                score[symbol] += 4
        elif self.Time.month >= 5 and self.Time.month <= 10:
            for symbol in self.defensive:
                score[symbol] += 4
        
        # Trade execution
        long = [x[0] for x in score.items() if x[1] > 9]
        short = [x[0] for x in score.items() if x[1] < 3]
        invested = [x.Key.Value for x in self.Portfolio if x.Value.Invested]
        for symbol in invested:
            if symbol not in long + short:
                self.Liquidate(symbol)
                
        for symbol in long:
            self.SetHoldings(symbol, 1 / len(long))
        for symbol in short:
            self.SetHoldings(symbol, -1 / len(short))
# 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