Ver en GitHub

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 = 5
    • MAPeriod = 20
    • CandleType = 5 minute
    • StopLossPercent = 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;
		}
	}
}