OBV Mean Reversion Strategy
On Balance Volume (OBV) tracks cumulative volume flow to determine whether buyers or sellers are dominant. This strategy waits for OBV to diverge sharply from its average and then trades in anticipation of a return to typical levels.
Testing indicates an average annual return of about 79%. It performs best in the stocks market.
A buy signal occurs when OBV falls below its average minus Multiplier times the standard deviation and price is below the moving average. A sell signal is generated when OBV rises above the upper band with price above the average. Positions close when OBV crosses back through its mean line.
The approach is useful for traders who consider volume flows in addition to price action. Stops are placed a set percentage away to handle situations where volume continues to accelerate.
Details
- Entry Criteria:
- Long: OBV < Avg - Multiplier * StdDev && Close < MA
- Short: OBV > Avg + Multiplier * StdDev && Close > MA
- Long/Short: Both sides.
- Exit Criteria:
- Long: Exit when OBV > Avg
- Short: Exit when OBV < Avg
- Stops: Yes, percent stop-loss.
- Default Values:
AveragePeriod= 20Multiplier= 2.0mCandleType= TimeSpan.FromMinutes(5)
- Filters:
- Category: Mean Reversion
- Direction: Both
- Indicators: OBV
- Stops: Yes
- Complexity: Intermediate
- Timeframe: Intraday
- Seasonality: No
- Neural networks: No
- Divergence: No
- Risk Level: Medium
using System;
using System.Linq;
using System.Collections.Generic;
using Ecng.Common;
using Ecng.Collections;
using Ecng.Serialization;
using StockSharp.Algo.Indicators;
using StockSharp.Algo.Strategies;
using StockSharp.BusinessEntities;
using StockSharp.Messages;
namespace StockSharp.Samples.Strategies;
/// <summary>
/// OBV Mean Reversion Strategy (244).
/// Enter when OBV deviates from its average by a certain multiple of standard deviation.
/// Exit when OBV returns to its average.
/// </summary>
public class ObvMeanReversionStrategy : Strategy
{
private readonly StrategyParam<int> _averagePeriod;
private readonly StrategyParam<decimal> _multiplier;
private readonly StrategyParam<DataType> _candleType;
private OnBalanceVolume _obv;
private SimpleMovingAverage _obvAverage;
private StandardDeviation _obvStdDev;
private decimal? _currentObv;
private decimal? _obvAvgValue;
private decimal? _obvStdDevValue;
/// <summary>
/// Period for OBV average calculation.
/// </summary>
public int AveragePeriod
{
get => _averagePeriod.Value;
set => _averagePeriod.Value = value;
}
/// <summary>
/// Standard deviation multiplier for entry.
/// </summary>
public decimal Multiplier
{
get => _multiplier.Value;
set => _multiplier.Value = value;
}
/// <summary>
/// Type of candles to use.
/// </summary>
public DataType CandleType
{
get => _candleType.Value;
set => _candleType.Value = value;
}
/// <summary>
/// Initializes a new instance of the <see cref="ObvMeanReversionStrategy"/>.
/// </summary>
public ObvMeanReversionStrategy()
{
_averagePeriod = Param(nameof(AveragePeriod), 20)
.SetGreaterThanZero()
.SetDisplay("Average Period", "Period for OBV average calculation", "Strategy Parameters")
.SetOptimize(10, 30, 5);
_multiplier = Param(nameof(Multiplier), 2.0m)
.SetGreaterThanZero()
.SetDisplay("StdDev Multiplier", "Standard deviation multiplier for entry", "Strategy Parameters")
.SetOptimize(1.0m, 3.0m, 0.5m);
_candleType = Param(nameof(CandleType), TimeSpan.FromMinutes(5).TimeFrame())
.SetDisplay("Candle Type", "Type of candles to use", "Strategy Parameters");
}
/// <inheritdoc />
public override IEnumerable<(Security sec, DataType dt)> GetWorkingSecurities()
{
return [(Security, CandleType)];
}
/// <inheritdoc />
protected override void OnReseted()
{
base.OnReseted();
_currentObv = default;
_obvAvgValue = default;
_obvStdDevValue = default;
}
/// <inheritdoc />
protected override void OnStarted2(DateTime time)
{
base.OnStarted2(time);
// Create indicators
_obv = new OnBalanceVolume();
_obvAverage = new SMA { Length = AveragePeriod };
_obvStdDev = new StandardDeviation { Length = AveragePeriod };
// Create candle subscription
var subscription = SubscribeCandles(CandleType);
// Create processing chain
subscription
.BindEx(_obv, ProcessObv)
.Start();
// Setup chart visualization if available
var area = CreateChartArea();
if (area != null)
{
DrawCandles(area, subscription);
DrawIndicator(area, _obv);
DrawOwnTrades(area);
}
// Enable position protection
StartProtection(
takeProfit: new Unit(5, UnitTypes.Percent),
stopLoss: new Unit(2, UnitTypes.Percent)
);
}
private void ProcessObv(ICandleMessage candle, IIndicatorValue obvValue)
{
if (candle.State != CandleStates.Finished)
return;
// Extract OBV value
_currentObv = obvValue.ToDecimal();
// Process OBV through average and standard deviation indicators
var avgIndicatorValue = _obvAverage.Process(obvValue);
var stdDevIndicatorValue = _obvStdDev.Process(obvValue);
_obvAvgValue = avgIndicatorValue.ToDecimal();
_obvStdDevValue = stdDevIndicatorValue.ToDecimal();
// Check if strategy is ready for trading
if (!IsFormedAndOnlineAndAllowTrading() || !_obvAverage.IsFormed || !_obvStdDev.IsFormed)
return;
// Ensure we have all needed values
if (!_currentObv.HasValue || !_obvAvgValue.HasValue || !_obvStdDevValue.HasValue)
return;
// Calculate bands
var upperBand = _obvAvgValue.Value + Multiplier * _obvStdDevValue.Value;
var lowerBand = _obvAvgValue.Value - Multiplier * _obvStdDevValue.Value;
LogInfo($"OBV: {_currentObv}, OBV Avg: {_obvAvgValue}, Upper: {upperBand}, Lower: {lowerBand}");
// Entry logic
if (Position == 0)
{
// Long Entry: OBV is below lower band (OBV oversold)
if (_currentObv.Value < lowerBand)
{
LogInfo($"Buy Signal - OBV ({_currentObv}) < Lower Band ({lowerBand})");
BuyMarket(Volume);
}
// Short Entry: OBV is above upper band (OBV overbought)
else if (_currentObv.Value > upperBand)
{
LogInfo($"Sell Signal - OBV ({_currentObv}) > Upper Band ({upperBand})");
SellMarket(Volume);
}
}
// Exit logic
else if (Position > 0 && _currentObv.Value > _obvAvgValue.Value)
{
// Exit Long: OBV returned to average
LogInfo($"Exit Long - OBV ({_currentObv}) > OBV Avg ({_obvAvgValue})");
SellMarket(Math.Abs(Position));
}
else if (Position < 0 && _currentObv.Value < _obvAvgValue.Value)
{
// Exit Short: OBV returned to average
LogInfo($"Exit Short - OBV ({_currentObv}) < OBV Avg ({_obvAvgValue})");
BuyMarket(Math.Abs(Position));
}
}
}
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, UnitTypes, Unit
from StockSharp.Algo.Indicators import OnBalanceVolume, SimpleMovingAverage, StandardDeviation
from StockSharp.Algo.Strategies import Strategy
from datatype_extensions import *
from indicator_extensions import *
class obv_mean_reversion_strategy(Strategy):
"""
OBV Mean Reversion Strategy (244).
Enter when OBV deviates from its average by a certain multiple of standard deviation.
Exit when OBV returns to its average.
"""
def __init__(self):
super(obv_mean_reversion_strategy, self).__init__()
# Initialize strategy parameters
self._average_period = self.Param("AveragePeriod", 20) \
.SetGreaterThanZero() \
.SetDisplay("Average Period", "Period for OBV average calculation", "Strategy Parameters") \
.SetCanOptimize(True) \
.SetOptimize(10, 30, 5)
self._multiplier = self.Param("Multiplier", 2.0) \
.SetGreaterThanZero() \
.SetDisplay("StdDev Multiplier", "Standard deviation multiplier for entry", "Strategy Parameters") \
.SetCanOptimize(True) \
.SetOptimize(1.0, 3.0, 0.5)
self._candle_type = self.Param("CandleType", tf(5)) \
.SetDisplay("Candle Type", "Type of candles to use", "Strategy Parameters")
# Internal fields
self._obv = None
self._obv_average = None
self._obv_std_dev = None
self._current_obv = None
self._obv_avg_value = None
self._obv_std_dev_value = None
@property
def AveragePeriod(self):
"""Period for OBV average calculation."""
return self._average_period.Value
@AveragePeriod.setter
def AveragePeriod(self, value):
self._average_period.Value = value
@property
def Multiplier(self):
"""Standard deviation multiplier for entry."""
return self._multiplier.Value
@Multiplier.setter
def Multiplier(self, value):
self._multiplier.Value = value
@property
def CandleType(self):
"""Type of candles to use."""
return self._candle_type.Value
@CandleType.setter
def CandleType(self, value):
self._candle_type.Value = value
def OnReseted(self):
super(obv_mean_reversion_strategy, self).OnReseted()
self._current_obv = None
self._obv_avg_value = None
self._obv_std_dev_value = None
def OnStarted2(self, time):
super(obv_mean_reversion_strategy, self).OnStarted2(time)
# Create indicators
self._obv = OnBalanceVolume()
self._obv_average = SimpleMovingAverage()
self._obv_average.Length = self.AveragePeriod
self._obv_std_dev = StandardDeviation()
self._obv_std_dev.Length = self.AveragePeriod
# Create candle subscription
subscription = self.SubscribeCandles(self.CandleType)
# Create processing chain
subscription.BindEx(self._obv, self.ProcessObv).Start()
# Setup chart visualization if available
area = self.CreateChartArea()
if area is not None:
self.DrawCandles(area, subscription)
self.DrawIndicator(area, self._obv)
self.DrawOwnTrades(area)
# Enable position protection
self.StartProtection(
takeProfit=Unit(5, UnitTypes.Percent),
stopLoss=Unit(2, UnitTypes.Percent)
)
def ProcessObv(self, candle, obv_value):
if candle.State != CandleStates.Finished:
return
# Extract OBV value
self._current_obv = float(obv_value)
# Process OBV through average and standard deviation indicators
avg_indicator_value = self._obv_average.Process(obv_value)
std_dev_indicator_value = self._obv_std_dev.Process(obv_value)
self._obv_avg_value = float(avg_indicator_value)
self._obv_std_dev_value = float(std_dev_indicator_value)
# Check if strategy is ready for trading
if not self.IsFormedAndOnlineAndAllowTrading() or not self._obv_average.IsFormed or not self._obv_std_dev.IsFormed:
return
# Ensure we have all needed values
if self._current_obv is None or self._obv_avg_value is None or self._obv_std_dev_value is None:
return
# Calculate bands
upper_band = self._obv_avg_value + self.Multiplier * self._obv_std_dev_value
lower_band = self._obv_avg_value - self.Multiplier * self._obv_std_dev_value
self.LogInfo("OBV: {0}, OBV Avg: {1}, Upper: {2}, Lower: {3}".format(
self._current_obv, self._obv_avg_value, upper_band, lower_band))
# Entry logic
if self.Position == 0:
# Long Entry: OBV is below lower band (OBV oversold)
if self._current_obv < lower_band:
self.LogInfo("Buy Signal - OBV ({0}) < Lower Band ({1})".format(self._current_obv, lower_band))
self.BuyMarket(self.Volume)
# Short Entry: OBV is above upper band (OBV overbought)
elif self._current_obv > upper_band:
self.LogInfo("Sell Signal - OBV ({0}) > Upper Band ({1})".format(self._current_obv, upper_band))
self.SellMarket(self.Volume)
# Exit logic
elif self.Position > 0 and self._current_obv > self._obv_avg_value:
# Exit Long: OBV returned to average
self.LogInfo("Exit Long - OBV ({0}) > OBV Avg ({1})".format(self._current_obv, self._obv_avg_value))
self.SellMarket(Math.Abs(self.Position))
elif self.Position < 0 and self._current_obv < self._obv_avg_value:
# Exit Short: OBV returned to average
self.LogInfo("Exit Short - OBV ({0}) < OBV Avg ({1})".format(self._current_obv, self._obv_avg_value))
self.BuyMarket(Math.Abs(self.Position))
def CreateClone(self):
"""
!! REQUIRED!! Creates a new instance of the strategy.
"""
return obv_mean_reversion_strategy()