Ver no GitHub

Estratégia BSS de Separação Triple EMA

Visão Geral

A Estratégia BSS de Separação Triple EMA é uma portagem do StockSharp do expert advisor do MetaTrader 5 "BSS 1_0" (MQL ID 20591). A abordagem monitora três médias móveis com janelas de lookback crescentes e aguarda que elas se expandam por pelo menos uma distância configurável. Quando as médias rápida, média e lenta estão devidamente separadas, a estratégia entra na direção da tendência respeitando um período de espera entre preenchimentos e um limite no tamanho total da posição.

Esta implementação mantém o comportamento central do robô original enquanto expõe a configuração através de objetos StrategyParam do StockSharp. Todos os comentários e documentação são escritos em inglês conforme solicitado.

Lógica de Trading

  1. Assinar um único fluxo de velas definido pelo parâmetro CandleType e calcular três médias móveis (rápida, média, lenta). Cada média pode usar um método de suavização diferente (simples, exponencial, suavizado ou linearmente ponderado).
  2. Para uma configuração comprada as seguintes condições devem ser atendidas em uma vela terminada:
    • MA Lenta - MA Média >= MinimumDistance.
    • MA Média - MA Rápida >= MinimumDistance.
  3. Para uma configuração vendida a separação inversa é necessária:
    • MA Rápida - MA Média >= MinimumDistance.
    • MA Média - MA Lenta >= MinimumDistance.
  4. Antes de abrir uma operação a estratégia garante:
    • Todos os indicadores estão totalmente formados e a estratégia tem permissão para negociar (IsFormedAndOnlineAndAllowTrading).
    • A pausa desde a última entrada (MinimumPauseSeconds) decorreu.
    • Adicionar um novo lote não violará o limite de exposição MaxPositions.
  5. Em um sinal de entrada, a estratégia primeiro fecha qualquer posição aberta na direção oposta. Somente após a próxima vela ela considera abrir uma posição na nova direção, refletindo o comportamento do EA MQL original.
  6. Quando uma nova posição é aberta ou escalada, o tempo de preenchimento é armazenado para impor o período de espera entre entradas.

Nenhum stop-loss ou take-profit automático é usado. O gerenciamento de risco é alcançado através do filtro de distância, a pausa entre operações e o número máximo de lotes permitidos por direção.

Parâmetros

Parâmetro Padrão Descrição
OrderVolume 0.1 Volume usado para cada ordem de entrada. A posição líquida está limitada a OrderVolume * MaxPositions.
MaxPositions 2 Número máximo de lotes (por direção) que podem ser mantidos simultaneamente.
MinimumDistance 0.0005 Lacuna de preço mínima necessária entre médias móveis vizinhas. Escolha um valor apropriado para o instrumento (para um par FX de 5 dígitos, 0.0005 equivale a 5 pips).
MinimumPauseSeconds 600 Período de espera em segundos entre novas entradas. Fechar operações não reinicia o temporizador; apenas entradas o fazem.
FirstMaPeriod 5 Período da média móvel mais rápida. Deve ser estritamente menor que SecondMaPeriod.
FirstMaMethod Exponential Método de suavização usado para a média móvel rápida (Simple, Exponential, Smoothed, LinearWeighted).
SecondMaPeriod 25 Período da média móvel média. Deve ser estritamente menor que ThirdMaPeriod.
SecondMaMethod Exponential Método de suavização usado para a média móvel média.
ThirdMaPeriod 125 Período da média móvel lenta.
ThirdMaMethod Exponential Método de suavização usado para a média móvel lenta.
CandleType Período de 1 minuto Fonte de dados de velas usada para cálculos de indicadores e avaliação de sinais.

Notas de Implementação

  • A API de alto nível do StockSharp é usada: SubscribeCandles transmite dados, e .Bind alimenta as médias móveis e o manipulador de sinais simultaneamente.
  • As médias móveis são instanciadas no início da estratégia de acordo com os métodos selecionados. A configuração padrão corresponde ao EA original (três MAs exponenciais sobre preços de fechamento).
  • StartProtection() é invocado para habilitar as ferramentas de monitoramento de posição integradas fornecidas pelo StockSharp.
  • A estratégia substitui OnPositionChanged para registrar o tempo das entradas. Esse timestamp é comparado com MinimumPauseSeconds para manter o comportamento de período de espera da versão MetaTrader.
  • Posições opostas são niveladas antes que novas sejam consideradas, garantindo que a exposição líquida nunca mude de sinal sem primeiro passar por zero, assim como a implementação original onde todas as posições vendidas eram fechadas antes de abrir posições compradas.

Diretrizes de Uso

  1. Selecione um instrumento e garanta que seu tamanho de tick seja refletido no valor MinimumDistance. Por exemplo:
    • EURUSD (preços de 5 dígitos): 0.0005 equivale a 5 pips.
    • USDJPY (preços de 3 dígitos): 0.05 equivale a 5 pips.
  2. Ajuste os períodos e métodos da média móvel para se adequar ao regime de mercado que você está atacando.
  3. Aumente MinimumPauseSeconds em períodos mais lentos para evitar overtrading, ou diminua em períodos mais baixos se a estrutura do mercado permitir entradas frequentes.
  4. Teste diferentes valores de MaxPositions em combinação com o tamanho do contrato do seu broker para alinhar a exposição ao seu plano de risco.

Limitações Comparado com a Versão MQL

  • O expert MetaTrader permitia selecionar fontes de preço alternativas (abertura, máxima, mínima, etc.). A portagem do StockSharp atualmente opera apenas em preços de fechamento, o que corresponde à configuração padrão do robô original.
  • A portagem usa um modelo de posição líquida (positivo para comprados, negativo para vendidos). Quando MaxPositions é atingido, nenhum lote adicional é adicionado até que a exposição seja reduzida, reproduzindo o efeito do contador de posição por elemento original.

Com essas considerações você pode reproduzir o comportamento da estratégia BSS original dentro do ecossistema StockSharp e estendê-la com controles de risco adicionais ou análises conforme necessário.

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;
	}

}