“该策略交易新兴市场国家,做多高动量的后期输家并做空低动量的后期赢家,采用等权重配置,持有期为六个月,并每月对投资组合进行六分之一的比例再平衡。”

I. 策略概要

该策略针对来自26个新兴市场国家的股票,每月按60个月的表现进行排名。排名前25%的为后期赢家(LW),排名后25%的为后期输家(LL)。每个子组中的国家进一步按6个月的动量进行排名。投资者做多动量最佳的50%的LL国家,并做空动量最差的50%的LW国家。头寸采用等权重配置,持有期为六个月,投资组合每月进行再平衡,每月调整六分之一的比例以与更新的排名和动量信号保持一致。

II. 策略合理性

该策略结合了两种效应——长期反转策略与短期动量效应。动量效应有助于识别更有可能上涨(下跌)的输家(赢家)股票。

III. 来源论文

长期回报反转:来自国际市场指数的证据 [点击查看论文]

<摘要>

本文记录了国际股票市场长期回报反转的证据。我们利用近期的短期表现来更好地选择似乎准备反转的逆向投资标的。与传统的纯粹逆向策略相比,我们的后期逆向策略在应用于发达市场和新兴市场指数时,始终提供了更强的长期回报反转证据。尽管在我们1989年后的子样本中,发达市场没有出现横截面逆向利润,但纵向分析提供了这一时期反转的有力证据。总体而言,我们的研究结果表明,长期回报的反转可能比普遍理解的更为强劲和普遍。

IV. 回测表现

年化回报15.94%
波动率28.89%
β值-0.055
夏普比率0.41
索提诺比率-0.428
最大回撤N/A
胜率47%

V. 完整的 Python 代码

from AlgorithmImports import *
from math import floor
class LongTermReversalCombinedwithaMomentumEffect(QCAlgorithm):
    def Initialize(self):
        self.SetStartDate(2004, 1, 1)
        self.SetCash(100000)     
        
        self.symbols = [
            "EWJ",  # iShares MSCI Japan Index ETF
            "EWA",  # iShares MSCI Australia ETF
            "EWG",  # iShares MSCI Germany ETF
            "EWU",  # iShares MSCI United Kingdom ETF
            "EWW",  # iShares MSCI Mexico Inv. Mt. Idx
            "EWS",  # iShares MSCI Singapore ETF
            "ERUS", # iShares MSCI Russia ETF
            "IVV",  # iShares S&P 500 Index
            "AAXJ", # iShares MSCI All Country Asia ex Japan Index ETF
            "EWQ",  # iShares MSCI France Index ETF
            "EWH",  # iShares MSCI Hong Kong Index ETF
            "EPI",  # WisdomTree India Earnings ETF
            "EIDO"  # iShares MSCI Indonesia Investable Market Index ETF
            "EWI",  # iShares MSCI Italy Index ETF
            "ENZL", # iShares MSCI New Zealand Investable Market Index Fund
            "NORW"  # Global X FTSE Norway 30 ETF
            "EWY",  # iShares MSCI South Korea Index ETF
            "EWP",  # iShares MSCI Spain Index ETF
            "EWD",  # iShares MSCI Sweden Index ETF
            "EWL",  # iShares MSCI Switzerland Index ETF
            "GXC",  # SPDR S&P China ETF
            "EWC",  # iShares MSCI Canada Index ETF
            "EWZ",  # iShares MSCI Brazil Index ETF
            "ARGT", # Global X FTSE Argentina 20 ETF
            "EWO",  # iShares MSCI Austria Investable Mkt Index ETF
            "EWK",  # iShares MSCI Belgium Investable Market Index ETF
            "ECH",  # iShares MSCI Chile Investable Market Index ETF
            "EGPT", # Market Vectors Egypt Index ETF
        ]
        self.holding_period = 6
        
        self.data = {}
        self.managed_queue = []
        
        self.long_period = 60*21
        self.short_period = 6*21
        self.SetWarmUp(self.long_period, Resolution.Daily)
        
        for symbol in self.symbols:
            data = self.AddEquity(symbol, Resolution.Daily)
            data.SetLeverage(10)
            data.SetFeeModel(CustomFeeModel())
            
            self.data[symbol] = SymbolData(symbol, self.long_period)
        
        self.Schedule.On(self.DateRules.MonthStart(self.symbols[0]), self.TimeRules.AfterMarketOpen(self.symbols[0]), self.Rebalance)
    
    def OnData(self, data):
        for symbol in self.data:
            symbol_obj = self.Symbol(symbol)
            if symbol_obj in data and data[symbol_obj]:
                self.data[symbol].update(data[symbol_obj].Value)
                        
    def Rebalance(self):
        # momentum pair - long and short period momentum.
        momentum = {
            x : (self.data[x].performance(self.long_period), self.data[x].performance(self.short_period)) for x in self.symbols if self.data[x].is_ready()
        }
        
        long = []
        short = []
        if len(momentum) != 0:
            sorted_long_mom = sorted(momentum.items(), key = lambda x: x[1][0])
            quartile = floor(len(sorted_long_mom) / 4)
            long_winners = sorted_long_mom[-quartile:]
            long_loosers = sorted_long_mom[:quartile]
            
            short_term_n = 3
            long_winners_sorted_short_mom = sorted(long_winners, key = lambda x: x[1][1])
            short = [x[0] for x in long_winners_sorted_short_mom][:short_term_n]
            
            long_loosers_sorted_short_mom = sorted(long_loosers, key = lambda x: x[1][1])
            long = [x[0] for x in long_loosers_sorted_short_mom][-short_term_n:]
    
            long_w = self.Portfolio.TotalPortfolioValue / self.holding_period / len(long)
            short_w = self.Portfolio.TotalPortfolioValue / self.holding_period / len(short)
            
            # symbol/quantity collection
            long_symbol_q = [(x, floor(long_w / self.Securities[x].Price)) for x in long]
            short_symbol_q = [(x, -floor(short_w / self.Securities[x].Price)) for x in short]
        
            self.managed_queue.append(RebalanceQueueItem(long_symbol_q + short_symbol_q))
            
        if len(self.managed_queue) == 0: return
    
        remove_item = None
        
        # Rebalance portfolio
        for item in self.managed_queue:
            if item.holding_period == self.holding_period:
                
                # liquidate
                for symbol, quantity in item.symbol_q:
                    self.MarketOrder(symbol, -quantity)
                remove_item = item
                
            elif item.holding_period == 0:
                opened_symbol_q = []
                
                for symbol, quantity in item.symbol_q:
                    self.MarketOrder(symbol, quantity)
                    opened_symbol_q.append((symbol, quantity))
                            
                # Only opened orders will be closed        
                item.symbol_q = opened_symbol_q
                
            item.holding_period += 1
            
        # We need to remove closed part of portfolio after loop. Otherwise it will miss one item in self.managed_queue.
        if remove_item:
            self.managed_queue.remove(remove_item)
class RebalanceQueueItem():
    def __init__(self, symbol_q):
        # symbol/quantity collections
        self.symbol_q = symbol_q
        self.holding_period = 0
class SymbolData():
    def __init__(self, symbol, period):
        self.Symbol = symbol
        self.Price = RollingWindow[float](period)
        
    def update(self, value):
        self.Price.Add(value)
    
    def is_ready(self) -> bool:
        return self.Price.IsReady
        
    def performance(self, days_to_count_in) -> float:
        closes = [x for x in self.Price][:days_to_count_in]
        return (closes[0] / closes[-1] - 1)
# 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 的更多信息

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

继续阅读