“该策略将快速和慢速动量信号与状态依赖的阿尔法值相结合,针对市场状况(修正或反弹)进行优化,使用历史数据,并每月重新平衡,持续15年。”

I. 策略概要

该策略侧重于CRSP公司在纽约证券交易所、美国证券交易所或纳斯达克上市的美国超额价值加权因子(Mkt-RF)。它使用快速的1个月动量信号和慢速的12个月动量信号,并将它们与状态依赖变量alpha相结合。在牛市或熊市中,当信号一致时,alpha设置为0.5。然而,在修正或反弹状态下,alpha根据市场情况而变化,通过最大化下个月的夏普比率来计算。该策略使用历史估计进行实际应用,并每月重新平衡。基于77.5年的数据,在15年期间的交易策略中使用了状态依赖的alpha来优化性能。

II. 策略合理性

该论文根据慢速和快速动量指标的一致性,识别出四种市场周期:牛市、修正、熊市和反弹。在修正状态下,慢速和快速动量指标不一致,慢速动量指示做多,快速动量指示做空。在反弹状态下,指标显示相反。混合慢速和快速动量信号可以提高投资组合利润,但混合因子应在不同周期中变化以最大化夏普比率,从而优于静态方法。混合策略产生更高的夏普比率、更小的回撤、更正的偏度以及更强的可预测性。在牛市或熊市状态下,动量信号一致,而在修正和反弹状态下,经济意外不那么显著,这表明宏观环境可能发生变化。研究得出结论,根据市场周期量身定制的动态动量策略比传统的静态策略更有效。

III. 来源论文

Momentum Turning Points [点击查看论文]

<摘要>

我们使用慢速和快速时间序列动量来表征四个股票市场周期——牛市、修正、熊市和反弹。熊市的急剧市场下跌集中在高风险状态,但预测负预期回报,这在大多数时变风险溢价模型中难以合理化。使用模型分析慢速和快速动量策略,我们估计美国股票市场回报中存在相对较高的均值持续性和实现噪声。通过混合慢速和快速动量策略形成的中速动量投资组合,将市场周期中的预测信息转化为正的无条件阿尔法,为此我们提出了一种新颖的分解方法。

IV. 回测表现

年化回报6.11%
波动率10%
β值0.55
夏普比率0.61
索提诺比率0.168
最大回撤N/A
胜率68%

V. 完整的 Python 代码

from AlgorithmImports import *
class DynamicMomentumStrategy(QCAlgorithm):
    def Initialize(self):
        self.SetStartDate(2010, 1, 1)
        self.SetCash(100000)
        
        self.slow_period = 12*21
        self.fast_period = 21
        
        # subscribe 
        data = self.AddData(QuantpediaFutures, 'CME_ES1', Resolution.Daily)     # E-mini S&P 500 Futures, Continuous Contract #1
        data.SetFeeModel(CustomFeeModel())
        data.SetLeverage(5)
        self.market = data.Symbol
        
        # daily price data
        self.price_data = RollingWindow[float](self.slow_period)
        self.recent_month:int = -1
    def OnData(self, data):
        # check if data is still coming.
        if self.securities[self.market].get_last_data() and self.time.date() > QuantpediaFutures.get_last_update_date()[self.market]:
            self.liquidate()
            return
        # store daily market price
        if self.market in data and data[self.market]:
            self.price_data.Add(data[self.market].Value)
            if self.recent_month != self.Time.month:
                self.recent_month = self.Time.month
                
                if self.price_data.IsReady:
                    slow_momentum = self.price_data[0] / self.price_data[self.price_data.Count-1] - 1
                    fast_momentum = self.price_data[0] / self.price_data[21] - 1
                    
                    slow_signal = 1 if slow_momentum >= 0 else -1
                    fast_signal = 1 if fast_momentum >= 0 else -1
                    
                    # market cycles
                    # A month ending at date t is classified as Bull if both the trailing 12-month return (arithmetic average monthly return), rt−12,t, is nonnegative and
                    # the trailing 1-month return, rt−1,t, is nonnegative. A month is classified as Correction if rt−12,t ≥ 0 but rt−1,t < 0; as Bear if rt−12,t < 0 and rt−1,t < 0; 
                    # and as Rebound if rt−12,t < 0 but rt−1,t ≥ 0. 
                    bull = slow_signal == 1 and fast_signal == 1
                    bear = slow_signal == -1 and fast_signal == -1
                    correction = slow_signal == 1 and fast_signal == -1
                    rebound = slow_signal == -1 and fast_signal == 1
                    
                    alpha = 0
                    
                    # if the market`s state is bear or bull – the alpha is not important since the signals agree and it could be set at one half
                    if bull or bear:
                        alpha = 0.5
                    # source: https://papers.ssrn.com/sol3/papers.cfm?abstract_id=3489539
                    # Table 6
                    elif correction:
                        alpha = 0.16
                    elif rebound:
                        alpha = 0.69
                    
                    # weight calculation
                    w = ((1-alpha) * slow_signal) + (alpha*fast_signal)
                    self.SetHoldings(self.market, w)
# Custom fee model.
class CustomFeeModel(FeeModel):
    def GetOrderFee(self, parameters):
        fee = parameters.Security.Price * parameters.Order.AbsoluteQuantity * 0.00005
        return OrderFee(CashAmount(fee, "USD"))
        
# Quantpedia data.
# NOTE: IMPORTANT: Data order must be ascending (datewise)
class QuantpediaFutures(PythonData):
    _last_update_date:Dict[Symbol, datetime.date] = {}
    @staticmethod
    def get_last_update_date() -> Dict[Symbol, datetime.date]:
       return QuantpediaFutures._last_update_date
    def GetSource(self, config, date, isLiveMode):
        return SubscriptionDataSource("data.quantpedia.com/backtesting_data/futures/{0}.csv".format(config.Symbol.Value), SubscriptionTransportMedium.RemoteFile, FileFormat.Csv)
    def Reader(self, config, line, date, isLiveMode):
        data = QuantpediaFutures()
        data.Symbol = config.Symbol
        
        if not line[0].isdigit(): return None
        split = line.split(';')
        
        data.Time = datetime.strptime(split[0], "%d.%m.%Y") + timedelta(days=1)
        data['back_adjusted'] = float(split[1])
        data['spliced'] = float(split[2])
        data.Value = float(split[1])
        if config.Symbol not in QuantpediaFutures._last_update_date:
            QuantpediaFutures._last_update_date[config.Symbol] = datetime(1,1,1).date()
        if data.Time.date() > QuantpediaFutures._last_update_date[config.Symbol]:
            QuantpediaFutures._last_update_date[config.Symbol] = data.Time.date()
        return data

发表评论

了解 Quant Buffet 的更多信息

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

继续阅读