View on GitHub

BSS Triple EMA Separation Strategy

Overview

The BSS Triple EMA Separation Strategy is a StockSharp port of the MetaTrader 5 expert advisor "BSS 1_0" (MQL ID 20591). The approach monitors three moving averages with increasing lookback windows and waits for them to fan out by at least a configurable distance. When the fast, medium and slow averages are properly separated, the strategy enters in the direction of the trend while respecting a cooldown between fills and a cap on the total position size.

This implementation keeps the core behaviour of the original robot while exposing the configuration through StockSharp StrategyParam objects. All comments and documentation are written in English as requested.

Trading Logic

  1. Subscribe to a single candle stream defined by the CandleType parameter and calculate three moving averages (fast, medium, slow). Each average can use a different smoothing method (simple, exponential, smoothed, or linear weighted).
  2. For a long setup the following conditions must be met on a finished candle:
    • Slow MA - Medium MA >= MinimumDistance.
    • Medium MA - Fast MA >= MinimumDistance.
  3. For a short setup the inverse separation is required:
    • Fast MA - Medium MA >= MinimumDistance.
    • Medium MA - Slow MA >= MinimumDistance.
  4. Before opening a trade the strategy ensures:
    • All indicators are fully formed and the strategy is allowed to trade (IsFormedAndOnlineAndAllowTrading).
    • The pause since the last entry (MinimumPauseSeconds) has elapsed.
    • Adding a new lot will not violate the MaxPositions exposure limit.
  5. On an entry signal the strategy first closes any open position in the opposite direction. Only after the next candle does it consider opening a position in the new direction, mirroring the behaviour of the original MQL EA.
  6. When a new position is opened or scaled in, the fill time is stored to enforce the cooldown between entries.

No automatic stop-loss or take-profit is used. Risk management is achieved through the distance filter, the pause between trades, and the maximum number of lots allowed per direction.

Parameters

Parameter Default Description
OrderVolume 0.1 Volume used for each entry order. The net position is limited to OrderVolume * MaxPositions.
MaxPositions 2 Maximum number of lots (per direction) that can be held simultaneously.
MinimumDistance 0.0005 Minimum price gap required between neighbouring moving averages. Choose a value appropriate for the instrument (for a 5-digit FX pair, 0.0005 equals 5 pips).
MinimumPauseSeconds 600 Cooldown in seconds between new entries. Closing trades does not reset the timer; only entries do.
FirstMaPeriod 5 Period of the fastest moving average. Must be strictly less than SecondMaPeriod.
FirstMaMethod Exponential Smoothing method used for the fast moving average (Simple, Exponential, Smoothed, LinearWeighted).
SecondMaPeriod 25 Period of the medium moving average. Must be strictly less than ThirdMaPeriod.
SecondMaMethod Exponential Smoothing method used for the medium moving average.
ThirdMaPeriod 125 Period of the slow moving average.
ThirdMaMethod Exponential Smoothing method used for the slow moving average.
CandleType 1-minute time frame Candle data source used for indicator calculations and signal evaluation.

Implementation Notes

  • High-level StockSharp API is used: SubscribeCandles streams data, and .Bind feeds the moving averages and the signal handler simultaneously.
  • The moving averages are instantiated on strategy start according to the selected methods. The default configuration matches the original EA (three exponential MAs on closing prices).
  • StartProtection() is invoked to enable the built-in position monitoring tools provided by StockSharp.
  • The strategy overrides OnPositionChanged to timestamp entries. This timestamp is compared against MinimumPauseSeconds to maintain the cooldown behaviour of the MetaTrader version.
  • Opposite positions are flattened before new ones are considered, ensuring that the net exposure never flips without first going through zero, just like the original implementation where all short positions were closed before opening longs.

Usage Guidelines

  1. Select an instrument and ensure its tick size is reflected in the MinimumDistance value. For example:
    • EURUSD (5-digit pricing): 0.0005 equals 5 pips.
    • USDJPY (3-digit pricing): 0.05 equals 5 pips.
  2. Adjust the moving average periods and methods to fit the market regime you are targeting.
  3. Increase MinimumPauseSeconds on slower time frames to avoid over-trading, or decrease it on lower time frames if the market structure allows frequent entries.
  4. Test different MaxPositions values in combination with your broker’s contract size to align the exposure with your risk plan.

Limitations Compared to the MQL Version

  • The MetaTrader expert allowed selecting alternative price sources (open, high, low, etc.). The StockSharp port currently operates on closing prices only, which matches the default configuration of the original robot.
  • The port uses a net-position model (positive for longs, negative for shorts). When MaxPositions is reached no additional lots are added until the exposure is reduced, reproducing the effect of the original per-position counter.

With these considerations you can reproduce the behaviour of the original BSS strategy inside the StockSharp ecosystem and extend it with additional risk controls or analytics as needed.

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;

namespace StockSharp.Samples.Strategies;

public class BssTripleEmaSeparationStrategy : Strategy
{
	public enum MaMethods
	{
		Simple,
		Exponential,
		Smoothed,
		LinearWeighted,
	}

	// Small epsilon used to compare decimal volumes without floating point noise.
	private readonly StrategyParam<decimal> _volumeTolerance;

	// User configurable parameters.
	private readonly StrategyParam<decimal> _orderVolume;
	private readonly StrategyParam<int> _maxPositions;
	private readonly StrategyParam<decimal> _minimumDistance;
	private readonly StrategyParam<int> _minimumPauseSeconds;
	private readonly StrategyParam<int> _firstMaPeriod;
	private readonly StrategyParam<int> _secondMaPeriod;
	private readonly StrategyParam<int> _thirdMaPeriod;
	private readonly StrategyParam<MaMethods> _firstMaMethod;
	private readonly StrategyParam<MaMethods> _secondMaMethod;
	private readonly StrategyParam<MaMethods> _thirdMaMethod;
	private readonly StrategyParam<DataType> _candleType;

	// Indicator instances created according to the selected parameters.
	private IIndicator _firstMa = null!;
	private IIndicator _secondMa = null!;
	private IIndicator _thirdMa = null!;

	// Timestamp of the last position entry used to enforce the pause between trades.
	private DateTimeOffset? _lastEntryTime;

	/// <summary>
	/// Tolerance used when comparing accumulated volume values.
	/// </summary>
	public decimal VolumeTolerance
	{
		get => _volumeTolerance.Value;
		set => _volumeTolerance.Value = value;
	}

	public decimal OrderVolume
	{
		get => _orderVolume.Value;
		set => _orderVolume.Value = value;
	}

	public int MaxPositions
	{
		get => _maxPositions.Value;
		set => _maxPositions.Value = value;
	}

	public decimal MinimumDistance
	{
		get => _minimumDistance.Value;
		set => _minimumDistance.Value = value;
	}

	public int MinimumPauseSeconds
	{
		get => _minimumPauseSeconds.Value;
		set => _minimumPauseSeconds.Value = value;
	}

	public int FirstMaPeriod
	{
		get => _firstMaPeriod.Value;
		set => _firstMaPeriod.Value = value;
	}

	public int SecondMaPeriod
	{
		get => _secondMaPeriod.Value;
		set => _secondMaPeriod.Value = value;
	}

	public int ThirdMaPeriod
	{
		get => _thirdMaPeriod.Value;
		set => _thirdMaPeriod.Value = value;
	}

	public MaMethods FirstMaMethod
	{
		get => _firstMaMethod.Value;
		set => _firstMaMethod.Value = value;
	}

	public MaMethods SecondMaMethod
	{
		get => _secondMaMethod.Value;
		set => _secondMaMethod.Value = value;
	}

	public MaMethods ThirdMaMethod
	{
		get => _thirdMaMethod.Value;
		set => _thirdMaMethod.Value = value;
	}

	public DataType CandleType
	{
		get => _candleType.Value;
		set => _candleType.Value = value;
	}

	public BssTripleEmaSeparationStrategy()
	{
		_volumeTolerance = Param(nameof(VolumeTolerance), 1e-8m)
			.SetGreaterThanZero()
			.SetDisplay("Volume Tolerance", "Tolerance when comparing volume values", "Risk");

		_orderVolume = Param(nameof(OrderVolume), 0.1m)
			.SetGreaterThanZero()
			.SetDisplay("Order Volume", "Volume used for each entry order", "Trading");

		_maxPositions = Param(nameof(MaxPositions), 2)
			.SetGreaterThanZero()
			.SetDisplay("Max Positions", "Maximum simultaneous entries per direction", "Risk");

		_minimumDistance = Param(nameof(MinimumDistance), 50m)
			.SetGreaterThanZero()
			.SetDisplay("Minimum Distance", "Minimum price gap between moving averages", "Signals");

		_minimumPauseSeconds = Param(nameof(MinimumPauseSeconds), 600)
			.SetNotNegative()
			.SetDisplay("Minimum Pause (sec)", "Pause between new entries in seconds", "Risk");

		_firstMaPeriod = Param(nameof(FirstMaPeriod), 5)
			.SetGreaterThanZero()
			.SetDisplay("First MA Period", "Period for the fastest moving average", "Indicators");

		_firstMaMethod = Param(nameof(FirstMaMethod), MaMethods.Exponential)
			.SetDisplay("First MA Method", "Smoothing method for the fastest moving average", "Indicators");

		_secondMaPeriod = Param(nameof(SecondMaPeriod), 25)
			.SetGreaterThanZero()
			.SetDisplay("Second MA Period", "Period for the medium moving average", "Indicators");

		_secondMaMethod = Param(nameof(SecondMaMethod), MaMethods.Exponential)
			.SetDisplay("Second MA Method", "Smoothing method for the medium moving average", "Indicators");

		_thirdMaPeriod = Param(nameof(ThirdMaPeriod), 125)
			.SetGreaterThanZero()
			.SetDisplay("Third MA Period", "Period for the slowest moving average", "Indicators");

		_thirdMaMethod = Param(nameof(ThirdMaMethod), MaMethods.Exponential)
			.SetDisplay("Third MA Method", "Smoothing method for the slowest moving average", "Indicators");

		_candleType = Param(nameof(CandleType), TimeSpan.FromHours(4).TimeFrame())
			.SetDisplay("Candle Type", "Timeframe used for calculations", "General");
	}

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

	protected override void OnReseted()
	{
		base.OnReseted();

		_lastEntryTime = null;
	}

	protected override void OnStarted2(DateTime time)
	{
		base.OnStarted2(time);

		if (FirstMaPeriod >= SecondMaPeriod)
			throw new InvalidOperationException("First MA period must be less than second MA period.");

		if (SecondMaPeriod >= ThirdMaPeriod)
			throw new InvalidOperationException("Second MA period must be less than third MA period.");

		_firstMa = CreateMovingAverage(FirstMaMethod, FirstMaPeriod);
		_secondMa = CreateMovingAverage(SecondMaMethod, SecondMaPeriod);
		_thirdMa = CreateMovingAverage(ThirdMaMethod, ThirdMaPeriod);

		_lastEntryTime = null;

		var subscription = SubscribeCandles(CandleType);
		subscription.Bind(_firstMa, _secondMa, _thirdMa, ProcessCandle).Start();

	}

	private static IIndicator CreateMovingAverage(MaMethods method, int period)
	{
		return method switch
		{
			MaMethods.Simple => new SimpleMovingAverage { Length = period },
			MaMethods.Smoothed => new SmoothedMovingAverage { Length = period },
			MaMethods.LinearWeighted => new WeightedMovingAverage { Length = period },
			_ => new ExponentialMovingAverage { Length = period },
		};
	}

	private void ProcessCandle(ICandleMessage candle, decimal firstValue, decimal secondValue, decimal thirdValue)
	{
		if (candle.State != CandleStates.Finished)
			return;

		if (!_firstMa.IsFormed || !_secondMa.IsFormed || !_thirdMa.IsFormed)
			return;

		var minDistance = MinimumDistance;

		var longSpreadOk = thirdValue - secondValue >= minDistance && secondValue - firstValue >= minDistance;
		var shortSpreadOk = firstValue - secondValue >= minDistance && secondValue - thirdValue >= minDistance;

		if (!longSpreadOk && !shortSpreadOk)
			return;

		var time = candle.OpenTime;

		if (longSpreadOk)
		{
			if (TryCloseOppositePositions(true))
				return;

			if (CanEnterPosition(time, true))
			{
				BuyMarket(OrderVolume);
				_lastEntryTime = time;
			}

			return;
		}

		if (shortSpreadOk)
		{
			if (TryCloseOppositePositions(false))
				return;

			if (CanEnterPosition(time, false))
			{
				SellMarket(OrderVolume);
				_lastEntryTime = time;
			}
		}
	}

	private bool CanEnterPosition(DateTimeOffset time, bool isLong)
	{
		// Trading is allowed only when the strategy is ready, the pause elapsed, and exposure stays within bounds.

		if (!IsPauseElapsed(time))
			return false;

		var targetPosition = Position + (isLong ? OrderVolume : -OrderVolume);
		var maxExposure = MaxPositions * OrderVolume;

		return Math.Abs(targetPosition) <= maxExposure + VolumeTolerance;
	}

	private bool IsPauseElapsed(DateTimeOffset time)
	{
		var pauseSeconds = MinimumPauseSeconds;

		if (pauseSeconds <= 0)
			return true;

		if (_lastEntryTime is null)
			return true;

		return time - _lastEntryTime.Value >= TimeSpan.FromSeconds(pauseSeconds);
	}

	private bool TryCloseOppositePositions(bool isLong)
	{
		// Close active trades in the opposite direction before opening a new position.
		if (isLong)
		{
			if (Position < -VolumeTolerance)
			{
				BuyMarket(Math.Abs(Position));
				return true;
			}
		}
		else
		{
			if (Position > VolumeTolerance)
			{
				SellMarket(Position);
				return true;
			}
		}

		return false;
	}

}