
“该策略投资于过去36个月回报最高的ETF的最高十分位数,并做空最低十分位数,使用价值加权投资组合,每月重新平衡。”
资产类别: ETF | 地区: 美国 | 周期: 每月 | 市场: 股票 | 关键词: 动量
I. 策略概要
该投资范围包括在美国CRSP数据库上市的ETF,市值至少2000万美元,股价高于1美元。ETF根据过去36个月的累计回报进行排序。投资者做多回报最高的十分位ETF,做空回报最低的十分位ETF。投资组合采用价值加权,每月重新平衡。
II. 策略合理性
该研究回顾了推动ETF动量的各种因素。研究发现,ETF动量回报呈现出持续性,随后出现反转,利润在78个月后逐渐消失。与股票动量不同,ETF动量不能用与基准股票策略或宏观经济风险的协变来解释。即使在标准普尔500指数回报较低的时期,该策略也持续产生正回报。股票特征和流动性风险也不能解释ETF动量。值得注意的是,ETF动量在持有大盘股的ETF中更为显著。这表明ETF中的动量与股票动量不同,可能由其他市场动态驱动。
III. 来源论文
ETF Momentum [点击查看论文]
- 李维凯(Weikai Li)、张明杰(Melvyn Teo)和杨楚翘(Chloe Yang),新加坡管理大学李光前商学院;新加坡管理大学李光前商学院;复旦大学泛海国际金融学院(FISF)
<摘要>
我们发现,当根据过去两到四年的回报对ETF进行排序时,动量利润在经济上是巨大的。基于ETF动量的价值加权多空策略每月可产生高达1.20%的Carhart(1997)四因子阿尔法。无论是横截面股票动量还是与宏观经济和流动性风险的协变都无法解释ETF动量。相反,持有期后的回报最符合延迟过度反应的行为故事。虽然ETF动量在多次交易成本调整后仍然存在,但由于利润波动且集中在具有高特质波动性或持有分析师覆盖率低的股票的ETF中,因此可能难以套利。


IV. 回测表现
| 年化回报 | 16.08% |
| 波动率 | 25.78% |
| β值 | 0.39 |
| 夏普比率 | 0.62 |
| 索提诺比率 | 0.325 |
| 最大回撤 | N/A |
| 胜率 | 53% |
V. 完整的 Python 代码
from AlgorithmImports import *
class ETFMomentum(QCAlgorithm):
def Initialize(self):
self.SetStartDate(2008, 1, 1)
self.SetCash(100000)
self.market = self.AddEquity('SPY', Resolution.Daily).Symbol
# Daily ROC data with market cap.
self.data = {}
self.period = 36 * 21
self.SetWarmUp(self.period)
# Data source: https://etfdb.com/screener/#page=4&tab=overview&sort_by=assets&sort_direction=desc&asset_class=equity®ions=U.S.&inception_on_start=2005-01-01&inception_on_end=2020-09-01
csv_string_file = self.Download('data.quantpedia.com/backtesting_data/economic/us_equities.csv')
# header: symbol;etf_name;total_assets_mm;previous_closing_price
lines = csv_string_file.split('\r\n')
for line in lines[1:]:
line_split = line.split(';')
etf_symbols = line_split[0]
market_cap = float(line_split[2]) # $MM
last_price = float(line_split[3])
if market_cap > 20 and last_price > 1:
data = self.AddEquity(etf_symbols, Resolution.Daily)
data.SetLeverage(5)
self.data[etf_symbols] = (self.ROC(etf_symbols, self.period, Resolution.Daily), market_cap)
self.Schedule.On(self.DateRules.MonthStart(self.market), self.TimeRules.AfterMarketOpen(self.market), self.Rebalance)
def Rebalance(self):
performance = {x : self.data[x][0].Current.Value for x in self.data if self.data[x][0].IsReady}
sorted_by_performance = sorted(performance.items(), key = lambda x: x[1], reverse = True)
decile = int(len(sorted_by_performance) / 10)
# Symbol, market cap tuples.
long = [(x[0], self.data[x[0]][1]) for x in sorted_by_performance[:decile]]
short = [(x[0], self.data[x[0]][1]) for x in sorted_by_performance[-decile:]]
# Market cap weighting.
weight = {}
total_market_cap_long = sum([x[1] for x in long])
for symbol, market_cap in long:
weight[symbol] = market_cap / total_market_cap_long
total_market_cap_short = sum([x[1] for x in short])
for symbol, market_cap in short:
weight[symbol] = -market_cap / total_market_cap_short
# Trade execution.
invested = [x.Key for x in self.Portfolio if x.Value.Invested]
for symbol in invested:
if symbol not in weight:
self.Liquidate(symbol)
for symbol, w in weight.items():
if self.Securities[symbol].Price != 0 and self.Securities[symbol].IsTradable:
self.SetHoldings(symbol, w)