Improved Cross-Asset Time-Series Momentum I-XTSM
Log in to collectAcademic paper
Cross-Asset Time-Series Momentum Strategy: A New Perspective
dezhong xu; Bin Li; Tarlok Singh; Jung Chul Park
- ?Independent - affiliation not provided to SSRN
- Griffith University
- ?Griffith University - Department of Accounting, Finance and Economics
- University of South Florida
Strategy in a nutshell
Quarterly momentum-based strategy using S&P500 (SPY) and GSCI industrial metals index. Take long/short positions in the stock market based on combined signals of past stock and commodity returns; invest in risk-free asset if signals conflict.
Economic rationale
Industrial metals’ momentum signals have strong predictive power for future stock returns. The strategy exploits this correlation to generate robust excess returns, outperforming traditional TSM/XTSM approaches and mitigating losses during market downturns.
Backtest performance
Annualised return20.82%
Volatility28.96%
Beta0.229
Sharpe ratio0.72
Win rate52%
Full Python code
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