OBV Divergence Strategy
On-Balance Volume tracks cumulative trading volume with the idea that volume precedes price. When price forms a new high but OBV fails to confirm—or vice versa—a reversal may be brewing. This strategy uses that divergence to fade unsustainable moves.
Testing indicates an average annual return of about 112%. It performs best in the forex market.
For each candle OBV is updated and compared with the prior reading. A bullish signal emerges if price makes a lower low while OBV prints a higher low. A bearish signal occurs when price rallies to a higher high but OBV lags. A moving average provides an exit point, while a percentage stop keeps losses controlled.
The approach attempts to capture mean reversion following volume exhaustion and often holds trades only until price crosses back over the average line.
Details
- Entry Criteria: Price/OBV divergence.
- Long/Short: Both.
- Exit Criteria: Price crossing moving average or stop-loss.
- Stops: Yes, percentage based.
- Default Values:
DivergencePeriod= 5MAPeriod= 20CandleType= 5 minuteStopLossPercent= 2
- Filters:
- Category: Divergence
- Direction: Both
- Indicators: OBV, MA
- Stops: Yes
- Complexity: Intermediate
- Timeframe: Intraday
- Seasonality: No
- Neural networks: No
- Divergence: Yes
- Risk level: Medium
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>
/// OBV (On-Balance Volume) Divergence strategy.
/// Tracks OBV direction vs price direction over a lookback window.
/// Bullish divergence: price trending down but OBV trending up.
/// Bearish divergence: price trending up but OBV trending down.
/// Uses SMA for exit signals.
/// </summary>
public class ObvDivergenceStrategy : Strategy
{
private readonly StrategyParam<int> _maPeriod;
private readonly StrategyParam<int> _lookback;
private readonly StrategyParam<DataType> _candleType;
private readonly StrategyParam<int> _cooldownBars;
private decimal _cumulativeObv;
private decimal _prevClosePrice;
private readonly List<decimal> _priceHistory = new();
private readonly List<decimal> _obvHistory = new();
private int _cooldown;
/// <summary>
/// MA Period.
/// </summary>
public int MAPeriod
{
get => _maPeriod.Value;
set => _maPeriod.Value = value;
}
/// <summary>
/// Lookback period for divergence.
/// </summary>
public int Lookback
{
get => _lookback.Value;
set => _lookback.Value = value;
}
/// <summary>
/// Candle type.
/// </summary>
public DataType CandleType
{
get => _candleType.Value;
set => _candleType.Value = value;
}
/// <summary>
/// Cooldown bars.
/// </summary>
public int CooldownBars
{
get => _cooldownBars.Value;
set => _cooldownBars.Value = value;
}
/// <summary>
/// Constructor.
/// </summary>
public ObvDivergenceStrategy()
{
_maPeriod = Param(nameof(MAPeriod), 20)
.SetGreaterThanZero()
.SetDisplay("MA Period", "Period for SMA exit signal", "Indicators");
_lookback = Param(nameof(Lookback), 10)
.SetGreaterThanZero()
.SetDisplay("Lookback", "Lookback period for divergence detection", "Indicators");
_candleType = Param(nameof(CandleType), TimeSpan.FromMinutes(1).TimeFrame())
.SetDisplay("Candle Type", "Type of candles to use", "General");
_cooldownBars = Param(nameof(CooldownBars), 500)
.SetRange(1, 1000)
.SetDisplay("Cooldown Bars", "Bars to wait between trades", "General");
}
/// <inheritdoc />
public override IEnumerable<(Security sec, DataType dt)> GetWorkingSecurities()
{
return [(Security, CandleType)];
}
/// <inheritdoc />
protected override void OnReseted()
{
base.OnReseted();
_cumulativeObv = default;
_prevClosePrice = default;
_priceHistory.Clear();
_obvHistory.Clear();
_cooldown = default;
}
/// <inheritdoc />
protected override void OnStarted2(DateTime time)
{
base.OnStarted2(time);
_cumulativeObv = 0;
_prevClosePrice = 0;
_priceHistory.Clear();
_obvHistory.Clear();
_cooldown = 0;
var sma = new SimpleMovingAverage { Length = MAPeriod };
var subscription = SubscribeCandles(CandleType);
subscription
.Bind(sma, ProcessCandle)
.Start();
var area = CreateChartArea();
if (area != null)
{
DrawCandles(area, subscription);
DrawIndicator(area, sma);
DrawOwnTrades(area);
}
}
private void ProcessCandle(ICandleMessage candle, decimal smaValue)
{
if (candle.State != CandleStates.Finished)
return;
// Calculate OBV manually
if (_prevClosePrice > 0)
{
if (candle.ClosePrice > _prevClosePrice)
_cumulativeObv += candle.TotalVolume;
else if (candle.ClosePrice < _prevClosePrice)
_cumulativeObv -= candle.TotalVolume;
}
_prevClosePrice = candle.ClosePrice;
// Store history
_priceHistory.Add(candle.ClosePrice);
_obvHistory.Add(_cumulativeObv);
// Keep only what we need
if (_priceHistory.Count > Lookback + 1)
{
_priceHistory.RemoveAt(0);
_obvHistory.RemoveAt(0);
}
if (!IsFormedAndOnlineAndAllowTrading())
return;
if (_priceHistory.Count <= Lookback)
return;
if (_cooldown > 0)
{
_cooldown--;
return;
}
// Compare current values to lookback-period-ago values
var priceChange = _priceHistory[_priceHistory.Count - 1] - _priceHistory[0];
var obvChange = _obvHistory[_obvHistory.Count - 1] - _obvHistory[0];
// Bullish divergence: price down but OBV up
var bullishDiv = priceChange < 0 && obvChange > 0;
// Bearish divergence: price up but OBV down
var bearishDiv = priceChange > 0 && obvChange < 0;
if (Position == 0 && bullishDiv)
{
BuyMarket();
_cooldown = CooldownBars;
}
else if (Position == 0 && bearishDiv)
{
SellMarket();
_cooldown = CooldownBars;
}
else if (Position > 0 && candle.ClosePrice < smaValue)
{
SellMarket();
_cooldown = CooldownBars;
}
else if (Position < 0 && candle.ClosePrice > smaValue)
{
BuyMarket();
_cooldown = 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
from StockSharp.Messages import DataType, CandleStates
from StockSharp.Algo.Indicators import SimpleMovingAverage
from StockSharp.Algo.Strategies import Strategy
class obv_divergence_strategy(Strategy):
"""
OBV (On-Balance Volume) Divergence strategy.
Tracks OBV direction vs price direction over a lookback window.
Bullish divergence: price trending down but OBV trending up.
Bearish divergence: price trending up but OBV trending down.
Uses SMA for exit signals.
"""
def __init__(self):
super(obv_divergence_strategy, self).__init__()
self._ma_period = self.Param("MAPeriod", 20).SetDisplay("MA Period", "Period for SMA exit signal", "Indicators")
self._lookback = self.Param("Lookback", 10).SetDisplay("Lookback", "Lookback period for divergence detection", "Indicators")
self._candle_type = self.Param("CandleType", DataType.TimeFrame(TimeSpan.FromMinutes(1))).SetDisplay("Candle Type", "Type of candles to use", "General")
self._cooldown_bars = self.Param("CooldownBars", 500).SetDisplay("Cooldown Bars", "Bars to wait between trades", "General")
self._cumulative_obv = 0.0
self._prev_close = 0.0
self._price_history = []
self._obv_history = []
self._cooldown = 0
@property
def candle_type(self):
return self._candle_type.Value
def OnReseted(self):
super(obv_divergence_strategy, self).OnReseted()
self._cumulative_obv = 0.0
self._prev_close = 0.0
self._price_history = []
self._obv_history = []
self._cooldown = 0
def OnStarted2(self, time):
super(obv_divergence_strategy, self).OnStarted2(time)
self._cumulative_obv = 0.0
self._prev_close = 0.0
self._price_history = []
self._obv_history = []
self._cooldown = 0
sma = SimpleMovingAverage()
sma.Length = self._ma_period.Value
subscription = self.SubscribeCandles(self.candle_type)
subscription.Bind(sma, self._process_candle).Start()
area = self.CreateChartArea()
if area is not None:
self.DrawCandles(area, subscription)
self.DrawIndicator(area, sma)
self.DrawOwnTrades(area)
def _process_candle(self, candle, sma_val):
if candle.State != CandleStates.Finished:
return
close = float(candle.ClosePrice)
vol = float(candle.TotalVolume)
# Calculate OBV manually
if self._prev_close > 0:
if close > self._prev_close:
self._cumulative_obv += vol
elif close < self._prev_close:
self._cumulative_obv -= vol
self._prev_close = close
# Store history
self._price_history.append(close)
self._obv_history.append(self._cumulative_obv)
lb = self._lookback.Value
# Keep only what we need
if len(self._price_history) > lb + 1:
self._price_history.pop(0)
self._obv_history.pop(0)
if len(self._price_history) <= lb:
return
if self._cooldown > 0:
self._cooldown -= 1
return
# Compare current values to lookback-period-ago values
price_change = self._price_history[-1] - self._price_history[0]
obv_change = self._obv_history[-1] - self._obv_history[0]
# Bullish divergence: price down but OBV up
bullish_div = price_change < 0 and obv_change > 0
# Bearish divergence: price up but OBV down
bearish_div = price_change > 0 and obv_change < 0
sv = float(sma_val)
cd = self._cooldown_bars.Value
if self.Position == 0 and bullish_div:
self.BuyMarket()
self._cooldown = cd
elif self.Position == 0 and bearish_div:
self.SellMarket()
self._cooldown = cd
elif self.Position > 0 and close < sv:
self.SellMarket()
self._cooldown = cd
elif self.Position < 0 and close > sv:
self.BuyMarket()
self._cooldown = cd
def CreateClone(self):
return obv_divergence_strategy()