Returns Signal Momentum
Log in to collectAcademic paper
Fotis Papailias
- University of London
- King's College London
- ?Quantf Research
- ?University of London, King's College London, Department of Management
Strategy in a nutshell
This investment strategy uses 55 liquid exchange-traded futures. Each month, returns are analyzed over a 12-month look-back period. A long position is taken when the probability of positive returns is ≥0.4; otherwise, a short position is entered. Returns are scaled by annualized ex-ante volatility, and the portfolio is rebalanced monthly to adjust for changing conditions. This systematic, probability-driven approach with volatility management aims to optimize portfolio returns.
Economic rationale
The strategy’s rationale is based on short-term under-reaction and delayed over-reaction in markets. By detecting predictable patterns in return signs and their conditional means, it exploits the tendency of financial assets to produce positive returns over time, aligning trading decisions with broader market trends.
Backtest performance
Full Python code
import numpy as np
from AlgorithmImports import *
class ReturnsSignalMomentum(QCAlgorithm):
def Initialize(self):
self.SetStartDate(2005, 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 # 'Settlement price' instead of 'settle' on quandl.
#"ASX_YT1", # 3 Year Commonwealth Treasury Bond Futures, Continuous Contract #1 # 'Settlement price' instead of 'settle' on quandl.
"EUREX_FGBL1", # Euro-Bund (10Y) Futures, Continuous Contract #1
#"EUREX_FBTP1", # Long-Term Euro-BTP Futures, Continuous Contract #1 # Short history
"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 # 'Settlement price' instead of 'settle' on quandl.
]
self.data = {}
self.return_history = {}
# lookup_period = 60
lookup_period = 21
self.return_months_count = 12
self.SetWarmUp(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](lookup_period)
self.return_history[symbol] = RollingWindow[float](self.return_months_count)
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
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]]
volatility[symbol] = self.Volatility(prices)
prices = prices[:21] # Last month of daily prices
self.return_history[symbol].Add(self.Return(prices))
if len(volatility) == 0:
self.Liquidate()
return
long = []
short = []
threshold = int(0.4*self.return_months_count)
# Create long and short portfolio
for symbol, roll_window in self.return_history.items():
# Check if monthly returns are ready
if not roll_window.IsReady:
continue
if symbol not in volatility:
continue
# Select only positive returns
temp = [x for x in roll_window if x > 0]
if len(temp) >= threshold:
long.append(symbol)
else:
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):
values = np.array(history)
returns = (values[:-1] - values[1:]) / values[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