该策略针对在纽约证券交易所、美国证券交易所和纳斯达克上市的公司,数据来源于CRSP和I/B/E/S。计算3-2动量信号和12-2动量信号,以作为控制/预测变量。优化阶段寻求短期与长期动量策略的最佳组合权重,目标是最大化夏普比率,使用240个月的数据进行评估。收益计算考虑当前波动率与过去20年的比较,依据波动率分布调整权重。策略在低(高)波动率月份分配不同的动量权重,并每月进行重新平衡。

策略概述

投资范围包括在纽约证券交易所(NYSE)、美国证券交易所(Amex)或纳斯达克(Nasdaq)上市的公司。(基本数据和股票数据可从CRSP获取。共识(中位数)每股收益(EPS)预测可从I/B/E/S未经调整的汇总文件获取。)

<策略计算和执行>

  1. 计算3-2动量信号,代表组合形成前三个月的股票收益,跳过组合形成前一个月,以及适当的12-2动量。它们将作为下一步中的控制/预测变量使用。
  2. 进行优化,我们在这一步中寻找短期动量策略和长期动量策略的组合权重,两个版本的总权重为100%。优化在240个月的数据上进行,我们寻找一个在样本内最大化夏普比率的权重,基于当前月份的波动率与过去20年数据的波动率进行比较。实时混合动量策略的收益计算为 r_MM,t+1 = (1 − w^∗_t *1_HV,t) * r_LM,t+1 + w^∗t * 1_HV,t * r_SM,t+1,其中 1_HV,t 是一个指示变量,如果月份 t 的波动率在过去 20 年的波动率分布中位于第 70 个百分位以上,则为 1,否则为 0。在下一步中使用计算得出的估计β系数。
  3. 如果 t – 1 月是低(高)波动率月份,则混合动量策略在长期动量上分配权重为 1(1 − w^∗_t−1),在短期动量上分配权重为 0(w^∗_t−1)。

策略由前面的步骤加权并每月重新平衡。

策略合理性

这篇论文中的研究可以重现盈利预期的僵化模式。基于内生性注意力的简单模型的资产定价含义表明,它与资产定价文献中几个已知的规律一致:动量的无条件盈利性(Jegadeesh和Titman,1993)、动量崩盘(Daniel和Moskowitz,2016)、动量的衰减(Chordia等人,2014)以及波动率管理的动量的增强盈利性(Barroso和Santa-Clara,2015;Moreira和Muir,2017)。无条件下,长期动量优于短期动量,但在高波动率期间,短期和长期动量的相对盈利性会发生变化。一种旨在利用这种模式的交易策略显著提高了动量收益。

论文来源

Earnings Expectations and Asset Prices [点击浏览原文]

<摘要>
这篇论文记录了关于股票分析师盈利预期的以下事实:(1) 共识盈利预期对新闻的反应在无条件情况下存在反应不足,(2) 在高波动期间,反应不足程度下降,(3) 反应不足程度在我们的样本中经历了持续下降。为了解释这些发现,我们开发了一个简单的模型,其中包含内生性注意力。我们展示了我们的模型能够解释动量的无条件盈利能力、动量崩盘、随着时间推移的动量衰减以及波动管理动量的增强盈利能力。最后,我们提出了一种实时交易策略,在高波动时期混合短期和长期动量策略,并展示了由此产生的交易策略相对于传统动量策略产生了经济上可观的收益。

回测表现

年化收益率13.83%
波动率21.09%
Beta-0.098
夏普比率0.66
索提诺比率0.118
最大回撤N/A
胜率51%

Python代码及解释

完整python代码

from AlgorithmImports import *
import pandas as pd
import numpy as np
from collections import deque
import data_tools
from numpy import isnan
from pandas.core.frame import DataFrame
from pandas.core.series import Series

class SwitchingbetweenValueMomentum(QCAlgorithm):

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

        self.exchange_codes: List[str] = ['NYS', 'NAS', 'ASE']	
        self.ticker_to_ignore: List[str] = ['TOPS', 'SSCC']

        self.data: Dict[Symbol, SymbolData] = {}
        self.performance_data: DataFrame = pd.DataFrame()

        self.period: int = 120
        self.data_period: int = 12
        self.month_period: int = 21
        self.leverage: int = 5
        self.quantile: int = 10
        self.min_traded_weight: float = 0.00001
        self.short_momentum_period: int = 3
        self.long_momentum_period: int = 12

        self.short_term_long: List[Symbol] = []
        self.short_term_short: List[Symbol] = []
        self.long_term_long: List[Symbol] = []
        self.long_term_short: List[Symbol] = []

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

        self.fundamental_count: int = 500
        self.fundamental_sorting_key = lambda x: x.DollarVolume

        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)

    def OnSecuritiesChanged(self, changes: SecurityChanges) -> None:
        for security in changes.AddedSecurities:
            security.SetFeeModel(data_tools.CustomFeeModel())
            security.SetLeverage(self.leverage)
            
    def FundamentalSelectionFunction(self, fundamental: List[Fundamental]) -> List[Symbol]:
        if not self.selection_flag:
            return Universe.Unchanged

        # update the rolling window every month
        for stock in fundamental:
            symbol: Symbol = stock.Symbol
            
            # store monthly price
            if symbol in self.data:
                self.data[symbol].update(stock.AdjustedPrice)

        selected: List[Fundamental] = [x for x in fundamental if x.HasFundamentalData and x.Market == 'usa' \
                                    and x.SecurityReference.ExchangeId in self.exchange_codes and x.Symbol.Value not in self.ticker_to_ignore]

        if len(selected) > self.fundamental_count:
            selected = [x for x in sorted(selected, key=self.fundamental_sorting_key, reverse=True)[:self.fundamental_count]]

        short_term_momentum: Dict[Symbol, float] = {}
        long_term_momentum: Dict[Symbol, float] = {}

        # warmup price rolling windows
        for stock in selected:
            symbol: Symbol = stock.Symbol

            if symbol not in self.data:
                self.data[symbol] = data_tools.SymbolData(self.data_period)
                history: DataFrame = self.History(symbol, self.data_period * self.month_period, Resolution.Daily)
                if history.empty:
                    self.Log(f"Not enough data for {symbol} yet.")
                    continue
                data: Series = history.loc[symbol]
                monthly_data: Series = data.groupby(pd.Grouper(freq='MS')).last()
                for time, row in monthly_data.iterrows():
                    self.data[symbol].update(row.close)
            
            if self.data[symbol].is_ready():
                short_term_momentum[symbol] = self.data[symbol].momentum(self.short_momentum_period)
                long_term_momentum[symbol] = self.data[symbol].momentum(self.long_momentum_period)
        
        if len(short_term_momentum) >= self.quantile and len(long_term_momentum) >= self.quantile:
            # sorting by long term and short term momentum
            sorted_short_term_momentum: List[Symbol] = sorted(short_term_momentum, key = short_term_momentum.get, reverse=True)
            sorted_long_term_momentum: List[Symbol] = sorted(long_term_momentum, key = long_term_momentum.get, reverse=True)
            quantile: int = int(len(sorted_short_term_momentum) / self.quantile)

            self.short_term_long = sorted_short_term_momentum[:quantile]
            self.short_term_short = sorted_short_term_momentum[-quantile:]
            self.long_term_long = sorted_long_term_momentum[:quantile]
            self.long_term_short = sorted_long_term_momentum[-quantile:]
        
        return self.short_term_long + self.short_term_short + self.long_term_long + self.long_term_short

    def OnData(self, data: Slice) -> None:
        if not self.selection_flag:
            return
        self.selection_flag = False
        
        if len(set(self.short_term_long + self.short_term_short)) == 0 or len(set(self.long_term_long + self.long_term_short)) == 0: 
            return
    
        portfolio_list: List[List[Symbol]] = [self.short_term_long, self.short_term_short, self.long_term_long, self.long_term_short]
        
        # optimization process
        price_data: List[Dict[Symbol, List[float]]] = [{ symbol : self.data[symbol].get_prices() for symbol in portfolio if symbol in data and data[symbol]} for portfolio in portfolio_list]
        returns_df_list: List[DataFrame] = [pd.DataFrame(portfolio_prices, columns=portfolio_prices.keys()).pct_change().dropna() for portfolio_prices in price_data]

        # store factors' performance
        df_returns: DataFrame = pd.concat([(returns_df_list[0].sum(axis=1) - returns_df_list[1].sum(axis=1)), (returns_df_list[2].sum(axis=1) - returns_df_list[3].sum(axis=1))], axis=1)
        self.performance_data = pd.concat([self.performance_data, df_returns], axis=0)
        
        if len(self.performance_data.index) < self.period:
            return

        self.performance_data = self.performance_data[-self.period:]
        optimiztion = data_tools.PortfolioOptimization(self.performance_data, 0, df_returns.shape[1])
        opt_weight = optimiztion.opt_portfolio()
        
        if isnan(sum(opt_weight)):
            return
        
        trade_quantities: Dict[Symbol, float] = {}

        for i, term in enumerate([[self.short_term_long, self.short_term_short], [self.long_term_long, self.long_term_short]]):
            for n, portfolio in enumerate(term):
                w: float = opt_weight[i]
                for symbol in portfolio:
                    if w > self.min_traded_weight:
                        if symbol in data and data[symbol]:
                            quantity: float = ((self.Portfolio.TotalPortfolioValue / len(portfolio)) * w) // data[symbol].Price
                            if symbol not in trade_quantities:
                                trade_quantities[symbol] = 0
                            trade_quantities[symbol] += ((-1) ** n) * quantity

        # trade execution
        stocks_invested: List[Symbol] = [x.Key for x in self.Portfolio if x.Value.Invested]
        for symbol in stocks_invested:
            if symbol not in trade_quantities:
                self.Liquidate(symbol)

        for symbol, new_quantity in trade_quantities.items():
            quantity:float = new_quantity - self.Portfolio[symbol].Quantity
            if abs(quantity) >= 1.:
                self.MarketOrder(symbol, quantity)

    def Selection(self) -> None:
        self.selection_flag = True

Leave a Reply

Discover more from Quant Buffet

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

Continue reading