Ver en GitHub

Estrategia Exp XBullsBearsEyes Vol Directa

Descripción general

Esta estrategia es una conversión en C# del experto de MetaTrader Exp_XBullsBearsEyes_Vol_Direct. Recrea el oscilador personalizado construido a partir de Bulls Power y Bears Power, lo multiplica por una fuente de volumen configurable y aplica un suavizado adaptativo idéntico al del indicador original. Las decisiones de trading se basan exclusivamente en el búfer de dirección del indicador: el algoritmo reacciona a los cambios de momentum en lugar de cruzar niveles, abriendo o cerrando posiciones cuando el histograma suavizado cambia de pendiente.

A diferencia de muchas conversiones, la versión de StockSharp conserva la etapa de ponderación por volumen y el filtro gamma de cuatro niveles utilizado en el código MQL. El oscilador se suaviza dos veces con el mismo método de media móvil —una pasada para el histograma y otra para el flujo de volumen—, por lo que las señales aparecen solo cuando ambos componentes están completamente formados. La estrategia procesa únicamente velas cerradas y admite volumen de ticks o volumen real negociado, lo que la hace portable entre diferentes mercados.

Lógica del indicador

  1. Calcular Bulls Power y Bears Power con una media móvil exponencial del precio de cierre sobre Period velas.
  2. Aplicar el filtro gamma de cuatro etapas original (parámetros Gamma, L0L3) para combinar las dos fuerzas en un histograma normalizado entre -50 y +50.
  3. Multiplicar el histograma por la fuente de volumen seleccionada (número de ticks o volumen negociado).
  4. Suavizar el histograma y el volumen bruto con la misma familia de medias móviles (Method, SmoothingLength, SmoothingPhase).
  5. Derivar un búfer de dirección: color 0 cuando el histograma suavizado sube, color 1 cuando baja. Esto imita el ColorDirectBuffer de la implementación de MetaTrader.

Los búferes de umbral superior/inferior del indicador se calculan internamente pero no se usan para filtros de operaciones, lo que reproduce el comportamiento del experto original que solo dependía de los cambios de dirección.

Reglas de trading

  • Cerrar cortos cuando la dirección de la barra anterior era alcista (olderColor = 0).
  • Abrir largos si las entradas largas están permitidas, una barra alcista es seguida por una bajista (currentColor = 1), y la estrategia no está ya en largo.
  • Cerrar largos cuando la dirección de la barra anterior era bajista (olderColor = 1).
  • Abrir cortos si las entradas cortas están permitidas, una barra bajista es seguida por una alcista (currentColor = 0), y no hay posición larga activa.
  • Las reversiones de posición cierran primero el lado opuesto y luego envían una orden de mercado con el OrderVolume configurado.

Las señales se evalúan con un desplazamiento de barra configurable (SignalBar). El valor predeterminado de 1 emula al experto MQL que esperaba una vela completamente cerrada antes de reaccionar al cambio de dirección.

Parámetros

Nombre Descripción
CandleType Tipo/marco temporal de vela suscrito por la estrategia (predeterminado: velas de 2 horas).
Period Período de lookback utilizado para Bulls/Bears Power.
Gamma Factor de suavizado (0…1) del filtro gamma adaptativo.
VolumeMode Fuente de volumen: número de ticks o volumen negociado.
Method Familia de medias móviles para suavizar histograma y volumen (SMA, EMA, SMMA, LWMA, Jurik; los tipos heredados no soportados vuelven a SMA).
SmoothingLength Longitud de ambas etapas de suavizado.
SmoothingPhase Parámetro de fase Jurik (mantenido por compatibilidad).
SignalBar Número de barras atrás para leer al evaluar el búfer de dirección.
AllowBuyOpen / AllowSellOpen Habilitar o deshabilitar la apertura de posiciones largas/cortas.
AllowBuyClose / AllowSellClose Habilitar o deshabilitar salidas forzadas en señales opuestas.
OrderVolume Tamaño de la orden de mercado para nuevas entradas.
StopLossPoints Stop de protección opcional en pasos de precio (0 deshabilita el stop).
TakeProfitPoints Objetivo de protección opcional en pasos de precio (0 deshabilita el objetivo).

Notas de uso

  • La estrategia opera sobre un único instrumento devuelto por GetWorkingSecurities() y funciona mejor en símbolos que proporcionan un flujo de volumen estable.
  • Se recomienda el volumen de ticks para símbolos de FX al contado donde el volumen real negociado no está disponible. Establezca VolumeMode en Real para bolsas que publiquen volumen ejecutado.
  • Las distancias de stop-loss y take-profit se expresan en pasos de precio y se convierten en unidades de precio absolutas usando el PriceStep del instrumento.
  • Debido a que la lógica se basa en cambios de dirección, valores del histograma consecutivamente iguales mantienen la dirección previa hasta que aparece una nueva pendiente, exactamente como en la versión de MetaTrader.
  • La salida del gráfico muestra solo velas de precio de forma predeterminada. Puede agregar gráficos personalizados para el histograma si se requiere confirmación visual.
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;

using StockSharp.Algo;

namespace StockSharp.Samples.Strategies;

/// <summary>
/// Strategy converted from the MetaTrader expert Exp_XBullsBearsEyes_Vol_Direct.
/// It recreates the smoothed Bulls/Bears Power oscillator multiplied by volume
/// and reacts to direction flips detected by the original indicator.
/// </summary>
public class ExpXBullsBearsEyesVolDirectStrategy : Strategy
{
	private readonly StrategyParam<DataType> _candleType;
	private readonly StrategyParam<int> _period;
	private readonly StrategyParam<decimal> _gamma;
	private readonly StrategyParam<VolumeSources> _volumeSource;
	private readonly StrategyParam<SmoothingMethods> _smoothingMethod;
	private readonly StrategyParam<int> _smoothingLength;
	private readonly StrategyParam<int> _smoothingPhase;
	private readonly StrategyParam<int> _signalBar;
	private readonly StrategyParam<bool> _allowBuyOpen;
	private readonly StrategyParam<bool> _allowSellOpen;
	private readonly StrategyParam<bool> _allowBuyClose;
	private readonly StrategyParam<bool> _allowSellClose;
	private readonly StrategyParam<decimal> _orderVolume;
	private readonly StrategyParam<int> _stopLossPoints;
	private readonly StrategyParam<int> _takeProfitPoints;

	private ExponentialMovingAverage _ema;
	private IIndicator _histogramSmoother;
	private IIndicator _volumeSmoother;

	private readonly List<int> _directionHistory = new();
	private decimal _l0;
	private decimal _l1;
	private decimal _l2;
	private decimal _l3;
	private decimal? _previousSmoothedValue;
	private int _previousDirection;

	/// <summary>
	/// Candle type used for calculations and trading signals.
	/// </summary>
	public DataType CandleType
	{
		get => _candleType.Value;
		set => _candleType.Value = value;
	}

	/// <summary>
	/// Lookback period of Bulls/Bears Power.
	/// </summary>
	public int Period
	{
		get => _period.Value;
		set => _period.Value = value;
	}

	/// <summary>
	/// Smoothing factor applied to the adaptive filter (0..1).
	/// </summary>
	public decimal Gamma
	{
		get => _gamma.Value;
		set => _gamma.Value = value;
	}

	/// <summary>
	/// Volume source used to weight the oscillator.
	/// </summary>
	public VolumeSources VolumeMode
	{
		get => _volumeSource.Value;
		set => _volumeSource.Value = value;
	}

	/// <summary>
	/// Moving average type for smoothing histogram and volume.
	/// </summary>
	public SmoothingMethods Method
	{
		get => _smoothingMethod.Value;
		set => _smoothingMethod.Value = value;
	}

	/// <summary>
	/// Length of the smoothing windows.
	/// </summary>
	public int SmoothingLength
	{
		get => _smoothingLength.Value;
		set => _smoothingLength.Value = value;
	}

	/// <summary>
	/// Jurik phase parameter kept for compatibility.
	/// </summary>
	public int SmoothingPhase
	{
		get => _smoothingPhase.Value;
		set => _smoothingPhase.Value = value;
	}

	/// <summary>
	/// Bar shift used when reading direction buffers.
	/// </summary>
	public int SignalBar
	{
		get => _signalBar.Value;
		set => _signalBar.Value = value;
	}

	/// <summary>
	/// Allow opening long positions.
	/// </summary>
	public bool AllowBuyOpen
	{
		get => _allowBuyOpen.Value;
		set => _allowBuyOpen.Value = value;
	}

	/// <summary>
	/// Allow opening short positions.
	/// </summary>
	public bool AllowSellOpen
	{
		get => _allowSellOpen.Value;
		set => _allowSellOpen.Value = value;
	}

	/// <summary>
	/// Allow closing long positions on bearish signals.
	/// </summary>
	public bool AllowBuyClose
	{
		get => _allowBuyClose.Value;
		set => _allowBuyClose.Value = value;
	}

	/// <summary>
	/// Allow closing short positions on bullish signals.
	/// </summary>
	public bool AllowSellClose
	{
		get => _allowSellClose.Value;
		set => _allowSellClose.Value = value;
	}

	/// <summary>
	/// Default order size.
	/// </summary>
	public decimal OrderVolume
	{
		get => _orderVolume.Value;
		set => _orderVolume.Value = value;
	}

	/// <summary>
	/// Stop-loss distance measured in price steps.
	/// </summary>
	public int StopLossPoints
	{
		get => _stopLossPoints.Value;
		set => _stopLossPoints.Value = value;
	}

	/// <summary>
	/// Take-profit distance measured in price steps.
	/// </summary>
	public int TakeProfitPoints
	{
		get => _takeProfitPoints.Value;
		set => _takeProfitPoints.Value = value;
	}

	/// <summary>
	/// Initializes a new instance of <see cref="ExpXBullsBearsEyesVolDirectStrategy"/>.
	/// </summary>
	public ExpXBullsBearsEyesVolDirectStrategy()
	{
		_candleType = Param(nameof(CandleType), TimeSpan.FromHours(2).TimeFrame())
		.SetDisplay("Candle Type", "Timeframe used by the indicator", "General");

		_period = Param(nameof(Period), 13)
		.SetGreaterThanZero()
		.SetDisplay("Bulls/Bears Period", "Lookback window of Bulls/Bears Power", "Indicator");

		_gamma = Param(nameof(Gamma), 0.6m)
		.SetDisplay("Gamma", "Adaptive filter smoothing factor", "Indicator");

		_volumeSource = Param(nameof(VolumeMode), VolumeSources.Tick)
		.SetDisplay("Volume Source", "Volume applied to the histogram", "Indicator");

		_smoothingMethod = Param(nameof(Method), SmoothingMethods.Sma)
		.SetDisplay("Smoothing Method", "Moving average type for histogram and volume", "Indicator");

		_smoothingLength = Param(nameof(SmoothingLength), 12)
		.SetGreaterThanZero()
		.SetDisplay("Smoothing Length", "Length of the smoothing moving averages", "Indicator");

		_smoothingPhase = Param(nameof(SmoothingPhase), 15)
		.SetDisplay("Smoothing Phase", "Phase parameter used by Jurik averaging", "Indicator");

		_signalBar = Param(nameof(SignalBar), 1)
		.SetNotNegative()
		.SetDisplay("Signal Bar", "Shift applied when evaluating direction", "Trading");

		_allowBuyOpen = Param(nameof(AllowBuyOpen), true)
		.SetDisplay("Allow Buy Open", "Enable opening long positions", "Trading");

		_allowSellOpen = Param(nameof(AllowSellOpen), true)
		.SetDisplay("Allow Sell Open", "Enable opening short positions", "Trading");

		_allowBuyClose = Param(nameof(AllowBuyClose), true)
		.SetDisplay("Allow Buy Close", "Enable closing longs on bearish flips", "Trading");

		_allowSellClose = Param(nameof(AllowSellClose), true)
		.SetDisplay("Allow Sell Close", "Enable closing shorts on bullish flips", "Trading");

		_orderVolume = Param(nameof(OrderVolume), 1m)
		.SetGreaterThanZero()
		.SetDisplay("Order Volume", "Default market order size", "Trading");

		_stopLossPoints = Param(nameof(StopLossPoints), 1000)
		.SetNotNegative()
		.SetDisplay("Stop Loss", "Protective stop in price steps", "Risk");

		_takeProfitPoints = Param(nameof(TakeProfitPoints), 2000)
		.SetNotNegative()
		.SetDisplay("Take Profit", "Protective target in price steps", "Risk");
	}

	/// <inheritdoc />
	public override IEnumerable<(Security sec, DataType dt)> GetWorkingSecurities()
	{
		return [(Security, CandleType)];
	}

	/// <inheritdoc />
	protected override void OnStarted2(DateTime time)
	{
		base.OnStarted2(time);

		_ema = new EMA
		{
			Length = Math.Max(1, Period)
		};

		_histogramSmoother = CreateMovingAverage(Method, SmoothingLength, SmoothingPhase);
		_volumeSmoother = CreateMovingAverage(Method, SmoothingLength, SmoothingPhase);

		_directionHistory.Clear();
		_previousSmoothedValue = null;
		_previousDirection = 0;
		_l0 = _l1 = _l2 = _l3 = 0m;

		var subscription = SubscribeCandles(CandleType);
		subscription
		.Bind(ProcessCandle)
		.Start();

		Volume = Math.Max(Security?.MinVolume ?? 1m, OrderVolume);

		var priceStep = Security?.PriceStep ?? 0m;
		Unit stopLoss = null;
		Unit takeProfit = null;

		if (StopLossPoints > 0 && priceStep > 0m)
		{
			stopLoss = new Unit(StopLossPoints * priceStep, UnitTypes.Absolute);
		}

		if (TakeProfitPoints > 0 && priceStep > 0m)
		{
			takeProfit = new Unit(TakeProfitPoints * priceStep, UnitTypes.Absolute);
		}

		if (stopLoss != null || takeProfit != null)
		{
			StartProtection(takeProfit: takeProfit, stopLoss: stopLoss);
		}

		var area = CreateChartArea();
		if (area != null)
		{
			DrawCandles(area, subscription);
		}
	}

	/// <inheritdoc />
	protected override void OnReseted()
	{
		base.OnReseted();

		_ema?.Reset();
		_histogramSmoother?.Reset();
		_volumeSmoother?.Reset();
		_directionHistory.Clear();
		_previousSmoothedValue = null;
		_previousDirection = 0;
		_l0 = _l1 = _l2 = _l3 = 0m;
	}

	private void ProcessCandle(ICandleMessage candle)
	{
		if (candle.State != CandleStates.Finished)
		return;

		if (_ema is null || _histogramSmoother is null || _volumeSmoother is null)
		return;

		// Feed the EMA with the candle close to emulate iBullsPower/iBearsPower internals.
		var emaValue = _ema.Process(new DecimalIndicatorValue(_ema, candle.ClosePrice, candle.OpenTime) { IsFinal = true });
		if (!emaValue.IsFinal)
		return;

		var ema = emaValue.ToDecimal();

		// Rebuild Bulls and Bears Power values.
		var bulls = candle.HighPrice - ema;
		var bears = candle.LowPrice - ema;

		// Run the four-stage adaptive smoothing described in the MQL version.
		var l0Prev = _l0;
		var l1Prev = _l1;
		var l2Prev = _l2;
		var l3Prev = _l3;

		var sum = bulls + bears;
		var gamma = Gamma;

		_l0 = ((1m - gamma) * sum) + (gamma * l0Prev);
		_l1 = (-gamma * _l0) + l0Prev + (gamma * l1Prev);
		_l2 = (-gamma * _l1) + l1Prev + (gamma * l2Prev);
		_l3 = (-gamma * _l2) + l2Prev + (gamma * l3Prev);

		var cu = 0m;
		var cd = 0m;

		if (_l0 >= _l1)
		{
			cu = _l0 - _l1;
		}
		else
		{
			cd = _l1 - _l0;
		}

		if (_l1 >= _l2)
		{
			cu += _l1 - _l2;
		}
		else
		{
			cd += _l2 - _l1;
		}

		if (_l2 >= _l3)
		{
			cu += _l2 - _l3;
		}
		else
		{
			cd += _l3 - _l2;
		}

		var denom = cu + cd;
		var result = denom != 0m ? cu / denom : 0m;
		var histogram = (result * 100m) - 50m;

		// Apply the requested volume type to the histogram.
		var volume = GetVolume(candle);
		var scaledHistogram = histogram * volume;

		var histogramValue = _histogramSmoother.Process(new DecimalIndicatorValue(_histogramSmoother, scaledHistogram, candle.OpenTime) { IsFinal = true });
		var volumeValue = _volumeSmoother.Process(new DecimalIndicatorValue(_volumeSmoother, volume, candle.OpenTime) { IsFinal = true });

		if (histogramValue is not DecimalIndicatorValue { IsFinal: true } histogramResult)
		return;

		if (volumeValue is not DecimalIndicatorValue { IsFinal: true })
		return;

		var smoothedHistogram = histogramResult.Value;
		var direction = CalculateDirection(smoothedHistogram);

		UpdateHistory(direction);

		// trading guard removed

		if (!TryGetColors(out var olderColor, out var currentColor))
		return;

		HandleSignals(olderColor, currentColor);
	}

	private int CalculateDirection(decimal currentValue)
	{
		if (_previousSmoothedValue is not decimal previous)
		{
			_previousSmoothedValue = currentValue;
			_previousDirection = 0;
			return _previousDirection;
		}

		int direction;
		if (currentValue > previous)
		{
			direction = 0;
		}
		else if (currentValue < previous)
		{
			direction = 1;
		}
		else
		{
			direction = _previousDirection;
		}

		_previousSmoothedValue = currentValue;
		_previousDirection = direction;
		return direction;
	}

	private void UpdateHistory(int direction)
	{
		_directionHistory.Add(direction);
		var maxHistory = Math.Max(4, SignalBar + 3);
		if (_directionHistory.Count > maxHistory)
		{
			_directionHistory.RemoveAt(0);
		}
	}

	private bool TryGetColors(out int olderColor, out int currentColor)
	{
		olderColor = 0;
		currentColor = 0;

		var shift = Math.Max(0, SignalBar);
		var currentIndex = _directionHistory.Count - 1 - shift;
		var olderIndex = currentIndex - 1;

		if (currentIndex < 0 || olderIndex < 0)
		{
			return false;
		}

		currentColor = _directionHistory[currentIndex];
		olderColor = _directionHistory[olderIndex];
		return true;
	}

	private void HandleSignals(int olderColor, int currentColor)
	{
		// Color 0 is produced when the smoothed histogram rises, color 1 when it declines.
		if (olderColor == 0)
		{
			if (AllowSellClose && Position < 0)
			{
				BuyMarket(-Position);
			}

			if (AllowBuyOpen && currentColor == 1 && Position <= 0)
			{
				BuyMarket(Volume + Math.Abs(Position));
			}
		}
		else if (olderColor == 1)
		{
			if (AllowBuyClose && Position > 0)
			{
				SellMarket(Position);
			}

			if (AllowSellOpen && currentColor == 0 && Position >= 0)
			{
				SellMarket(Volume + Math.Abs(Position));
			}
		}
	}

	private decimal GetVolume(ICandleMessage candle)
	{
		return VolumeMode switch
		{
			VolumeSources.Tick => candle.TotalTicks.HasValue ? (decimal)candle.TotalTicks.Value : candle.TotalVolume,
			VolumeSources.Real => candle.TotalVolume,
			_ => candle.TotalVolume,
		};
	}

	private static IIndicator CreateMovingAverage(SmoothingMethods method, int length, int phase)
	{
		var effectiveLength = Math.Max(1, length);
		return method switch
		{
			SmoothingMethods.Sma => new SMA { Length = effectiveLength },
			SmoothingMethods.Ema => new EMA { Length = effectiveLength },
			SmoothingMethods.Smma => new SmoothedMovingAverage { Length = effectiveLength },
			SmoothingMethods.Lwma => new WeightedMovingAverage { Length = effectiveLength },
			SmoothingMethods.Jurik => CreateJurik(effectiveLength, phase),
			_ => new SMA { Length = effectiveLength },
		};
	}

	private static IIndicator CreateJurik(int length, int phase)
	{
		var jurik = new JurikMovingAverage
		{
			Length = Math.Max(1, length)
		};

		var property = jurik.GetType().GetProperty("Phase");
		if (property != null)
		{
			var value = Math.Max(-100, Math.Min(100, phase));
			property.SetValue(jurik, value);
		}

		return jurik;
	}

	/// <summary>
	/// Supported volume sources.
	/// </summary>
	public enum VolumeSources
	{
		/// <summary>
		/// Use tick count when available.
		/// </summary>
		Tick,

		/// <summary>
		/// Use traded volume (lots/contracts).
		/// </summary>
		Real
	}

	/// <summary>
	/// Supported smoothing methods.
	/// </summary>
	public enum SmoothingMethods
	{
		/// <summary>
		/// Simple moving average.
		/// </summary>
		Sma,

		/// <summary>
		/// Exponential moving average.
		/// </summary>
		Ema,

		/// <summary>
		/// Smoothed moving average (RMA).
		/// </summary>
		Smma,

		/// <summary>
		/// Linear weighted moving average.
		/// </summary>
		Lwma,

		/// <summary>
		/// Jurik moving average.
		/// </summary>
		Jurik,

		/// <summary>
		/// Parabolic, T3, VIDYA and AMA are not available in StockSharp by default.
		/// The strategy falls back to SMA for these legacy options.
		/// </summary>
		Parabolic,

		/// <summary>
		/// Tillson T3 smoother.
		/// </summary>
		T3,

		/// <summary>
		/// Variable index dynamic average.
		/// </summary>
		Vidya,

		/// <summary>
		/// Adaptive moving average.
		/// </summary>
		Ama
	}
}