Ver no GitHub

Estratégia Exp XBullsBearsEyes Vol Direta

Visão geral

Esta estratégia é uma conversão em C# do especialista MetaTrader Exp_XBullsBearsEyes_Vol_Direct. Ela recria o oscilador personalizado construído a partir de Bulls Power e Bears Power, multiplica-o por uma fonte de volume configurável e aplica suavização adaptativa idêntica ao indicador original. As decisões de negociação são guiadas exclusivamente pelo buffer de direção do indicador: o algoritmo reage a oscilações de momentum em vez de cruzamentos de nível, abrindo ou fechando posições quando o histograma suavizado muda de inclinação.

Ao contrário de muitas conversões, a versão StockSharp mantém a etapa de ponderação por volume e o filtro gamma de quatro níveis usado pelo código MQL. O oscilador é suavizado duas vezes com o mesmo método de média móvel — uma passagem para o histograma em si e uma para o fluxo de volume — de modo que os sinais aparecem apenas quando ambos os componentes estão totalmente formados. A estratégia processa apenas velas fechadas e suporta volume de ticks ou volume real negociado, tornando-a portável entre diferentes mercados.

Lógica do indicador

  1. Calcular Bulls Power e Bears Power com uma média móvel exponencial do preço de fechamento ao longo de Period velas.
  2. Aplicar o filtro gamma original de quatro estágios (parâmetros Gamma, L0L3) para combinar as duas forças em um histograma normalizado de -50 a +50.
  3. Multiplicar o histograma pela fonte de volume selecionada (contagem de ticks ou volume negociado).
  4. Suavizar o histograma e o volume bruto com a mesma família de médias móveis (Method, SmoothingLength, SmoothingPhase).
  5. Derivar um buffer de direção: cor 0 quando o histograma suavizado sobe, cor 1 quando cai. Isso imita o ColorDirectBuffer da implementação MetaTrader.

Os buffers de limiar superior/inferior do indicador são calculados internamente, mas não são usados para filtros de negociação, correspondendo ao comportamento do consultor especialista original que dependia apenas de mudanças de direção.

Regras de negociação

  • Fechar vendidos quando a direção da barra anterior era de alta (olderColor = 0).
  • Abrir comprados se entradas compradas são permitidas, uma barra de alta é seguida por uma de baixa (currentColor = 1), e a estratégia não está já comprada.
  • Fechar comprados quando a direção da barra anterior era de baixa (olderColor = 1).
  • Abrir vendidos se entradas vendidas são permitidas, uma barra de baixa é seguida por uma de alta (currentColor = 0), e nenhuma posição comprada está ativa.
  • Reversões de posição fecham o lado oposto primeiro e depois enviam uma ordem a mercado com o OrderVolume configurado.

Os sinais são avaliados com um deslocamento de barra configurável (SignalBar). O valor padrão de 1 emula o especialista MQL que esperava uma vela completamente fechada antes de reagir à mudança de direção.

Parâmetros

Nome Descrição
CandleType Tipo/período de vela subscrito pela estratégia (padrão: velas de 2 horas).
Period Período de lookback usado para Bulls/Bears Power.
Gamma Fator de suavização (0…1) do filtro gamma adaptativo.
VolumeMode Fonte de volume: contagem de ticks ou volume negociado.
Method Família de médias móveis usada para suavizar histograma e volume (SMA, EMA, SMMA, LWMA, Jurik; tipos legados não suportados revertem para SMA).
SmoothingLength Comprimento de ambos os estágios de suavização.
SmoothingPhase Parâmetro de fase Jurik (mantido para compatibilidade).
SignalBar Número de barras atrás a ler ao avaliar o buffer de direção.
AllowBuyOpen / AllowSellOpen Habilitar ou desabilitar a abertura de posições compradas/vendidas.
AllowBuyClose / AllowSellClose Habilitar ou desabilitar saídas forçadas em sinais opostos.
OrderVolume Tamanho da ordem a mercado para novas entradas.
StopLossPoints Stop de proteção opcional em passos de preço (0 desabilita o stop).
TakeProfitPoints Alvo de proteção opcional em passos de preço (0 desabilita o alvo).

Notas de uso

  • A estratégia opera sobre um único ativo retornado por GetWorkingSecurities() e funciona melhor em símbolos que fornecem um fluxo de volume estável.
  • Volume de ticks é recomendado para símbolos FX à vista onde o volume real negociado não está disponível. Defina VolumeMode como Real para bolsas que publicam volume executado.
  • As distâncias de stop-loss e take-profit são expressas em passos de preço e convertidas em unidades de preço absolutas usando o PriceStep do ativo.
  • Como a lógica depende de mudanças de direção, valores consecutivamente iguais do histograma mantêm a direção anterior até que uma nova inclinação apareça, exatamente como na versão MetaTrader.
  • A saída do gráfico exibe apenas velas de preço por padrão. Você pode adicionar plotagem personalizada para o histograma se a confirmação visual for necessária.
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
	}
}