Ver no GitHub

Estratégia Exp XBullsBearsEyes Vol

Visão geral

Esta estratégia é uma conversão em C# do expert de MetaTrader Exp_XBullsBearsEyes_Vol. O consultor original combina as leituras de Bulls Power e Bears Power, multiplica o resultado pelo volume do candle e colore o histograma de acordo com o momentum resultante. Dois slots de posição independentes são mantidos tanto para o lado longo quanto para o curto, permitindo ao sistema escalar quando a intensidade da cor aumenta. O port para StockSharp recria o filtro multi-estágio, a lógica de cores e o gerenciamento de negociações enquanto usa chamadas de API de alto nível para ordens e controle de risco.

O algoritmo subscreve um período configurável, reconstrói o indicador XBullsBearsEyes personalizado e reage apenas a candles concluídos. As transições de cor determinam tanto as entradas quanto as saídas: cores de alta fecham negociações curtas e podem abrir um ou dois slots longos; cores de baixa realizam a ação espelhada. As distâncias de stop-loss e take-profit são traduzidas em parâmetros de StartProtection para que os gerenciadores de risco da plataforma possam lidar com ordens protetoras.

Lógica do indicador

  1. Os valores de Bulls Power e Bears Power são reconstruídos com uma EMA de período IndicatorPeriod usando a máxima/mínima do candle contra o fechamento suavizado.
  2. Um filtro adaptativo de quatro estágios acumula pressão de alta (CU) e de baixa (CD) com coeficiente Gamma. O valor do indicador é CU / (CU + CD) * 100 - 50.
  3. O valor filtrado é multiplicado pelo volume de tick ou volume real, dependendo de VolumeType.
  4. As séries multiplicadas e o volume bruto são suavizados por uma média móvel escolhida através de SmoothingMethod, SmoothingLength e SmoothingPhase (a fase Jurik é respeitada quando a classe subjacente a expõe).
  5. Os níveis de cor são derivados de HighLevel1, HighLevel2, LowLevel1 e LowLevel2. Valores acima das bandas superiores produzem cores 0 ou 1, enquanto valores abaixo das bandas inferiores produzem cores 3 ou 4. A cor 2 indica um estado neutro.
  6. O histórico de cores é armazenado para que os sinais possam ser avaliados na barra SignalBar (padrão: um candle fechado atrás). A cor da barra de sinal atual é comparada com a cor anterior para detectar transições.

Regras de negociação

  • As cores 1 e 0 denotam pressão de alta. Quando a cor muda para um desses valores e a cor anterior era mais fraca, o slot 1 (PrimaryVolume) ou slot 2 (SecondaryVolume) abre uma posição longa respectivamente. Ambos os eventos fecham qualquer exposição curta existente se AllowShortExit estiver habilitado.
  • As cores 3 e 4 denotam pressão de baixa. Quando a cor se move para esses valores e a cor anterior era mais forte, o slot 1 ou slot 2 abre uma posição curta respectivamente. Ambos os eventos fecham qualquer exposição longa existente se AllowLongExit estiver habilitado.
  • Cada slot lembra se já tem uma posição aberta e ignora sinais repetidos até que a direção correspondente tenha sido fechada.
  • SignalBar define quantos candles concluídos são pulados antes de avaliar a cor (0 = último candle terminado). O código requer pelo menos dois históricos de cores para comparar.
  • Stop-loss e take-profit expressos em pontos (StopLossPoints, TakeProfitPoints) são convertidos em distâncias de preço absoluto com Security.PriceStep e usados para iniciar a proteção da plataforma com saídas de mercado.

Parâmetros

Nome Descrição
PrimaryVolume Volume para o primeiro slot (acionado pela cor 1 / 3).
SecondaryVolume Volume para o segundo slot (acionado pela cor 0 / 4).
StopLossPoints / TakeProfitPoints Distâncias protetoras em passos de preço. Definir como zero para desabilitar.
AllowLongEntry / AllowShortEntry Habilitar escalonamento na direção correspondente.
AllowLongExit / AllowShortExit Habilitar saídas automatizadas quando a cor oposta aparecer.
CandleType Período subscrito para candles e cálculo do indicador (padrão: 8 horas).
IndicatorPeriod Período EMA usado para reconstruir Bulls/Bears Power.
Gamma Fator de suavização adaptativo para o filtro de quatro estágios (0.0 – 0.999).
VolumeType Selecionar volume de tick ou volume real para ponderação.
HighLevel1, HighLevel2, LowLevel1, LowLevel2 Multiplicadores de nível que definem os limiares de cor.
SmoothingMethod Tipo de média móvel usado para suavizar o indicador e o volume (SMA, EMA, SMMA, LWMA, Jurik, JurX, ParMA→EMA, T3, VIDYA→EMA, AMA).
SmoothingLength Comprimento da média móvel de suavização.
SmoothingPhase Parâmetro de fase Jurik (limitado a [-100, 100]).
SignalBar Número de candles fechados a recuar antes de avaliar as transições de cor.

Notas de uso

  • A estratégia opera com um único instrumento retornado por GetWorkingSecurities() e usa ordens de mercado para entradas e saídas.
  • O gerenciamento de slots é líquido: entradas adicionais somam à posição líquida, enquanto saídas aplainam toda a exposição para o lado afetado.
  • Se a plataforma fornecer apenas volume de tick, selecionar VolumeType = Real recorrerá à contagem de tick disponível.
  • As suavizações VIDYA e Parabólica recorrem a médias móveis exponenciais porque o StockSharp expõe essas implementações diretamente.
  • Certificar-se de configurar o passo de preço do instrumento para que StopLossPoints e TakeProfitPoints sejam convertidos nas distâncias absolutas pretendidas.
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 System.Reflection;
using StockSharp.Algo;
using StockSharp.Algo.Candles;

namespace StockSharp.Samples.Strategies;

/// <summary>
/// Strategy converted from the MetaTrader expert Exp_XBullsBearsEyes_Vol.
/// It recreates the Bulls/Bears pressure indicator that multiplies trend
/// strength by the candle volume and uses the colour transitions to drive
/// entries and exits while supporting two independent position slots per side.
	/// </summary>
public class ExpXBullsBearsEyesVolStrategy : Strategy
{
	private readonly StrategyParam<decimal> _primaryVolume;
	private readonly StrategyParam<decimal> _secondaryVolume;
	private readonly StrategyParam<int> _stopLossPoints;
	private readonly StrategyParam<int> _takeProfitPoints;
	private readonly StrategyParam<bool> _allowLongEntry;
	private readonly StrategyParam<bool> _allowShortEntry;
	private readonly StrategyParam<bool> _allowLongExit;
	private readonly StrategyParam<bool> _allowShortExit;
	private readonly StrategyParam<DataType> _candleType;

	private readonly StrategyParam<int> _indicatorPeriod;
	private readonly StrategyParam<decimal> _gamma;
	private readonly StrategyParam<AppliedVolumes> _volumeType;
	private readonly StrategyParam<int> _highLevel2;
	private readonly StrategyParam<int> _highLevel1;
	private readonly StrategyParam<int> _lowLevel1;
	private readonly StrategyParam<int> _lowLevel2;
	private readonly StrategyParam<SmoothMethods> _smoothMethod;
	private readonly StrategyParam<int> _smoothLength;
	private readonly StrategyParam<int> _smoothPhase;
	private readonly StrategyParam<int> _signalBar;

	private XBullsBearsEyesVolCalculator _indicator;

	private readonly List<ColorSample> _colorHistory = new();

	private DateTimeOffset? _lastLongPrimarySignalTime;
	private DateTimeOffset? _lastLongSecondarySignalTime;
	private DateTimeOffset? _lastShortPrimarySignalTime;
	private DateTimeOffset? _lastShortSecondarySignalTime;

	private bool _isLongPrimaryOpen;
	private bool _isLongSecondaryOpen;
	private bool _isShortPrimaryOpen;
	private bool _isShortSecondaryOpen;

	/// <summary>
	/// Initializes a new instance of the <see cref="ExpXBullsBearsEyesVolStrategy"/> class.
	/// </summary>
	public ExpXBullsBearsEyesVolStrategy()
	{
		_primaryVolume = Param(nameof(PrimaryVolume), 0.1m)
		.SetGreaterThanZero()
		.SetDisplay("Primary Volume", "Order volume used by the first long/short slot", "Trading");

		_secondaryVolume = Param(nameof(SecondaryVolume), 0.2m)
		.SetGreaterThanZero()
		.SetDisplay("Secondary Volume", "Order volume used by the second long/short slot", "Trading");

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

		_takeProfitPoints = Param(nameof(TakeProfitPoints), 2000)
		.SetNotNegative()
		.SetDisplay("Take Profit (points)", "Target distance expressed in price steps", "Risk");

		_allowLongEntry = Param(nameof(AllowLongEntry), true)
		.SetDisplay("Allow Long Entry", "Enable opening long positions", "Trading");

		_allowShortEntry = Param(nameof(AllowShortEntry), true)
		.SetDisplay("Allow Short Entry", "Enable opening short positions", "Trading");

		_allowLongExit = Param(nameof(AllowLongExit), true)
		.SetDisplay("Allow Long Exit", "Enable closing long positions on bearish colours", "Trading");

		_allowShortExit = Param(nameof(AllowShortExit), true)
		.SetDisplay("Allow Short Exit", "Enable closing short positions on bullish colours", "Trading");

		_candleType = Param(nameof(CandleType), TimeSpan.FromHours(8).TimeFrame())
		.SetDisplay("Candle Type", "Timeframe used by the indicator and trading signals", "General");

		_indicatorPeriod = Param(nameof(IndicatorPeriod), 13)
		.SetGreaterThanZero()
		.SetDisplay("Indicator Period", "EMA period used by Bulls/Bears power", "Indicator");

		_gamma = Param(nameof(Gamma), 0.6m)
		.SetDisplay("Gamma", "Adaptive smoothing factor used by the four-stage filter", "Indicator");

		_volumeType = Param(nameof(VolumeType), AppliedVolumes.Tick)
		.SetDisplay("Volume Type", "Volume source multiplied by the indicator", "Indicator");

		_highLevel2 = Param(nameof(HighLevel2), 25)
		.SetDisplay("High Level 2", "Upper level that marks strong bullish pressure", "Indicator");

		_highLevel1 = Param(nameof(HighLevel1), 10)
		.SetDisplay("High Level 1", "Upper level that marks moderate bullish pressure", "Indicator");

		_lowLevel1 = Param(nameof(LowLevel1), -10)
		.SetDisplay("Low Level 1", "Lower level that marks moderate bearish pressure", "Indicator");

		_lowLevel2 = Param(nameof(LowLevel2), -25)
		.SetDisplay("Low Level 2", "Lower level that marks strong bearish pressure", "Indicator");

		_smoothMethod = Param(nameof(SmoothingMethod), SmoothMethods.Sma)
		.SetDisplay("Smoothing Method", "Moving average used for indicator smoothing", "Indicator");

		_smoothLength = Param(nameof(SmoothingLength), 12)
		.SetGreaterThanZero()
		.SetDisplay("Smoothing Length", "Length of the smoothing filter", "Indicator");

		_smoothPhase = Param(nameof(SmoothingPhase), 15)
		.SetDisplay("Smoothing Phase", "Phase parameter for Jurik based smoothing", "Indicator");

		_signalBar = Param(nameof(SignalBar), 1)
		.SetNotNegative()
		.SetDisplay("Signal Bar", "Shift applied before evaluating colour transitions", "Trading");
	}

	/// <summary>
	/// Volume used by the first long/short slot.
	/// </summary>
	public decimal PrimaryVolume
	{
		get => _primaryVolume.Value;
		set => _primaryVolume.Value = value;
	}

	/// <summary>
	/// Volume used by the second long/short slot.
	/// </summary>
	public decimal SecondaryVolume
	{
		get => _secondaryVolume.Value;
		set => _secondaryVolume.Value = value;
	}

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

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

	/// <summary>
	/// Enable or disable opening long positions.
	/// </summary>
	public bool AllowLongEntry
	{
		get => _allowLongEntry.Value;
		set => _allowLongEntry.Value = value;
	}

	/// <summary>
	/// Enable or disable opening short positions.
	/// </summary>
	public bool AllowShortEntry
	{
		get => _allowShortEntry.Value;
		set => _allowShortEntry.Value = value;
	}

	/// <summary>
	/// Enable or disable closing long positions on bearish colours.
	/// </summary>
	public bool AllowLongExit
	{
		get => _allowLongExit.Value;
		set => _allowLongExit.Value = value;
	}

	/// <summary>
	/// Enable or disable closing short positions on bullish colours.
	/// </summary>
	public bool AllowShortExit
	{
		get => _allowShortExit.Value;
		set => _allowShortExit.Value = value;
	}

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

	/// <summary>
	/// EMA period used by Bulls/Bears power calculations.
	/// </summary>
	public int IndicatorPeriod
	{
		get => _indicatorPeriod.Value;
		set => _indicatorPeriod.Value = value;
	}

	/// <summary>
	/// Adaptive smoothing factor used by the internal filter.
	/// </summary>
	public decimal Gamma
	{
		get => _gamma.Value;
		set => _gamma.Value = value;
	}

	/// <summary>
	/// Volume source multiplied by the indicator output.
	/// </summary>
	public AppliedVolumes VolumeType
	{
		get => _volumeType.Value;
		set => _volumeType.Value = value;
	}

	/// <summary>
	/// Upper level that marks strong bullish pressure.
	/// </summary>
	public int HighLevel2
	{
		get => _highLevel2.Value;
		set => _highLevel2.Value = value;
	}

	/// <summary>
	/// Upper level that marks moderate bullish pressure.
	/// </summary>
	public int HighLevel1
	{
		get => _highLevel1.Value;
		set => _highLevel1.Value = value;
	}

	/// <summary>
	/// Lower level that marks moderate bearish pressure.
	/// </summary>
	public int LowLevel1
	{
		get => _lowLevel1.Value;
		set => _lowLevel1.Value = value;
	}

	/// <summary>
	/// Lower level that marks strong bearish pressure.
	/// </summary>
	public int LowLevel2
	{
		get => _lowLevel2.Value;
		set => _lowLevel2.Value = value;
	}

	/// <summary>
	/// Moving average used for indicator smoothing.
	/// </summary>
	public SmoothMethods SmoothingMethod
	{
		get => _smoothMethod.Value;
		set => _smoothMethod.Value = value;
	}

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

	/// <summary>
	/// Phase parameter for Jurik based smoothing.
	/// </summary>
	public int SmoothingPhase
	{
		get => _smoothPhase.Value;
		set => _smoothPhase.Value = value;
	}

	/// <summary>
	/// Shift applied before evaluating colour transitions.
	/// </summary>
	public int SignalBar
	{
		get => _signalBar.Value;
		set => _signalBar.Value = value;
	}

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

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

		_indicator?.Reset();
		_colorHistory.Clear();
		_lastLongPrimarySignalTime = null;
		_lastLongSecondarySignalTime = null;
		_lastShortPrimarySignalTime = null;
		_lastShortSecondarySignalTime = null;
		_isLongPrimaryOpen = false;
		_isLongSecondaryOpen = false;
		_isShortPrimaryOpen = false;
		_isShortSecondaryOpen = false;
		_indicator = null;
	}

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

		_indicator = new XBullsBearsEyesVolCalculator(
			IndicatorPeriod,
			Gamma,
			VolumeType,
			HighLevel2,
			HighLevel1,
			LowLevel1,
			LowLevel2,
			SmoothingMethod,
			SmoothingLength,
			SmoothingPhase);

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

		var step = Security?.PriceStep ?? 1m;
		var stopLoss = StopLossPoints > 0 ? new Unit(StopLossPoints * step, UnitTypes.Absolute) : null;
		var takeProfit = TakeProfitPoints > 0 ? new Unit(TakeProfitPoints * step, UnitTypes.Absolute) : null;

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

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

		if (_indicator is null)
			return;

		var result = _indicator.Process(candle);
		if (result is null)
			return;

		var signalTime = GetSignalTime(candle);
		var r = result.Value;
		AddColorSample(new ColorSample(signalTime, r.Value, r.Volume, r.Color));

		// trading guard removed

		var (currentColor, previousColor, colorTime) = GetSignalContext();
		if (currentColor is null || previousColor is null || colorTime is null)
			return;

		var openLongPrimary = false;
		var openLongSecondary = false;
		var openShortPrimary = false;
		var openShortSecondary = false;
		var closeLong = false;
		var closeShort = false;

		if (currentColor == 1)
		{
			if (AllowLongEntry && previousColor > 1)
				openLongPrimary = true;

			if (AllowShortExit)
				closeShort = true;
		}

		if (currentColor == 0)
		{
			if (AllowLongEntry && previousColor > 0)
				openLongSecondary = true;

			if (AllowShortExit)
				closeShort = true;
		}

		if (currentColor == 3)
		{
			if (AllowShortEntry && previousColor < 3)
				openShortPrimary = true;

			if (AllowLongExit)
				closeLong = true;
		}

		if (currentColor == 4)
		{
			if (AllowShortEntry && previousColor < 4)
				openShortSecondary = true;

			if (AllowLongExit)
				closeLong = true;
		}

		if (closeLong && Position > 0)
		{
			SellMarket();
			_isLongPrimaryOpen = false;
			_isLongSecondaryOpen = false;
			_lastLongPrimarySignalTime = null;
			_lastLongSecondarySignalTime = null;
		}

		if (closeShort && Position < 0)
		{
			BuyMarket();
			_isShortPrimaryOpen = false;
			_isShortSecondaryOpen = false;
			_lastShortPrimarySignalTime = null;
			_lastShortSecondarySignalTime = null;
		}

		if (openLongPrimary && !_isLongPrimaryOpen && _lastLongPrimarySignalTime != colorTime)
		{
			var volume = PrimaryVolume;
			if (volume > 0m)
			{
				BuyMarket();
				_isLongPrimaryOpen = true;
				_lastLongPrimarySignalTime = colorTime;
			}
		}

		if (openLongSecondary && !_isLongSecondaryOpen && _lastLongSecondarySignalTime != colorTime)
		{
			var volume = SecondaryVolume;
			if (volume > 0m)
			{
				BuyMarket();
				_isLongSecondaryOpen = true;
				_lastLongSecondarySignalTime = colorTime;
			}
		}

		if (openShortPrimary && !_isShortPrimaryOpen && _lastShortPrimarySignalTime != colorTime)
		{
			var volume = PrimaryVolume;
			if (volume > 0m)
			{
				SellMarket();
				_isShortPrimaryOpen = true;
				_lastShortPrimarySignalTime = colorTime;
			}
		}

		if (openShortSecondary && !_isShortSecondaryOpen && _lastShortSecondarySignalTime != colorTime)
		{
			var volume = SecondaryVolume;
			if (volume > 0m)
			{
				SellMarket();
				_isShortSecondaryOpen = true;
				_lastShortSecondarySignalTime = colorTime;
			}
		}
	}

	private DateTimeOffset GetSignalTime(ICandleMessage candle)
	{
		var timeFrame = CandleType.Arg is TimeSpan span ? span : TimeSpan.Zero;
		var closeTime = candle.CloseTime != default ? candle.CloseTime : candle.OpenTime + timeFrame;
		return closeTime;
	}

	private (int? current, int? previous, DateTimeOffset? time) GetSignalContext()
	{
		if (SignalBar < 0)
			return (null, null, null);

		var index = _colorHistory.Count - 1 - SignalBar;
		if (index < 0 || index >= _colorHistory.Count)
			return (null, null, null);

		var previousIndex = index - 1;
		if (previousIndex < 0)
			return (null, null, null);

		var currentSample = _colorHistory[index];
		var previousSample = _colorHistory[previousIndex];

		return (currentSample.Color, previousSample.Color, currentSample.Time);
	}

	private void AddColorSample(ColorSample sample)
	{
		_colorHistory.Add(sample);

		const int maxItems = 1024;
		if (_colorHistory.Count > maxItems)
			_colorHistory.RemoveRange(0, _colorHistory.Count - maxItems);
	}

	private readonly struct ColorSample
	{
		public ColorSample(DateTimeOffset time, decimal value, decimal volume, int color)
		{
			Time = time;
			Value = value;
			Volume = volume;
			Color = color;
		}

		public DateTimeOffset Time { get; }

		public decimal Value { get; }

		public decimal Volume { get; }

		public int Color { get; }
	}
	/// <summary>
	/// Volume source applied to the indicator output.
	/// </summary>
	public enum AppliedVolumes
{
	/// <summary>
	/// Multiply the indicator by tick volume.
	/// </summary>
			Tick,

	/// <summary>
	/// Multiply the indicator by real volume.
	/// </summary>
			Real,
}

	/// <summary>
	/// Moving average methods supported by the indicator.
	/// </summary>
	public enum SmoothMethods
{
	/// <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 (JJMA).
	/// </summary>
			Jjma,

	/// <summary>
	/// Jurik moving average (JurX variant).
	/// </summary>
			JurX,

	/// <summary>
	/// Parabolic moving average approximation.
	/// </summary>
			ParMa,

	/// <summary>
	/// Triple exponential moving average (T3).
	/// </summary>
			T3,

	/// <summary>
	/// VIDYA adaptive moving average (approximated by EMA).
	/// </summary>
			Vidya,

	/// <summary>
	/// Kaufman adaptive moving average.
	/// </summary>
			Ama,
}

	private sealed class XBullsBearsEyesVolCalculator
{
	private readonly ExponentialMovingAverage _ema;
	private readonly DecimalLengthIndicator _valueSmoother;
	private readonly DecimalLengthIndicator _volumeSmoother;
	private readonly AppliedVolumes _volumeType;
	private readonly decimal _gamma;
	private readonly decimal _highLevel2;
	private readonly decimal _highLevel1;
	private readonly decimal _lowLevel1;
	private readonly decimal _lowLevel2;

	private decimal _l0;
	private decimal _l1;
	private decimal _l2;
	private decimal _l3;

	public XBullsBearsEyesVolCalculator(
		int emaPeriod,
		decimal gamma,
		AppliedVolumes volumeType,
		int highLevel2,
		int highLevel1,
		int lowLevel1,
		int lowLevel2,
		SmoothMethods method,
		int smoothLength,
		int smoothPhase)
		{
			var period = Math.Max(1, emaPeriod);
			_ema = new EMA { Length = period };
			_gamma = Math.Min(0.999m, Math.Max(0m, gamma));
			_volumeType = volumeType;
			_highLevel2 = highLevel2;
			_highLevel1 = highLevel1;
			_lowLevel1 = lowLevel1;
			_lowLevel2 = lowLevel2;
			_valueSmoother = CreateSmoother(method, smoothLength, smoothPhase);
			_volumeSmoother = CreateSmoother(method, smoothLength, smoothPhase);
		}

		public void Reset()
		{
			_ema.Reset();
			_valueSmoother.Reset();
			_volumeSmoother.Reset();
			_l0 = 0m;
			_l1 = 0m;
			_l2 = 0m;
			_l3 = 0m;
		}

		public XBullsBearsEyesVolResult? Process(ICandleMessage candle)
		{
			var time = candle.CloseTime != default ? candle.CloseTime : candle.OpenTime;
			var emaValue = _ema.Process(new DecimalIndicatorValue(_ema, candle.ClosePrice, time)).ToNullableDecimal();
			if (emaValue is null)
			return null;

			var bulls = candle.HighPrice - emaValue.Value;
			var bears = candle.LowPrice - emaValue.Value;
			var combined = bulls + bears;

			var l0 = (1m - _gamma) * combined + _gamma * _l0;
			var l1 = -_gamma * l0 + _l0 + _gamma * _l1;
			var l2 = -_gamma * l1 + _l1 + _gamma * _l2;
			var l3 = -_gamma * l2 + _l2 + _gamma * _l3;

			_l0 = l0;
			_l1 = l1;
			_l2 = l2;
			_l3 = l3;

			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 sum = cu + cd;
			var ratio = sum <= 0m ? 0m : cu / sum;
			var baseValue = ratio * 100m - 50m;

			var volume = GetVolume(candle);
			var scaled = baseValue * volume;

			var smoothedValue = _valueSmoother.Process(new DecimalIndicatorValue(_valueSmoother, scaled, time)).ToNullableDecimal();
			var smoothedVolume = _volumeSmoother.Process(new DecimalIndicatorValue(_volumeSmoother, volume, time)).ToNullableDecimal();

			if (smoothedValue is null || smoothedVolume is null)
			return null;

			var color = DetermineColor(smoothedValue.Value, smoothedVolume.Value);
			return new XBullsBearsEyesVolResult(smoothedValue.Value, smoothedVolume.Value, color);
		}

		private int DetermineColor(decimal value, decimal volume)
		{
			var maxLevel = _highLevel2 * volume;
			var upLevel = _highLevel1 * volume;
			var downLevel = _lowLevel1 * volume;
			var minLevel = _lowLevel2 * volume;

			if (value > maxLevel)
			return 0;

			if (value > upLevel)
			return 1;

			if (value < minLevel)
			return 4;

			if (value < downLevel)
			return 3;

			return 2;
		}

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

		private static DecimalLengthIndicator CreateSmoother(SmoothMethods method, int length, int phase)
		{
			var normalizedLength = Math.Max(1, length);

			return method switch
			{
				SmoothMethods.Sma => new SMA { Length = normalizedLength },
				SmoothMethods.Ema => new EMA { Length = normalizedLength },
				SmoothMethods.Smma => new SmoothedMovingAverage { Length = normalizedLength },
				SmoothMethods.Lwma => new WeightedMovingAverage { Length = normalizedLength },
				SmoothMethods.Jjma => CreateJurik(normalizedLength, phase),
				SmoothMethods.JurX => CreateJurik(normalizedLength, phase),
				SmoothMethods.ParMa => new EMA { Length = normalizedLength },
				SmoothMethods.T3 => new TripleExponentialMovingAverage { Length = normalizedLength },
				SmoothMethods.Vidya => new EMA { Length = normalizedLength },
				SmoothMethods.Ama => new KaufmanAdaptiveMovingAverage { Length = normalizedLength },
				_ => new SMA { Length = normalizedLength },
			};
		}

		private static DecimalLengthIndicator CreateJurik(int length, int phase)
		{
			var jurik = new JurikMovingAverage { Length = length };
			var property = jurik.GetType().GetProperty("Phase", BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic);
			if (property != null)
			{
				var value = Math.Max(-100, Math.Min(100, phase));
				property.SetValue(jurik, value);
			}

			return jurik;
		}
	}

	private readonly record struct XBullsBearsEyesVolResult(decimal Value, decimal Volume, int Color);
}