Time Series Reversal of Momentum on Futures
Log in to collectAcademic paper
Time Series Reversal of Financial Assets
Jiadong Liu; Fotis Papailias
- Queen's University Belfast
- ?Queen's University Belfast - Queen's Management School
- King's College London
- University of London
- ?Quantf Research
- ?University of London, King's College London, Department of Management
Strategy in a nutshell
This strategy trades 24 commodity futures, 9 forex futures, 9 developed equity indexes, and 13 government bonds. Trend-following signals are generated using 12-month simple moving averages (SMA) from months T-23 to T-12. Futures with negative SMA signals but positive recent returns (“Fault Losers”) are selected and held for month T+1. Positions are rebalanced monthly, and returns are scaled by annualized ex-ante volatility, ensuring systematic risk-adjusted portfolio management.
Economic rationale
The strategy exploits short-term under-reaction and delayed over-reaction in markets. Time-series reversal arises from a security’s autocorrelation, while trend continuation and reversal interplay, reflecting behavioral patterns in asset returns and supporting systematic momentum and reversal strategies.
Backtest performance
Full Python code
import numpy as np
from AlgorithmImports import *
class TimeSeriesReversal(QCAlgorithm):
def Initialize(self):
self.SetStartDate(2000, 1, 1)
self.SetCash(100000)
self.symbols = ["CME_S1", # Soybean Futures, Continuous Contract
"CME_W1", # Wheat Futures, Continuous Contract
"CME_SM1", # Soybean Meal Futures, Continuous Contract
"CME_BO1", # Soybean Oil Futures, Continuous Contract
"CME_C1", # Corn Futures, Continuous Contract
"CME_O1", # Oats Futures, Continuous Contract
"CME_LC1", # Live Cattle Futures, Continuous Contract
"CME_FC1", # Feeder Cattle Futures, Continuous Contract
"CME_LN1", # Lean Hog Futures, Continuous Contract
"CME_GC1", # Gold Futures, Continuous Contract
"CME_SI1", # Silver Futures, Continuous Contract
"CME_PL1", # Platinum Futures, Continuous Contract
"CME_CL1", # Crude Oil Futures, Continuous Contract
"CME_HG1", # Copper Futures, Continuous Contract
"CME_LB1", # Random Length Lumber Futures, Continuous Contract
# "CME_NG1", # Natural Gas (Henry Hub) Physical Futures, Continuous Contract
"CME_PA1", # Palladium Futures, Continuous Contract
"CME_RR1", # Rough Rice Futures, Continuous Contract
"ICE_RS1", # Canola Futures, Continuous Contract
"ICE_GO1", # Gas Oil Futures, Continuous Contract
"CME_RB2", # Gasoline Futures, Continuous Contract
"CME_KW2", # Wheat Kansas, Continuous Contract
"ICE_WT1", # WTI Crude Futures, Continuous Contract
"ICE_CC1", # Cocoa Futures, Continuous Contract
"ICE_CT1", # Cotton No. 2 Futures, Continuous Contract
"ICE_KC1", # Coffee C Futures, Continuous Contract
"ICE_O1", # Heating Oil Futures, Continuous Contract
"ICE_OJ1", # Orange Juice Futures, Continuous Contract
"ICE_SB1", # Sugar No. 11 Futures, Continuous Contract
"CME_AD1", # Australian Dollar Futures, Continuous Contract #1
"CME_BP1", # British Pound Futures, Continuous Contract #1
"CME_CD1", # Canadian Dollar Futures, Continuous Contract #1
"CME_EC1", # Euro FX Futures, Continuous Contract #1
"CME_JY1", # Japanese Yen Futures, Continuous Contract #1
"CME_MP1", # Mexican Peso Futures, Continuous Contract #1
#"CME_NE1",# New Zealand Dollar Futures, Continuous Contract #1 # Short history ~2007
"CME_SF1", # Swiss Franc Futures, Continuous Contract #1
"ICE_DX1", # US Dollar Index Futures, Continuous Contract #1
"CME_NQ1", # E-mini NASDAQ 100 Futures, Continuous Contract #1
"EUREX_FDAX1", # DAX Futures, Continuous Contract #1
"CME_ES1", # E-mini S&P 500 Futures, Continuous Contract #1
"EUREX_FSMI1", # SMI Futures, Continuous Contract #1
"EUREX_FSTX1", # STOXX Europe 50 Index Futures, Continuous Contract #1
"LIFFE_FCE1", # CAC40 Index Futures, Continuous Contract #1
"LIFFE_Z1", # FTSE 100 Index Futures, Continuous Contract #1
"SGX_NK1", # SGX Nikkei 225 Index Futures, Continuous Contract #1
"CME_TY1", # 10 Yr Note Futures, Continuous Contract #1
"CME_FV1", # 5 Yr Note Futures, Continuous Contract #1
"CME_TU1", # 2 Yr Note Futures, Continuous Contract #1
#"ASX_XT1", # 10 Year Commonwealth Treasury Bond Futures, Continuous Contract #1
#"ASX_YT1", # 3 Year Commonwealth Treasury Bond Futures, Continuous Contract #1
"EUREX_FGBL1", # Euro-Bund (10Y) Futures, Continuous Contract #1
#"EUREX_FBTP1", # Long-Term Euro-BTP Futures, Continuous Contract #1 # Short history ~2010
"EUREX_FGBM1", # Euro-Bobl Futures, Continuous Contract #1
"EUREX_FGBS1", # Euro-Schatz Futures, Continuous Contract #1
"SGX_JB1", # SGX 10-Year Mini Japanese Government Bond Futures
"LIFFE_R1" # Long Gilt Futures, Continuous Contract #1
#"MX_CGB1", # Ten-Year Government of Canada Bond Futures, Continuous Contract #1
]
self.data = {}
self.lookup_period = 24*21
self.SetWarmUp(self.lookup_period)
for symbol in self.symbols:
data = self.AddData(QuantpediaFutures, symbol, Resolution.Daily)
data.SetFeeModel(CustomFeeModel())
data.SetLeverage(5)
self.data[symbol] = RollingWindow[float](self.lookup_period)
self.Schedule.On(self.DateRules.MonthStart(self.symbols[0]), self.TimeRules.At(0, 0), self.Rebalance)
def OnData(self, data):
for symbol in self.symbols:
if symbol in data and data[symbol]:
price = data[symbol].Value
if price != 0:
self.data[symbol].Add(price)
def Rebalance(self):
if self.IsWarmingUp: return
# Return sorting
returns = {}
volatility = {}
for symbol in self.symbols:
if self.data[symbol].IsReady:
if self.Securities[symbol].GetLastData() and self.Time.date() < QuantpediaFutures.get_last_update_date()[symbol]:
prices = [x for x in self.data[symbol]]
returns[symbol] = self.Return(prices)
# prices = prices[-60:]
prices = prices[:60]
volatility[symbol] = self.Volatility(prices)
if len(returns) == 0:
self.Liquidate()
return
# Return selection
long = []
short = []
half_period = int(self.lookup_period / 2)
for symbol, return_value in returns.items():
prices = [x for x in self.data[symbol]]
first_half_return = self.Return(prices[:half_period])
second_half_return = self.Return(prices[-half_period:])
if first_half_return < 0 and second_half_return > 0:
long.append(symbol)
elif first_half_return > 0 and second_half_return < 0:
short.append(symbol)
if len(long + short) == 0:
self.Liquidate()
return
# Volatility weighting
total_vol_long = sum([1 / volatility[x] for x in long if volatility[x] != 0])
total_vol_short = sum([1 / volatility[x] for x in short if volatility[x] != 0])
weight = {}
if total_vol_long != 0:
# Calculate long stocks weights
for symbol in long:
vol = volatility[symbol]
if vol != 0:
weight[symbol] = (1 / vol) / total_vol_long
if total_vol_short != 0:
# Calculate short stocks weights
for symbol in short:
vol = volatility[symbol]
if vol != 0:
weight[symbol] = (1 / vol) / total_vol_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():
self.SetHoldings(symbol, w)
def Return(self, history):
return (history[0] - history[-1]) / history[-1]
def Volatility(self, history):
prices = np.array(history)
returns = (prices[:-1]-prices[1:])/prices[1:]
return np.std(returns)
# Custom fee model.
class CustomFeeModel(FeeModel):
def GetOrderFee(self, parameters):
fee = parameters.Security.Price * parameters.Order.AbsoluteQuantity * 0.00005
return OrderFee(CashAmount(fee, "USD"))
# Quantpedia data
class QuantpediaFutures(PythonData):
_last_update_date:Dict[Symbol, datetime.date] = {}
@staticmethod
def get_last_update_date() -> Dict[Symbol, datetime.date]:
return QuantpediaFutures._last_update_date
def GetSource(self, config, date, isLiveMode):
return SubscriptionDataSource("http://data.quantpedia.com/backtesting_data/futures/{0}.csv".format(config.Symbol.Value), SubscriptionTransportMedium.RemoteFile, FileFormat.Csv)
def Reader(self, config, line, date, isLiveMode):
data = QuantpediaFutures()
data.Symbol = config.Symbol
try:
if not line[0].isdigit(): return None
split = line.split(';')
data.Time = datetime.strptime(split[0], "%d.%m.%Y") + timedelta(days=1)
data['settle'] = float(split[1])
data.Value = float(split[1])
except:
return None
if config.Symbol.Value not in QuantpediaFutures._last_update_date:
QuantpediaFutures._last_update_date[config.Symbol.Value] = datetime(1,1,1).date()
if data.Time.date() > QuantpediaFutures._last_update_date[config.Symbol.Value]:
QuantpediaFutures._last_update_date[config.Symbol.Value] = data.Time.date()
return data