
“该策略涉及根据规模和回报预测因子将股票分类到投资组合中,计算价值加权回报,并对51个股票因子进行排名。每月对表现最佳/最差的股票采取多头/空头头寸。”
资产类别: 股票 | 地区: 美国 | 周期: 每月 | 市场: 股票 | 关键词: 动量
I. 策略概要
该投资范围包括纽约证券交易所、美国证券交易所和纳斯达克的股票。投资者通过根据规模和回报预测因子将股票分类到六个投资组合中来构建51种股票因子策略。例如,使用纽约证券交易所的断点(规模的中位数和回报预测因子的30%和70%分位数)和独立排序来构建单一因子策略。一些因子使用离散信号,例如债务发行。投资者计算每个投资组合的价值加权回报,并通过平均高回报投资组合并减去低回报投资组合来计算因子的回报。因子每半年或每月重新平衡,每个月,投资者对因子进行排名,并对表现最佳和最差的因子采取多头/空头头寸,对每个选定的因子进行等权重。
II. 策略合理性
因子动量表现出强大且统计上显著的回报,类似于行业投资组合中的动量。它完全包含行业动量以及基于规模和账面市值的动量,使其更强。即使在控制了股价动量、行业动量和Fama-French五因子之后,这种动量仍然存在。它在不同的股票因子和实施限制下都表现稳健,包括仅使用大盘股或在投资组合构建中引入延迟。因子动量在1963年至2016年的整个样本期内保持一致,即使在2000年后也保持其强度。与可能崩溃的股票动量不同,因子动量在市场复苏期间(例如2009年)获得了显著利润。
III. 来源论文
Factor Momentum [点击查看论文]
- 罗伯特·阿诺特(R. Arnott)、马修·克莱门茨(M. Clements)、维塔利·卡列斯尼克(V. Kalesnik)、约翰·林奈因马(J. Linnainmaa),Research Affiliates, LLC,洛杉矶资本管理公司(Los Angeles Capital Management),Research Affiliates Global Advisors,达特茅斯学院塔克商学院(Dartmouth College – Tuck School of Business);美国国家经济研究局(NBER);Kepos Capital
<摘要>
过去的行业回报预测未来的行业回报,这种可预测性在一个月的时间范围内最强。我们表明,因子回报的横截面也具有这种特性,并且行业动量源于因子动量。因子动量通过行业因子载荷的变化传递到行业回报的横截面。我们表明,在“系统性行业”(由因子构建的模拟投资组合)中的动量,以及行业中性因子中的动量,都包含了行业动量。因此,行业动量是因子动量的副产品,反之则不然。动量完全集中在前几个最高特征值因子中。


IV. 回测表现
| 年化回报 | 10.49% |
| 波动率 | 15.28% |
| β值 | 0.085 |
| 夏普比率 | 0.69 |
| 索提诺比率 | -0.341 |
| 最大回撤 | N/A |
| 胜率 | 47% |
V. 完整的 Python 代码
from AlgorithmImports import *
#endregion
class FactorMomentum(QCAlgorithm):
def Initialize(self):
self.SetStartDate(2000, 1, 1)
self.SetCash(100000)
# daily price data
self.data:Dict[str, float] = {}
self.period:int = 12 * 21
self.SetWarmUp(self.period, Resolution.Daily)
self.leverage:int = 10
self.traded_count:int = 8
csv_string_file:str = self.Download('data.quantpedia.com/backtesting_data/equity/quantpedia_strategies/backtest_end_year.csv')
lines:str = csv_string_file.split('\r\n')
last_id:None|str = None
for line in lines[1:]:
split:str = line.split(';')
id:str = str(split[0])
backtest_to:int = int(split[1])
data:QuantpediaEquity = self.AddData(QuantpediaEquity, id, Resolution.Daily)
data.SetLeverage(self.leverage)
data.SetFeeModel(CustomFeeModel())
self.data[id] = self.ROC(id, self.period, Resolution.Daily)
if not last_id:
last_id = id
self.recent_month:int = -1
self.Settings.MinimumOrderMarginPortfolioPercentage = 0.
def OnData(self, data):
if self.IsWarmingUp:
return
if self.Time.month == self.recent_month:
return
self.recent_month = self.Time.month
if self.Time.month != 1: return
_last_update_date:Dict[str, datetime.date] = QuantpediaEquity.get_last_update_date()
# calculate performance of those strategies
performance:Dict[str, float] = { x : self.data[x].Current.Value for x in self.data \
if self.data[x].IsReady and \
x in data and data[x] and \
_last_update_date[x] > self.Time.date() }
long:List[str] = []
short:List[str] = []
# performance sorting
if len(performance) >= self.traded_count*2:
sorted_by_perf:List[str] = sorted(performance.items(), key = lambda x: x[1], reverse = True)
long = [x[0] for x in sorted_by_perf[:self.traded_count]]
short = [x[0] for x in sorted_by_perf[-self.traded_count:]]
# trade execution
invested:List[str] = [x.Key.Value for x in self.Portfolio if x.Value.Invested]
for symbol in invested:
if symbol not in long + short:
self.Liquidate(symbol)
long_count:int = len(long)
short_count:int = len(short)
for symbol in long:
self.SetHoldings(symbol, 1 / long_count)
for symbol in short:
self.SetHoldings(symbol, -1 / short_count)
# Quantpedia strategy equity curve data.
# NOTE: IMPORTANT: Data order must be ascending (datewise)
class QuantpediaEquity(PythonData):
def GetSource(self, config, date, isLiveMode):
return SubscriptionDataSource("data.quantpedia.com/backtesting_data/equity/quantpedia_strategies/{0}.csv".format(config.Symbol.Value), SubscriptionTransportMedium.RemoteFile, FileFormat.Csv)
_last_update_date:Dict[str, datetime.date] = {}
@staticmethod
def get_last_update_date() -> Dict[str, datetime.date]:
return QuantpediaEquity._last_update_date
def Reader(self, config, line, date, isLiveMode):
data = QuantpediaEquity()
data.Symbol = config.Symbol
if not line[0].isdigit(): return None
split = line.split(';')
data.Time = datetime.strptime(split[0], "%Y-%m-%d") + timedelta(days=1)
data['close'] = float(split[1])
data.Value = float(split[1])
# store last update date
if config.Symbol.Value not in QuantpediaEquity._last_update_date:
QuantpediaEquity._last_update_date[config.Symbol.Value] = datetime(1,1,1).date()
if data.Time.date() > QuantpediaEquity._last_update_date[config.Symbol.Value]:
QuantpediaEquity._last_update_date[config.Symbol.Value] = data.Time.date()
return data
# Custom fee model.
class CustomFeeModel(FeeModel):
def GetOrderFee(self, parameters):
fee = parameters.Security.Price * parameters.Order.AbsoluteQuantity * 0.00005
return OrderFee(CashAmount(fee, "USD"))