WTI Brent Spread
The trade targets the price differential between WTI and Brent crude oil. When the spread deviates from historical norms, the system bets on mean reversion by longing one grade and shorting the other.
Positions roll with the front‑month futures and are closed when the spread converges.
Details
- Data: Front‑month WTI and Brent futures prices.
- Entry: Long cheaper grade and short expensive when spread > threshold.
- Exit: Close when spread returns to average or at contract roll.
- Instruments: Crude oil futures.
- Risk: Dollar‑neutral with stop on spread widening.
// WTIBrentSpreadStrategy.cs
// -----------------------------------------------------------------------------
// Spread/mean-reversion trading strategy.
// Uses Bollinger Bands to identify when price deviates from its mean.
// Buys when price touches lower band, sells when it touches upper band.
// Exits when price returns to the middle band.
// Cooldown prevents excessive trading.
// -----------------------------------------------------------------------------
// Date: 2 Aug 2025
// -----------------------------------------------------------------------------
using System;
using System.Collections.Generic;
using Ecng.Common;
using StockSharp.Algo.Indicators;
using StockSharp.Algo.Strategies;
using StockSharp.BusinessEntities;
using StockSharp.Messages;
namespace StockSharp.Samples.Strategies;
/// <summary>
/// Mean-reversion spread strategy using Bollinger Bands.
/// </summary>
public class WTIBrentSpreadStrategy : Strategy
{
private readonly StrategyParam<int> _bbPeriod;
private readonly StrategyParam<decimal> _bbWidth;
private readonly StrategyParam<int> _cooldownBars;
private readonly StrategyParam<DataType> _candleType;
/// <summary>
/// Bollinger Bands period.
/// </summary>
public int BbPeriod
{
get => _bbPeriod.Value;
set => _bbPeriod.Value = value;
}
/// <summary>
/// Bollinger Bands width (standard deviations).
/// </summary>
public decimal BbWidth
{
get => _bbWidth.Value;
set => _bbWidth.Value = value;
}
/// <summary>
/// Cooldown bars between trades.
/// </summary>
public int CooldownBars
{
get => _cooldownBars.Value;
set => _cooldownBars.Value = value;
}
/// <summary>
/// The type of candles to use for strategy calculation.
/// </summary>
public DataType CandleType
{
get => _candleType.Value;
set => _candleType.Value = value;
}
private BollingerBands _bb;
private int _cooldownRemaining;
public WTIBrentSpreadStrategy()
{
_bbPeriod = Param(nameof(BbPeriod), 20)
.SetDisplay("BB Period", "Bollinger Bands period", "Parameters");
_bbWidth = Param(nameof(BbWidth), 2.0m)
.SetDisplay("BB Width", "Bollinger Bands width in std devs", "Parameters");
_cooldownBars = Param(nameof(CooldownBars), 10)
.SetDisplay("Cooldown Bars", "Bars to wait between trades", "Risk");
_candleType = Param(nameof(CandleType), TimeSpan.FromMinutes(15).TimeFrame())
.SetDisplay("Candle Type", "Type of candles to use", "General");
}
/// <inheritdoc />
public override IEnumerable<(Security sec, DataType dt)> GetWorkingSecurities()
{
if (Security != null)
yield return (Security, CandleType);
}
/// <inheritdoc />
protected override void OnReseted()
{
base.OnReseted();
_bb = null;
_cooldownRemaining = 0;
}
/// <inheritdoc />
protected override void OnStarted2(DateTime time)
{
base.OnStarted2(time);
_bb = new BollingerBands
{
Length = BbPeriod,
Width = BbWidth
};
SubscribeCandles(CandleType)
.BindEx(_bb, ProcessCandle)
.Start();
}
private void ProcessCandle(ICandleMessage candle, IIndicatorValue value)
{
if (candle.State != CandleStates.Finished)
return;
if (!_bb.IsFormed)
return;
var bb = (BollingerBandsValue)value;
if (bb.UpBand is not decimal upper ||
bb.LowBand is not decimal lower ||
bb.MovingAverage is not decimal middle)
return;
if (!IsFormedAndOnlineAndAllowTrading())
return;
if (_cooldownRemaining > 0)
{
_cooldownRemaining--;
return;
}
var close = candle.ClosePrice;
// Price below lower band -> oversold -> buy
if (close <= lower && Position <= 0)
{
if (Position < 0)
BuyMarket(Math.Abs(Position));
BuyMarket(Volume);
_cooldownRemaining = CooldownBars;
}
// Price above upper band -> overbought -> sell
else if (close >= upper && Position >= 0)
{
if (Position > 0)
SellMarket(Math.Abs(Position));
SellMarket(Volume);
_cooldownRemaining = CooldownBars;
}
// Price returns to middle -> exit
else if (Position > 0 && close >= middle)
{
SellMarket(Math.Abs(Position));
_cooldownRemaining = CooldownBars;
}
else if (Position < 0 && close <= middle)
{
BuyMarket(Math.Abs(Position));
_cooldownRemaining = CooldownBars;
}
}
}
import clr
clr.AddReference("StockSharp.Messages")
clr.AddReference("StockSharp.Algo")
clr.AddReference("StockSharp.Algo.Indicators")
clr.AddReference("StockSharp.Algo.Strategies")
from System import TimeSpan, Math
from StockSharp.Messages import DataType, CandleStates
from StockSharp.Algo.Indicators import BollingerBands
from StockSharp.Algo.Strategies import Strategy
class wti_brent_spread_strategy(Strategy):
"""Mean-reversion spread strategy using Bollinger Bands."""
def __init__(self):
super(wti_brent_spread_strategy, self).__init__()
self._bb_period = self.Param("BbPeriod", 20) \
.SetDisplay("BB Period", "Bollinger Bands period", "Parameters")
self._bb_width = self.Param("BbWidth", 2.0) \
.SetDisplay("BB Width", "Bollinger Bands width in std devs", "Parameters")
self._cooldown_bars = self.Param("CooldownBars", 10) \
.SetDisplay("Cooldown Bars", "Bars to wait between trades", "Risk")
self._candle_type = self.Param("CandleType", DataType.TimeFrame(TimeSpan.FromMinutes(15))) \
.SetDisplay("Candle Type", "Type of candles to use", "General")
self._bb = None
self._cooldown_remaining = 0
@property
def candle_type(self):
return self._candle_type.Value
def OnReseted(self):
super(wti_brent_spread_strategy, self).OnReseted()
self._bb = None
self._cooldown_remaining = 0
def OnStarted2(self, time):
super(wti_brent_spread_strategy, self).OnStarted2(time)
self._bb = BollingerBands()
self._bb.Length = int(self._bb_period.Value)
self._bb.Width = float(self._bb_width.Value)
subscription = self.SubscribeCandles(self.candle_type)
subscription.BindEx(self._bb, self._process_candle).Start()
def _process_candle(self, candle, bb_value):
if candle.State != CandleStates.Finished:
return
if not self._bb.IsFormed:
return
if bb_value.UpBand is None or bb_value.LowBand is None or bb_value.MovingAverage is None:
return
if not self.IsFormedAndOnlineAndAllowTrading():
return
if self._cooldown_remaining > 0:
self._cooldown_remaining -= 1
return
upper = float(bb_value.UpBand)
lower = float(bb_value.LowBand)
middle = float(bb_value.MovingAverage)
close = float(candle.ClosePrice)
cooldown = int(self._cooldown_bars.Value)
if close <= lower and self.Position <= 0:
if self.Position < 0:
self.BuyMarket(Math.Abs(self.Position))
self.BuyMarket(self.Volume)
self._cooldown_remaining = cooldown
elif close >= upper and self.Position >= 0:
if self.Position > 0:
self.SellMarket(Math.Abs(self.Position))
self.SellMarket(self.Volume)
self._cooldown_remaining = cooldown
elif self.Position > 0 and close >= middle:
self.SellMarket(Math.Abs(self.Position))
self._cooldown_remaining = cooldown
elif self.Position < 0 and close <= middle:
self.BuyMarket(Math.Abs(self.Position))
self._cooldown_remaining = cooldown
def CreateClone(self):
return wti_brent_spread_strategy()