Auf GitHub ansehen

BSS Triple EMA-Separations-Strategie

Übersicht

Die BSS Triple EMA-Separations-Strategie ist ein StockSharp-Port des MetaTrader 5-Expert Advisors "BSS 1_0" (MQL ID 20591). Der Ansatz überwacht drei gleitende Durchschnitte mit wachsenden Rückblickfenstern und wartet darauf, dass sie sich um mindestens eine konfigurierbare Distanz ausbreiten. Wenn die schnellen, mittleren und langsamen Durchschnitte ordnungsgemäß separiert sind, steigt die Strategie in Trendrichtung ein, wobei eine Abklingzeit zwischen Fills und eine Obergrenze für die Gesamtpositionsgröße eingehalten werden.

Diese Implementierung behält das Kernverhalten des ursprünglichen Roboters bei und exponiert die Konfiguration über StockSharp-StrategyParam-Objekte. Alle Kommentare und die Dokumentation sind auf Englisch geschrieben, wie angefordert.

Handelslogik

  1. Ein einzelner Kerzenstrom abonniert, der durch den CandleType-Parameter definiert ist, und drei gleitende Durchschnitte berechnen (schnell, mittel, langsam). Jeder Durchschnitt kann eine andere Glättungsmethode verwenden (einfach, exponentiell, geglättet oder linear gewichtet).
  2. Für ein Long-Setup müssen folgende Bedingungen auf einer abgeschlossenen Kerze erfüllt sein:
    • Langsamer MA - Mittlerer MA >= MinimumDistance.
    • Mittlerer MA - Schneller MA >= MinimumDistance.
  3. Für ein Short-Setup ist die inverse Separation erforderlich:
    • Schneller MA - Mittlerer MA >= MinimumDistance.
    • Mittlerer MA - Langsamer MA >= MinimumDistance.
  4. Vor dem Öffnen eines Trades stellt die Strategie sicher:
    • Alle Indikatoren sind vollständig gebildet und die Strategie kann handeln (IsFormedAndOnlineAndAllowTrading).
    • Die Pause seit dem letzten Einstieg (MinimumPauseSeconds) ist abgelaufen.
    • Das Hinzufügen eines neuen Lots verletzt nicht das Expositionslimit MaxPositions.
  5. Bei einem Einstiegssignal schließt die Strategie zunächst jede offene Position in der entgegengesetzten Richtung. Erst nach der nächsten Kerze erwägt sie das Öffnen einer Position in der neuen Richtung, was das Verhalten des ursprünglichen MQL-EA widerspiegelt.
  6. Wenn eine neue Position eröffnet oder skaliert wird, wird die Fill-Zeit gespeichert, um die Abklingzeit zwischen Einstiegen zu erzwingen.

Es werden keine automatischen Stop-Loss- oder Take-Profit-Level verwendet. Das Risikomanagement wird durch den Distanzfilter, die Pause zwischen Trades und die maximale Anzahl erlaubter Lots pro Richtung erreicht.

Parameter

Parameter Standard Beschreibung
OrderVolume 0.1 Volumen für jede Einstiegsorder. Die Nettoposition ist auf OrderVolume * MaxPositions begrenzt.
MaxPositions 2 Maximale Anzahl von Lots (pro Richtung), die gleichzeitig gehalten werden können.
MinimumDistance 0.0005 Minimale Preislücke zwischen benachbarten gleitenden Durchschnitten. Wählen Sie einen für das Instrument geeigneten Wert (für ein 5-stelliges FX-Paar entsprechen 0,0005 5 Pips).
MinimumPauseSeconds 600 Abklingzeit in Sekunden zwischen neuen Einstiegen. Das Schließen von Trades setzt den Timer nicht zurück; nur Einstiege tun es.
FirstMaPeriod 5 Periode des schnellsten gleitenden Durchschnitts. Muss strikt kleiner als SecondMaPeriod sein.
FirstMaMethod Exponential Für den schnellen gleitenden Durchschnitt verwendete Glättungsmethode (Simple, Exponential, Smoothed, LinearWeighted).
SecondMaPeriod 25 Periode des mittleren gleitenden Durchschnitts. Muss strikt kleiner als ThirdMaPeriod sein.
SecondMaMethod Exponential Für den mittleren gleitenden Durchschnitt verwendete Glättungsmethode.
ThirdMaPeriod 125 Periode des langsamen gleitenden Durchschnitts.
ThirdMaMethod Exponential Für den langsamen gleitenden Durchschnitt verwendete Glättungsmethode.
CandleType 1-Minuten-Zeitrahmen Kerzendatenquelle für Indikatorberechnungen und Signalbewertung.

Implementierungshinweise

  • Die StockSharp-High-Level-API wird verwendet: SubscribeCandles streamt Daten, und .Bind speist die gleitenden Durchschnitte und den Signalhandler gleichzeitig.
  • Die gleitenden Durchschnitte werden beim Strategiestart entsprechend den ausgewählten Methoden instanziiert. Die Standardkonfiguration entspricht dem ursprünglichen EA (drei exponentielle MAs auf Schlusskursen).
  • StartProtection() wird aufgerufen, um die von StockSharp bereitgestellten integrierten Positionsüberwachungstools zu aktivieren.
  • Die Strategie überschreibt OnPositionChanged, um Einstiege mit Zeitstempeln zu versehen. Dieser Zeitstempel wird mit MinimumPauseSeconds verglichen, um das Abklingverhalten der MetaTrader-Version beizubehalten.
  • Entgegengesetzte Positionen werden ausgeglichen, bevor neue berücksichtigt werden, was sicherstellt, dass die Nettoexposition nie ohne vorherigen Durchgang durch null das Vorzeichen wechselt, genau wie bei der ursprünglichen Implementierung, bei der alle Short-Positionen geschlossen wurden, bevor Longs eröffnet wurden.

Nutzungsrichtlinien

  1. Wählen Sie ein Instrument und stellen Sie sicher, dass seine Tick-Größe im MinimumDistance-Wert widergespiegelt ist. Beispielsweise:
    • EURUSD (5-stellige Preisgestaltung): 0,0005 entspricht 5 Pips.
    • USDJPY (3-stellige Preisgestaltung): 0,05 entspricht 5 Pips.
  2. Passen Sie die gleitenden Durchschnittsperioden und -methoden an das Marktregime an, das Sie ansprechen möchten.
  3. Erhöhen Sie MinimumPauseSeconds bei langsameren Zeitrahmen, um Überhandel zu vermeiden, oder verringern Sie es bei niedrigeren Zeitrahmen, wenn die Marktstruktur häufige Einstiege erlaubt.
  4. Testen Sie verschiedene MaxPositions-Werte in Kombination mit der Kontraktgröße Ihres Brokers, um die Exposition mit Ihrem Risikoplan abzustimmen.

Einschränkungen im Vergleich zur MQL-Version

  • Der MetaTrader-Experte erlaubte die Auswahl alternativer Preisquellen (Eröffnung, Hoch, Tief usw.). Der StockSharp-Port arbeitet derzeit nur auf Schlusskursen, was der Standardkonfiguration des ursprünglichen Roboters entspricht.
  • Der Port verwendet ein Nettopositoinsmodell (positiv für Longs, negativ für Shorts). Wenn MaxPositions erreicht ist, werden keine zusätzlichen Lots hinzugefügt, bis die Exposition reduziert wird, was die Wirkung des ursprünglichen Pro-Position-Zählers reproduziert.

Mit diesen Überlegungen können Sie das Verhalten der ursprünglichen BSS-Strategie innerhalb des StockSharp-Ökosystems reproduzieren und sie bei Bedarf mit zusätzlichen Risikokontrollen oder Analysen erweitern.

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

}