Auf GitHub ansehen

EMA Crossover Trailing-Strategie

Übersicht

Diese Strategie ist ein StockSharp-Port des MQL5 Expert Advisors "Intersection 2 iMA". Sie operiert mit zwei exponentiellen gleitenden Durchschnitten (EMAs) und reagiert auf Kreuzungen, die auf vollständig gebildeten Kerzen auftreten. Der ursprüngliche Experte wurde für MetaTrader 5 entwickelt und verwaltete das Handelsvolumen dynamisch; in dieser Konvertierung wird die Ordergröße durch einen konfigurierbaren Parameter gesteuert, während die Kreuzungs- und Trailing-Logik erhalten bleibt.

Handelslogik

  1. Signalgenerierung
    • Schnelle und langsame EMAs auf der ausgewählten Kerzenserie berechnen.
    • Eine bullische Kreuzung (schnelle EMA kreuzt über langsame EMA) löst ein Kaufsignal aus, wenn die vorherige Kerze mit der schnellen EMA unter oder gleich der langsamen EMA schloss und die aktuellen Werte die schnelle EMA über der langsamen zeigen.
    • Eine bärische Kreuzung (schnelle EMA kreuzt unter langsame EMA) spiegelt die obige Regel und produziert ein Verkaufssignal.
  2. Orderausführung
    • Wenn ein Kaufsignal erzeugt wird und keine Long-Position besteht, sendet die Strategie eine Market-Kauforder.
    • Wenn ein Verkaufssignal erzeugt wird und keine Short-Position besteht, sendet die Strategie eine Market-Verkaufsorder.
    • Wenn eine Gegenposition besteht, wird das Ordervolumen erhöht, um die bestehende Position zu schließen, bevor die neue eröffnet wird, was dem Verhalten des Quell-EA entspricht, der zuerst Gegentrades schloß.
  3. Trailing-Stop-Management
    • Ein gestufter Trailing-Stop hält einen festen Abstand (in Preisschritten) vom günstigsten Preis.
    • Der Stop bewegt sich nur, wenn der Preis um einen benutzerdefinierten Schritt vorangeschritten ist, was ständige Orderänderungen verhindert.
    • Wenn der Preis das Trailing-Niveau verletzt, wird die Position mit einer Market-Order geschlossen.

Parameter

Name Beschreibung Standard
FastPeriod Länge der schnellen EMA. 4
SlowPeriod Länge der langsamen EMA. 18
TrailingStopPoints Abstand zwischen Marktpreis und Trailing-Stop in Preisschritten (Punkten). Ein Wert von 0 deaktiviert Trailing. 20
TrailingStepPoints Minimaler Fortschritt in Preisschritten, bevor der Trailing-Stop vorgeschoben wird. 5
CandleType Kerzendatenserie für Berechnungen (Zeitrahmen). 15-Minuten-Kerzen
TradeVolume Ordergröße für Market-Einstiege. 1

Implementierungshinweise

  • Die Strategie verwendet die High-Level-API SubscribeCandles().Bind(...), um Kerzendaten mit EMA-Indikatoren zu verbinden und sicherzustellen, dass keine manuelle Bufferverwaltung erforderlich ist.
  • Trailing-Abstände werden berechnet, indem die konfigurierte Punktanzahl mit dem PriceStep des Instruments multipliziert wird, was die Ziffernanpassungslogik aus der MQL-Version repliziert.
  • Trailing-Stops werden intern über Market-Exits implementiert, da StockSharp nicht den gleichen PositionModify-Helper wie MetaTrader bereitstellt. Das Verhalten bleibt äquivalent: sobald das Trailing-Niveau verletzt wird, wird die Position sofort beendet.
  • Parameter werden über StrategyParam<T> bereitgestellt, sodass sie im Designer optimiert oder über die Benutzeroberfläche angepasst werden können.

Verwendungstipps

  • Den CandleType auf den Zeitrahmen ausrichten, der in Backtests oder im Live-Handel verwendet wird, um Indikatorwerte konsistent zu halten.
  • Beim Handel mit Instrumenten mit kleinen Tick-Größen TrailingStopPoints und TrailingStepPoints entsprechend anpassen; der effektive Preisabstand entspricht Punkte × PriceStep.
  • TradeVolume auf die gewünschte Kontrakt- oder Lotgröße einstellen. Die Strategie erhöht den Orderbetrag automatisch, um eine Gegenposition zu schließen, wenn ein neues Signal erscheint.

Unterschiede zum ursprünglichen Expert Advisor

  • Money-Management in MetaTrader verwendete MoneyFixedMargin; die StockSharp-Version stellt stattdessen einen festen Ordervolumen-Parameter bereit und überlässt das erweiterte Positionssizing der externen Konfiguration.
  • Der EA bot einen unbenutzten InpCloseHalf-Eingabewert an. Er hatte keine Auswirkung auf den Quellcode und wurde weggelassen.
  • Stop-Trailing wird intern statt durch Modifikation von Stop-Loss-Orders gehandhabt, da dies die Ausführung innerhalb von StockSharp vereinfacht und die Ausstiegslogik identisch hält.
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;

/// <summary>
/// EMA crossover strategy with trailing stop logic converted from the MQL5 "Intersection 2 iMA" expert advisor.
/// The strategy opens trades on moving average crossovers and maintains a stepped trailing stop.
/// </summary>
public class EmaCrossoverTrailingStrategy : Strategy
{
	private readonly StrategyParam<int> _fastPeriod;
	private readonly StrategyParam<int> _slowPeriod;
	private readonly StrategyParam<decimal> _trailingStopPoints;
	private readonly StrategyParam<decimal> _trailingStepPoints;
	private readonly StrategyParam<DataType> _candleType;
	private readonly StrategyParam<decimal> _tradeVolume;

	private ExponentialMovingAverage _fastEma = null!;
	private ExponentialMovingAverage _slowEma = null!;

	private decimal? _previousFastValue;
	private decimal? _previousSlowValue;

	private decimal? _longStopPrice;
	private decimal? _shortStopPrice;

	private decimal _stopDistance;
	private decimal _stepDistance;

	/// <summary>
	/// Initializes <see cref="EmaCrossoverTrailingStrategy"/>.
	/// </summary>
	public EmaCrossoverTrailingStrategy()
	{
		_fastPeriod = Param(nameof(FastPeriod), 4)
			.SetGreaterThanZero()
			.SetDisplay("Fast EMA", "Period of the fast exponential moving average", "Moving Averages")
			
			.SetOptimize(2, 20, 1);

		_slowPeriod = Param(nameof(SlowPeriod), 18)
			.SetGreaterThanZero()
			.SetDisplay("Slow EMA", "Period of the slow exponential moving average", "Moving Averages")
			
			.SetOptimize(10, 60, 2);

		_trailingStopPoints = Param(nameof(TrailingStopPoints), 20m)
			.SetNotNegative()
			.SetDisplay("Trailing Stop (points)", "Distance from price to trailing stop expressed in price steps", "Risk Management")
			
			.SetOptimize(5m, 40m, 5m);

		_trailingStepPoints = Param(nameof(TrailingStepPoints), 5m)
			.SetNotNegative()
			.SetDisplay("Trailing Step (points)", "Minimum price advancement before the trailing stop is moved", "Risk Management")
			
			.SetOptimize(1m, 10m, 1m);

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

		_tradeVolume = Param(nameof(TradeVolume), 1m)
			.SetGreaterThanZero()
			.SetDisplay("Volume", "Order volume used for entries", "General");
	}

	/// <summary>
	/// Fast EMA period.
	/// </summary>
	public int FastPeriod
	{
		get => _fastPeriod.Value;
		set => _fastPeriod.Value = value;
	}

	/// <summary>
	/// Slow EMA period.
	/// </summary>
	public int SlowPeriod
	{
		get => _slowPeriod.Value;
		set => _slowPeriod.Value = value;
	}

	/// <summary>
	/// Trailing stop distance expressed in price steps.
	/// </summary>
	public decimal TrailingStopPoints
	{
		get => _trailingStopPoints.Value;
		set => _trailingStopPoints.Value = value;
	}

	/// <summary>
	/// Minimum move required before shifting the trailing stop.
	/// </summary>
	public decimal TrailingStepPoints
	{
		get => _trailingStepPoints.Value;
		set => _trailingStepPoints.Value = value;
	}

	/// <summary>
	/// Candle type used for analysis.
	/// </summary>
	public DataType CandleType
	{
		get => _candleType.Value;
		set => _candleType.Value = value;
	}

	/// <summary>
	/// Volume used for market orders.
	/// </summary>
	public decimal TradeVolume
	{
		get => _tradeVolume.Value;
		set => _tradeVolume.Value = value;
	}

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

	/// <inheritdoc />
	protected override void OnReseted()
	{
		base.OnReseted();

		Volume = TradeVolume;
		_previousFastValue = null;
		_previousSlowValue = null;
		_longStopPrice = null;
		_shortStopPrice = null;
		_fastEma = null!;
		_slowEma = null!;
		_stopDistance = 0m;
		_stepDistance = 0m;
	}

	/// <inheritdoc />
	protected override void OnStarted2(DateTime time)
	{
		base.OnStarted2(time);

		Volume = TradeVolume;

		_fastEma = new EMA { Length = FastPeriod };
		_slowEma = new EMA { Length = SlowPeriod };

		_stopDistance = CalculateDistance(TrailingStopPoints);
		_stepDistance = CalculateDistance(TrailingStepPoints);

		var subscription = SubscribeCandles(CandleType);
		subscription
			.Bind(_fastEma, _slowEma, ProcessCandle)
			.Start();

		var area = CreateChartArea();
		if (area != null)
		{
			DrawCandles(area, subscription);
			DrawIndicator(area, _fastEma);
			DrawIndicator(area, _slowEma);
			DrawOwnTrades(area);
		}
	}

	private void ProcessCandle(ICandleMessage candle, decimal fastValue, decimal slowValue)
	{
		if (candle.State != CandleStates.Finished)
			return;

		_stopDistance = CalculateDistance(TrailingStopPoints);
		_stepDistance = CalculateDistance(TrailingStepPoints);

		UpdateTrailingStops(candle);

		if (!_fastEma.IsFormed || !_slowEma.IsFormed)
		{
			_previousFastValue = fastValue;
			_previousSlowValue = slowValue;
			return;
		}

		// indicators bound via .Bind()

		if (_previousFastValue is null || _previousSlowValue is null)
		{
			_previousFastValue = fastValue;
			_previousSlowValue = slowValue;
			return;
		}

		var fastPrev = _previousFastValue.Value;
		var slowPrev = _previousSlowValue.Value;

		var crossedUp = fastPrev <= slowPrev && fastValue > slowValue;
		var crossedDown = fastPrev >= slowPrev && fastValue < slowValue;

		if (crossedUp && Position <= 0)
		{
			var volumeToBuy = TradeVolume;

			if (Position < 0)
				volumeToBuy += Math.Abs(Position);

			if (volumeToBuy > 0)
			{
				BuyMarket();
				InitializeLongTrailing(candle.ClosePrice);
			}
		}
		else if (crossedDown && Position >= 0)
		{
			var volumeToSell = TradeVolume;

			if (Position > 0)
				volumeToSell += Math.Abs(Position);

			if (volumeToSell > 0)
			{
				SellMarket();
				InitializeShortTrailing(candle.ClosePrice);
			}
		}

		_previousFastValue = fastValue;
		_previousSlowValue = slowValue;
	}

	private decimal CalculateDistance(decimal points)
	{
		if (points <= 0m)
			return 0m;

		var priceStep = Security?.PriceStep ?? 0m;

		if (priceStep <= 0m)
			priceStep = 1m;

		return points * priceStep;
	}

	private void InitializeLongTrailing(decimal price)
	{
		if (_stopDistance <= 0m)
		{
			_longStopPrice = null;
			return;
		}

		_longStopPrice = price - _stopDistance;
		_shortStopPrice = null;
	}

	private void InitializeShortTrailing(decimal price)
	{
		if (_stopDistance <= 0m)
		{
			_shortStopPrice = null;
			return;
		}

		_shortStopPrice = price + _stopDistance;
		_longStopPrice = null;
	}

	private void UpdateTrailingStops(ICandleMessage candle)
	{
		if (_stopDistance <= 0m)
		{
			_longStopPrice = null;
			_shortStopPrice = null;
			return;
		}

		if (Position > 0)
		{
			if (_longStopPrice is null)
			{
				InitializeLongTrailing(candle.ClosePrice);
			}
			else
			{
				var newStop = candle.ClosePrice - _stopDistance;

				if (newStop - _longStopPrice.Value >= _stepDistance)
					_longStopPrice = newStop;

				if (candle.LowPrice <= _longStopPrice.Value)
				{
					SellMarket();
					_longStopPrice = null;
				}
			}
		}
		else if (Position < 0)
		{
			if (_shortStopPrice is null)
			{
				InitializeShortTrailing(candle.ClosePrice);
			}
			else
			{
				var newStop = candle.ClosePrice + _stopDistance;

				if (_shortStopPrice.Value - newStop >= _stepDistance)
					_shortStopPrice = newStop;

				if (candle.HighPrice >= _shortStopPrice.Value)
				{
					BuyMarket();
					_shortStopPrice = null;
				}
			}
		}
		else
		{
			_longStopPrice = null;
			_shortStopPrice = null;
		}
	}
}