Quant BuffetRelax, Not Over Thinking

Time Series Reversal of Momentum on Futures

Log in to collect

Academic paper

Time Series Reversal of Financial Assets

AuthorsJiadong Liu; Fotis Papailias

Institute
  • 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

Annualised return24.4%
Volatility20.9%
Beta0.231
Sharpe ratio1.17
Sortino ratio0.313
Maximum drawdown-26.2%
Win rate54%

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