Ver no GitHub

Exp XBullsBearsEyes Vol Direct Strategy

Overview

This strategy is a C# conversion of the MetaTrader expert Exp_XBullsBearsEyes_Vol_Direct. It recreates the custom oscillator built from Bulls Power and Bears Power, multiplies it by a configurable volume source and applies adaptive smoothing identical to the original indicator. Trading decisions are driven exclusively by the direction buffer of the indicator: the algorithm reacts to momentum swings rather than level crossings, opening or closing positions when the smoothed histogram changes slope.

Unlike many conversions, the StockSharp version keeps the volume weighting stage and the four-level gamma filter used by the MQL code. The oscillator is smoothed twice with the same moving average method—one pass for the histogram itself and one for the volume stream—so signals appear only when both components are fully formed. The strategy processes finished candles only and supports tick volume or real traded volume, making it portable across different markets.

Indicator logic

  1. Compute Bulls Power and Bears Power with an exponential moving average of the closing price over Period candles.
  2. Apply the original four-stage gamma filter (parameters Gamma, L0L3) to combine the two forces into a normalized histogram ranging from -50 to +50.
  3. Multiply the histogram by the selected volume source (tick count or traded volume).
  4. Smooth the histogram and the raw volume with the same moving average family (Method, SmoothingLength, SmoothingPhase).
  5. Derive a direction buffer: color 0 when the smoothed histogram rises, color 1 when it falls. This mimics the ColorDirectBuffer from the MetaTrader implementation.

The higher/lower threshold buffers from the indicator are calculated internally but are not used for trade filters, matching the behaviour of the original expert advisor that relied only on direction flips.

Trading rules

  • Close shorts when the previous bar’s direction was bullish (olderColor = 0).
  • Open longs if long entries are allowed, a bullish bar is followed by a bearish one (currentColor = 1), and the strategy is not already long.
  • Close longs when the previous bar’s direction was bearish (olderColor = 1).
  • Open shorts if short entries are allowed, a bearish bar is followed by a bullish one (currentColor = 0), and no long position is active.
  • Position reversals close the opposite side first and then submit a market order with the configured OrderVolume.

Signals are evaluated with a configurable bar shift (SignalBar). The default value of 1 emulates the MQL expert that waited for a fully closed candle before reacting to the direction change.

Parameters

Name Description
CandleType Candle type/timeframe subscribed by the strategy (default: 2-hour candles).
Period Lookback period used for Bulls/Bears Power.
Gamma Smoothing factor (0…1) of the adaptive gamma filter.
VolumeMode Volume source: tick count or traded volume.
Method Moving average family used to smooth histogram and volume (SMA, EMA, SMMA, LWMA, Jurik; unsupported legacy types fall back to SMA).
SmoothingLength Length of both smoothing stages.
SmoothingPhase Jurik phase parameter (kept for compatibility).
SignalBar Number of bars back to read when evaluating the direction buffer.
AllowBuyOpen / AllowSellOpen Enable or disable opening long/short positions.
AllowBuyClose / AllowSellClose Enable or disable forced exits on opposite signals.
OrderVolume Market order size used for new entries.
StopLossPoints Optional protective stop in price steps (0 disables the stop).
TakeProfitPoints Optional protective target in price steps (0 disables the target).

Usage notes

  • The strategy operates on a single security returned by GetWorkingSecurities() and works best on symbols that provide a stable volume stream.
  • Tick volume is recommended for spot FX symbols where real traded volume is unavailable. Set VolumeMode to Real for exchanges that publish executed volume.
  • Stop-loss and take-profit distances are expressed in price steps and converted into absolute price units using the security’s PriceStep.
  • Because the logic relies on direction flips, consecutive equal histogram values keep the previous direction until a new slope appears, exactly like the MetaTrader version.
  • Chart output plots only price candles by default. You can add custom plotting for the histogram if visual confirmation is required.
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
	}
}