Quant BuffetRelax, Not Over Thinking

WTI-Brent Spread Reversion via 20-Day SMA

Log in to collect

Academic paper

Trading and filtering futures spread portfolios: Further applications of threshold and correlation filters

AuthorsChristian L. Dunis; Jason Laws; Ben J. Evans

Institute
  • Liverpool John Moores University
  • ?Professor of Banking and Finance at Liverpool Business School, Liverpool John Moores University,
  • DEFriedrich-Ebert-Stiftung e.V.
  • ?GBT – Trading, Friedrich-Ebert-Straße, Kassel, 34119, Germany
  • ?GBT – Trading, Friedrich-Ebert-Straße

Strategy in a nutshell

A 20-day moving average of WTI/Brent spread is calculated each day. If the current spread value is above SMA 20, then we enter a short position in the spread on close (betting that the spread will decrease to the fair value represented by SMA 20). The trade is closed at the close of the trading day when the spread crosses below fair value. If the current spread value is below SMA 20, then we enter a long position betting that the spread will increase, and the trade is closed at the close of the trading day when the spread crosses above fair value.

Economic rationale

Both oils differ in chemical compositions, and they also differ in production and transportation attributes. These differences are reflected in the price spread between both futures contracts. The spread is mean reverting because most of the price shocks are only temporal, so the spread moves back to its long term economic equilibrium, and therefore it is possible to create a trading strategy based on this mean reversion. Caution should be only needed in utilizing parameters from the

Backtest performance

Annualised return9.92%
Volatility11.27%
Beta0.023
Sharpe ratio0.72
Sortino ratio0.66
Maximum drawdown70.80%
Win rate48%

Full Python code

from AlgoLib import *

class WTIBRENTSpread(XXX):

def Initialize(self):
self.SetStartDate(2000, 1, 1)
self.SetCash(100000)

self.symbols = [
    "ICE_WT1",  # WTI Crude Futures, Continuous Contract
    "ICE_B1"    # Brent Crude Oil Futures, Continuous Contract
]

self.spread = RollingWindow[float](20)

for symbol in self.symbols:
    data = self.AddData(QuantpediaFutures, symbol, Resolution.Daily)
    data.SetLeverage(5)
    data.SetFeeModel(CustomFeeModel())

def OnData(self, data):
symbol1 = self.Symbol(self.symbols[0])
symbol2 = self.Symbol(self.symbols[1])

if symbol1 in data.Keys and symbol2 in data.Keys and data[symbol1] and data[symbol2]:
    price1 = data[symbol1].Price
    price2 = data[symbol2].Price
    
    if price1 != 0 and price2 != 0:
        spread = price1 - price2
        self.spread.Add(spread)

# MA calculation.
if self.spread.IsReady:
    if (self.Time.date() - self.Securities[symbol1].GetLastData().Time.date()).days < 5 and (self.Time.date() - self.Securities[symbol2].GetLastData().Time.date()).days < 5:
        spreads = [x for x in self.spread]
        spread_ma20 = sum(spreads) / len(spreads)
        
        current_spread = spreads[0]
        
        if current_spread > spread_ma20:
            self.SetHoldings(symbol1, -1)
            self.SetHoldings(symbol2, 1)
        elif current_spread < spread_ma20:
            self.SetHoldings(symbol1, 1)
            self.SetHoldings(symbol2, -1)
    else:
        self.Liquidate()

# Quantpedia data.
# NOTE: IMPORTANT: Data order must be ascending (datewise)
class QuantpediaFutures(PythonData):
def GetSource(self, config, date, isLiveMode):
return SubscriptionDataSource("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

if not line[0].isdigit(): return None
split = line.split(';')

data.Time = datetime.strptime(split[0], "%d.%m.%Y") + timedelta(days=1)
data['back_adjusted'] = float(split[1])
data['spliced'] = float(split[2])
data.Value = float(split[1])

return data

# Custom fee model.
class CustomFeeModel(FeeModel):
def GetOrderFee(self, parameters):
fee = parameters.Security.Price * parameters.Order.AbsoluteQuantity * 0.00005
return OrderFee(CashAmount(fee, "USD"))