
“该策略交易新兴市场国家,做多高动量的后期输家并做空低动量的后期赢家,采用等权重配置,持有期为六个月,并每月对投资组合进行六分之一的比例再平衡。”
资产类别: ETFs | 地区: 新兴市场 | 周期: 每月 | 市场: 股票 | 关键词: 长期、反转、动量
I. 策略概要
该策略针对来自26个新兴市场国家的股票,每月按60个月的表现进行排名。排名前25%的为后期赢家(LW),排名后25%的为后期输家(LL)。每个子组中的国家进一步按6个月的动量进行排名。投资者做多动量最佳的50%的LL国家,并做空动量最差的50%的LW国家。头寸采用等权重配置,持有期为六个月,投资组合每月进行再平衡,每月调整六分之一的比例以与更新的排名和动量信号保持一致。
II. 策略合理性
该策略结合了两种效应——长期反转策略与短期动量效应。动量效应有助于识别更有可能上涨(下跌)的输家(赢家)股票。
III. 来源论文
长期回报反转:来自国际市场指数的证据 [点击查看论文]
- Malin、Bornholt,格里菲斯大学 – 会计、金融与经济系,格里菲斯大学
<摘要>
本文记录了国际股票市场长期回报反转的证据。我们利用近期的短期表现来更好地选择似乎准备反转的逆向投资标的。与传统的纯粹逆向策略相比,我们的后期逆向策略在应用于发达市场和新兴市场指数时,始终提供了更强的长期回报反转证据。尽管在我们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"))