“该策略投资于S&P 500指数(SPY)和无风险资产(如国库券ETF BIL)。结合GSCI工业金属指数(GSCI-IND)作为跨资产信号,回顾期为12个月,持有期为3个月。根据股票和跨资产动量信号,采取以下操作:a) 双信号为正时,买入股票;b) 双信号为负时,卖出股票;c) 股票信号正、跨资产信号负时,买入股票;d) 股票信号负、跨资产信号正时,投资无风险资产。组合每季度重新平衡。”
资产类别:差价合约(CFDs)、ETF、基金、期货 | 区域:美国 | 频率:季度 | 市场:债券、股票 | 关键词:I-XTSM
策略概述
投资范围包括1. 美国股票市场指数,使用标准普尔500指数(S&P500):ETF SPY,和2. 无风险利率资产;可以选择国库券(债券或票据),例如ETF BIL。 (数据主要应来自Bloomberg,联邦储备经济数据(FRED),和DataStream。研究中使用的资产详细描述及数据来源可见于“数据描述”部分及表1。重点在于获得19种常见商品资产现货价格和5种商品指数的准确数据。)
I-XTSM策略使用GSCI工业金属指数(GSCI-IND)作为跨资产(代替债券资产,用于投资决策,类似Pitkäjärvi等人2020年XTSM策略)。回顾期(K)为12个月,持有期(H)为3个月。
<计算广义市场和商品指数的动量>
在每3个月后重新评估之前,遵循以下四种信号进行操作: a) 如果过去股票收益和跨资产信号都是正值,则投资于股票市场资产的多头头寸(买入)。 b) 如果两者都是负值,则建立股票市场资产的空头头寸(卖出)。 c) 如果股票信号为正,跨资产信号为负,表示股票市场看涨。尽管股票有上涨趋势减弱的迹象,但它仍然有正收益,因此采取股票市场多头头寸。 d) 如果股票信号为负,而跨资产信号为正,则不建议投资股票市场。此时策略的状态是“跳出”,投资者应退出股票市场,转而投资无风险资产。
该组合每次只投资于一个资产(100%),并每季度(每三个月)进行再平衡。
策略合理性
研究人员填补了研究空白,通过证明工业金属资产,特别是它们的信号,对未来股票收益具有强大的预测能力,为资产定价的未来研究提供了有力依据。通过回归测试,研究表明,I-XTSM策略的盈利能力主要来源于工业金属信号对股票收益的显著预测能力。即便考虑市场暴露因素,I-XTSM策略仍表现出优异的表现,并解释了TSM和XTSM策略的超额利润。I-XTSM的夏普比率和信息比率高于其他策略,验证了其高性能。I-XTSM的收益并非随机概率的结果,验证了其在时间序列中的稳健盈利能力。该新提出的I-XTSM策略对实践者和学术界都有重要意义。此外,I-XTSM还有效避免了在经济动荡时期因动量崩溃导致的超额损失。
论文来源
Cross-Asset Time-Series Momentum Strategy: A New Perspective [点击浏览原文]
- 徐德忠,李彬,塔洛克·辛格,朴正哲,格里菲斯大学 – 会计、金融与经济系,南佛罗里达大学
<摘要>
我们提出了一种新的投资策略,即改进的跨资产时间序列动量策略(I-XTSM),以提高投资表现。通过分析1990年1月至2021年4月期间25个投资组合和常见商品的数据,我们发现I-XTSM策略在股票市场中大幅提升了盈利能力,并有效避免了动量崩溃。我们还证明了其盈利能力源自于工业金属资产过去信号的预测能力。即便在考虑市场风险敞口的情况下,I-XTSM仍表现出卓越的表现,并解释了其他动量策略的超额利润。


回测表现
| 年化收益率 | 20.82% |
| 波动率 | 28.96% |
| Beta | 0.229 |
| 夏普比率 | 0.72 |
| 索提诺比率 | N/A |
| 最大回撤 | N/A |
| 胜率 | 52% |
完整python代码
from AlgorithmImports import *
# endregion
class ImprovedCrossAssetTimeSeriesMomentumIXTSM(QCAlgorithm):
def Initialize(self):
self.SetStartDate(2005, 1, 1)
self.SetCash(100000)
self.market:Symbol = self.AddEquity('SPY', Resolution.Daily).Symbol
self.bil:Symbol = self.AddEquity('BIL', Resolution.Daily).Symbol
self.industrial_metal_index:Symbol = self.AddData(IndustrialMetalIndex, 'IMI', Resolution.Daily).Symbol
self.leverage:int = 5
self.period:int = 12 * 21
self.selection_months:List[int] = [1, 4, 7, 10]
self.SetWarmup(self.period, Resolution.Daily)
for symbol in [self.market, self.bil]:
self.Securities[symbol.Value].SetLeverage(self.leverage)
self.market_mom:Momentum = self.MOM(self.market, self.period, Resolution.Daily)
self.industrial_metal_mom:Momentum = self.MOM(self.industrial_metal_index, self.period, Resolution.Daily)
self.rebalance_flag:bool = False
self.Settings.MinimumOrderMarginPortfolioPercentage = 0.
self.Schedule.On(self.DateRules.MonthStart(self.market), self.TimeRules.AfterMarketOpen(self.market), self.Selection)
def OnData(self, data: Slice) -> None:
if self.IsWarmingUp:
return
# quarterly rebalance
if not self.rebalance_flag:
return
self.rebalance_flag = False
# check industrial metal index data arrival
industrial_metal_last_update_date:datetime.date = IndustrialMetalIndex.get_last_update_date()
if self.Securities[self.industrial_metal_index].GetLastData():
if self.Time.date() >= industrial_metal_last_update_date:
self.Liquidate()
return
# compare momentums and define traded asset and trade direction
traded_asset:Symbol|None = None
trade_direction:int = 0
if all(x in data and data[x] for x in [self.market, self.industrial_metal_index]):
market_mom:float = self.market_mom.Current.Value
industrial_metal_mom:float = self.industrial_metal_mom.Current.Value
if market_mom >= 0:
traded_asset = self.market
trade_direction = 1
else:
if industrial_metal_mom < 0:
traded_asset = self.market
trade_direction = -1
elif industrial_metal_mom >= 0:
traded_asset = self.bil
trade_direction = 1
# trade execution
if not self.Portfolio[traded_asset].Invested:
self.Liquidate()
self.SetHoldings(traded_asset, trade_direction)
else:
self.SetHoldings(traded_asset, trade_direction)
def Selection(self) -> None:
if self.Time.month in self.selection_months:
self.rebalance_flag = True
# Source: https://www.investing.com/indices/gsci-industrial-metals-historical-data
class IndustrialMetalIndex(PythonData):
def GetSource(self, config, date, isLiveMode):
return SubscriptionDataSource('data.quantpedia.com/backtesting_data/index/GSCI_Industrial_Metal_index.csv', SubscriptionTransportMedium.RemoteFile, FileFormat.Csv)
_last_update_date:datetime.date = datetime(1,1,1).date()
@staticmethod
def get_last_update_date() -> datetime.date:
return IndustrialMetalIndex._last_update_date
def Reader(self, config, line, date, isLiveMode):
data = IndustrialMetalIndex()
data.Symbol = config.Symbol
if not line[0].isdigit(): return None
split = line.split(';')
# Parse the CSV file's columns into the custom data class
data.Time = datetime.strptime(split[0], "%m/%d/%Y") + timedelta(days=1)
data.Value = float(split[1])
if data.Time.date() > IndustrialMetalIndex._last_update_date:
IndustrialMetalIndex._last_update_date = data.Time.date()
return data
