OBV Breakout
On-Balance Volume (OBV) tracks buying and selling pressure by accumulating volume. This strategy looks for OBV to break above a high or below a low over the lookback window while price confirms the move.
Testing indicates an average annual return of about 178%. It performs best in the stocks market.
A breakout in OBV suggests strong interest. The system goes long if OBV exceeds its previous maximum, or short if it breaks the minimum. Crossing the OBV moving average signals an exit.
This combines volume momentum with price action.
Details
- Entry Criteria: OBV surpasses highest or lowest value in lookback period.
- Long/Short: Both directions.
- Exit Criteria: OBV crosses its MA or stop.
- Stops: Yes.
- Default Values:
LookbackPeriod= 20OBVMAPeriod= 20CandleType= TimeSpan.FromMinutes(5)
- Filters:
- Category: Breakout
- Direction: Both
- Indicators: OBV, MA
- Stops: Yes
- Complexity: Intermediate
- Timeframe: Intraday
- Seasonality: No
- Neural Networks: No
- Divergence: No
- 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>
/// On-Balance Volume (OBV) Breakout strategy.
/// Enters when OBV crosses its moving average indicating volume confirmation of trend.
/// </summary>
public class ObvBreakoutStrategy : Strategy
{
private readonly StrategyParam<int> _maPeriod;
private readonly StrategyParam<DataType> _candleType;
private readonly StrategyParam<int> _cooldownBars;
private decimal _prevObv;
private int _cooldown;
/// <summary>
/// MA Period for OBV average.
/// </summary>
public int MAPeriod
{
get => _maPeriod.Value;
set => _maPeriod.Value = value;
}
/// <summary>
/// Candle type for strategy calculation.
/// </summary>
public DataType CandleType
{
get => _candleType.Value;
set => _candleType.Value = value;
}
/// <summary>
/// Cooldown bars between trades.
/// </summary>
public int CooldownBars
{
get => _cooldownBars.Value;
set => _cooldownBars.Value = value;
}
/// <summary>
/// Initialize the OBV Breakout strategy.
/// </summary>
public ObvBreakoutStrategy()
{
_maPeriod = Param(nameof(MAPeriod), 20)
.SetDisplay("MA Period", "Period for OBV moving average", "Indicators")
.SetOptimize(10, 50, 10);
_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();
_prevObv = default;
_cooldown = default;
}
/// <inheritdoc />
protected override void OnStarted2(DateTime time)
{
base.OnStarted2(time);
_prevObv = 0;
_cooldown = 0;
var obv = new OnBalanceVolume();
var sma = new SimpleMovingAverage { Length = MAPeriod };
var subscription = SubscribeCandles(CandleType);
subscription
.Bind(obv, sma, ProcessCandle)
.Start();
var area = CreateChartArea();
if (area != null)
{
DrawCandles(area, subscription);
DrawOwnTrades(area);
}
}
private void ProcessCandle(ICandleMessage candle, decimal obvValue, decimal smaValue)
{
if (candle.State != CandleStates.Finished)
return;
if (!IsFormedAndOnlineAndAllowTrading())
return;
if (_prevObv == 0)
{
_prevObv = obvValue;
return;
}
if (_cooldown > 0)
{
_cooldown--;
_prevObv = obvValue;
return;
}
// Use OBV direction + price vs SMA for signals
var obvRising = obvValue > _prevObv;
if (Position == 0)
{
if (obvRising && candle.ClosePrice > smaValue)
{
BuyMarket();
_cooldown = CooldownBars;
}
else if (!obvRising && candle.ClosePrice < smaValue)
{
SellMarket();
_cooldown = CooldownBars;
}
}
else if (Position > 0 && !obvRising)
{
SellMarket();
_cooldown = CooldownBars;
}
else if (Position < 0 && obvRising)
{
BuyMarket();
_cooldown = CooldownBars;
}
_prevObv = obvValue;
}
}
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 OnBalanceVolume, SimpleMovingAverage
from StockSharp.Algo.Strategies import Strategy
class obv_breakout_strategy(Strategy):
"""
On-Balance Volume (OBV) Breakout strategy.
Enters when OBV direction confirms price trend relative to SMA.
"""
def __init__(self):
super(obv_breakout_strategy, self).__init__()
self._ma_period = self.Param("MAPeriod", 20).SetDisplay("MA Period", "Period for OBV moving average", "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._prev_obv = 0.0
self._cooldown = 0
@property
def candle_type(self):
return self._candle_type.Value
def OnReseted(self):
super(obv_breakout_strategy, self).OnReseted()
self._prev_obv = 0.0
self._cooldown = 0
def OnStarted2(self, time):
super(obv_breakout_strategy, self).OnStarted2(time)
self._prev_obv = 0.0
self._cooldown = 0
obv = OnBalanceVolume()
sma = SimpleMovingAverage()
sma.Length = self._ma_period.Value
subscription = self.SubscribeCandles(self.candle_type)
subscription.Bind(obv, sma, self._process_candle).Start()
area = self.CreateChartArea()
if area is not None:
self.DrawCandles(area, subscription)
self.DrawOwnTrades(area)
def _process_candle(self, candle, obv_val, sma_val):
if candle.State != CandleStates.Finished:
return
ov = float(obv_val)
if self._prev_obv == 0:
self._prev_obv = ov
return
if self._cooldown > 0:
self._cooldown -= 1
self._prev_obv = ov
return
obv_rising = ov > self._prev_obv
close = float(candle.ClosePrice)
sv = float(sma_val)
cd = self._cooldown_bars.Value
if self.Position == 0:
if obv_rising and close > sv:
self.BuyMarket()
self._cooldown = cd
elif not obv_rising and close < sv:
self.SellMarket()
self._cooldown = cd
elif self.Position > 0 and not obv_rising:
self.SellMarket()
self._cooldown = cd
elif self.Position < 0 and obv_rising:
self.BuyMarket()
self._cooldown = cd
self._prev_obv = ov
def CreateClone(self):
return obv_breakout_strategy()