Ver en GitHub

Estrategia BSS de Separación Triple EMA

Descripción General

La Estrategia BSS de Separación Triple EMA es un puerto de StockSharp del asesor experto de MetaTrader 5 "BSS 1_0" (MQL ID 20591). El enfoque monitorea tres medias móviles con ventanas de búsqueda crecientes y espera a que se expandan al menos por una distancia configurable. Cuando las medias rápida, media y lenta están correctamente separadas, la estrategia entra en la dirección de la tendencia respetando un período de enfriamiento entre rellenos y un límite en el tamaño total de la posición.

Esta implementación mantiene el comportamiento central del robot original mientras expone la configuración a través de objetos StrategyParam de StockSharp. Todos los comentarios y la documentación están escritos en inglés según lo solicitado.

Lógica de Trading

  1. Suscribirse a un único flujo de velas definido por el parámetro CandleType y calcular tres medias móviles (rápida, media, lenta). Cada media puede usar un método de suavizado diferente (simple, exponencial, suavizado o ponderado linealmente).
  2. Para una configuración larga deben cumplirse las siguientes condiciones en una vela terminada:
    • MA Lenta - MA Media >= MinimumDistance.
    • MA Media - MA Rápida >= MinimumDistance.
  3. Para una configuración corta se requiere la separación inversa:
    • MA Rápida - MA Media >= MinimumDistance.
    • MA Media - MA Lenta >= MinimumDistance.
  4. Antes de abrir una operación la estrategia garantiza:
    • Todos los indicadores están completamente formados y la estrategia puede operar (IsFormedAndOnlineAndAllowTrading).
    • La pausa desde la última entrada (MinimumPauseSeconds) ha transcurrido.
    • Agregar un nuevo lote no violará el límite de exposición MaxPositions.
  5. En una señal de entrada, la estrategia primero cierra cualquier posición abierta en la dirección opuesta. Solo después de la siguiente vela considera abrir una posición en la nueva dirección, reflejando el comportamiento del EA MQL original.
  6. Cuando se abre una nueva posición o se escala, se almacena el tiempo de relleno para aplicar el enfriamiento entre entradas.

No se usa stop-loss ni take-profit automáticos. La gestión de riesgo se logra a través del filtro de distancia, la pausa entre operaciones y el número máximo de lotes permitidos por dirección.

Parámetros

Parámetro Predeterminado Descripción
OrderVolume 0.1 Volumen usado para cada orden de entrada. La posición neta está limitada a OrderVolume * MaxPositions.
MaxPositions 2 Número máximo de lotes (por dirección) que pueden mantenerse simultáneamente.
MinimumDistance 0.0005 Brecha de precio mínima requerida entre medias móviles vecinas. Elija un valor apropiado para el instrumento (para un par FX de 5 dígitos, 0.0005 equivale a 5 pips).
MinimumPauseSeconds 600 Enfriamiento en segundos entre nuevas entradas. Cerrar operaciones no reinicia el temporizador; solo lo hacen las entradas.
FirstMaPeriod 5 Período de la media móvil más rápida. Debe ser estrictamente menor que SecondMaPeriod.
FirstMaMethod Exponential Método de suavizado usado para la media móvil rápida (Simple, Exponential, Smoothed, LinearWeighted).
SecondMaPeriod 25 Período de la media móvil media. Debe ser estrictamente menor que ThirdMaPeriod.
SecondMaMethod Exponential Método de suavizado usado para la media móvil media.
ThirdMaPeriod 125 Período de la media móvil lenta.
ThirdMaMethod Exponential Método de suavizado usado para la media móvil lenta.
CandleType Marco temporal de 1 minuto Fuente de datos de velas usada para los cálculos del indicador y la evaluación de señales.

Notas de Implementación

  • Se usa la API de alto nivel de StockSharp: SubscribeCandles transmite datos, y .Bind alimenta las medias móviles y el manejador de señales simultáneamente.
  • Las medias móviles se instancian al inicio de la estrategia según los métodos seleccionados. La configuración predeterminada coincide con el EA original (tres EMA exponenciales sobre precios de cierre).
  • StartProtection() se invoca para habilitar las herramientas de monitoreo de posición integradas proporcionadas por StockSharp.
  • La estrategia anula OnPositionChanged para marcar el tiempo de las entradas. Esta marca de tiempo se compara con MinimumPauseSeconds para mantener el comportamiento de enfriamiento de la versión de MetaTrader.
  • Las posiciones opuestas se aplanan antes de considerar nuevas, asegurando que la exposición neta nunca cambie de signo sin pasar primero por cero, igual que la implementación original donde todas las posiciones cortas se cerraban antes de abrir largos.

Directrices de Uso

  1. Seleccione un instrumento y asegúrese de que su tamaño de tick se refleje en el valor de MinimumDistance. Por ejemplo:
    • EURUSD (precios de 5 dígitos): 0.0005 equivale a 5 pips.
    • USDJPY (precios de 3 dígitos): 0.05 equivale a 5 pips.
  2. Ajuste los períodos y métodos de la media móvil para adaptarse al régimen de mercado que está atacando.
  3. Aumente MinimumPauseSeconds en marcos temporales más lentos para evitar el sobretrading, o disminúyalo en marcos temporales inferiores si la estructura del mercado permite entradas frecuentes.
  4. Pruebe diferentes valores de MaxPositions en combinación con el tamaño del contrato de su broker para alinear la exposición con su plan de riesgo.

Limitaciones Comparado con la Versión MQL

  • El experto de MetaTrader permitía seleccionar fuentes de precio alternativas (apertura, máximo, mínimo, etc.). El puerto de StockSharp actualmente opera solo en precios de cierre, lo que coincide con la configuración predeterminada del robot original.
  • El puerto usa un modelo de posición neta (positivo para largos, negativo para cortos). Cuando se alcanza MaxPositions, no se añaden lotes adicionales hasta que la exposición se reduzca, reproduciendo el efecto del contador de posición por elemento original.

Con estas consideraciones puede reproducir el comportamiento de la estrategia BSS original dentro del ecosistema StockSharp y extenderla con controles de riesgo o análisis adicionales según sea necesario.

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

}