Estrategia de Divergencia con OBV
El On-Balance Volume rastrea el volumen de operaciones acumulado con la idea de que el volumen precede al precio. Cuando el precio forma un nuevo máximo pero OBV no lo confirma, o viceversa, puede estar gestándose una reversión. Esta estrategia usa esa divergencia para operar en contra de movimientos insostenibles.
Las pruebas indican un rendimiento anual promedio de aproximadamente el 112%. Funciona mejor en el mercado de divisas.
Para cada vela, el OBV se actualiza y se compara con la lectura anterior. Surge una señal alcista si el precio hace un mínimo más bajo mientras el OBV registra un mínimo más alto. Una señal bajista ocurre cuando el precio sube a un máximo más alto pero el OBV se queda rezagado. Una media móvil proporciona un punto de salida, mientras que un stop porcentual mantiene las pérdidas controladas.
El enfoque intenta capturar la reversión a la media tras el agotamiento del volumen y generalmente mantiene las operaciones solo hasta que el precio cruza de vuelta la línea media.
Detalles
- Criterios de entrada: Divergencia Precio/OBV.
- Largo/Corto: Ambos.
- Criterios de salida: Precio cruzando la media móvil o stop-loss.
- Stops: Sí, basados en porcentaje.
- Valores predeterminados:
DivergencePeriod= 5MAPeriod= 20CandleType= 5 minuteStopLossPercent= 2
- Filtros:
- Categoría: Divergencia
- Dirección: Ambos
- Indicadores: OBV, MA
- Stops: Sí
- Complejidad: Intermedio
- Marco temporal: Intradía
- Estacionalidad: No
- Redes neuronales: No
- Divergencia: Sí
- Nivel de riesgo: Medio
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()