Auf GitHub ansehen

EMA Cross-2-Strategie

Überblick

Diese Strategie ist ein StockSharp-Port des MetaTrader 4 Expert Advisors "EMA_CROSS_2" aus dem MQL-Repository. Das Original EA überwacht zwei exponentielle gleitende Durchschnitte (EMAs) und platziert Marktaufträge, wann immer die Durchschnittswerte ausgetauscht werden. Die Konvertierung behält den konträren Charakter des Skripts bei – es kauft, wenn der Long-EMA über den Short-EMA steigt, und verkauft, wenn der Short-EMA über den Long-EMA steigt – und bindet gleichzeitig die Logik in die übergeordnete StockSharp-Strategieinfrastruktur ein.

Die Strategie arbeitet mit abgeschlossenen Kerzen, die vom konfigurierbaren Kerzendatentyp bereitgestellt werden. Signale werden bei Kerzenschluss ausgewertet, um wiederholte Auslöser innerhalb desselben Balkens zu vermeiden. Das Risikomanagement ahmt das MetaTrader-Verhalten nach, indem es Take-Profit-, Stop-Loss- und Trailing-Stop-Distanzen verwendet, die in Brokerpunkten (Preisschritten) ausgedrückt werden.

Handelslogik

  1. Indikatorberechnung
    • Berechnen Sie die kurz- und langfristigen EMAs für jede abgeschlossene Kerze.
    • Überspringen Sie die erste Indikatoraktualisierung und passen Sie sie an das ursprüngliche Flag first_time an, das die allererste Auswertung ignoriert hat.
    • Erkennen Sie anschließend eine Richtungsänderung, wenn die relative Reihenfolge zwischen dem langen und dem kurzen EMA umkehrt.
  2. Signalinterpretation
    • Wenn sich der Long-Kurs EMA über den Short-Kurs EMA bewegt, eröffnet der ursprüngliche EA einen Kaufhandel. Der StockSharp-Port behält diese konträre Regel bei, obwohl er sich im Gegensatz zu einem klassischen Crossover-System verhält.
    • Wenn der Short-Kurs EMA über dem Long-Kurs EMA schließt, eröffnet die Strategie einen Verkaufshandel.
    • Neue Positionen sind nur zulässig, wenn derzeit kein Engagement offen ist, wodurch die Bedingung OrdersTotal() < 1 reproduziert wird.
  3. Auftragsausführung
    • Trades werden als Market Orders mit einem fest konfigurierbaren Volumen versendet.
    • Beim Einstieg zeichnet die Strategie Stop-Loss- und Take-Profit-Preise unter Verwendung der durch Parameter bereitgestellten Pip-Distanz auf.
  4. Risikomanagement
    • Bei jeder abgeschlossenen Kerze prüft die Strategie, ob die Preisbewegung die gespeicherten Stop-Loss- oder Take-Profit-Niveaus berührt. Bei Überschreitung eines dieser Level wird die gesamte Position mit einer Marktorder geschlossen.
    • Ein Trailing-Stop (ebenfalls in Broker-Punkten definiert) wird angewendet, sobald sich der Preis um mehr als die Trailing-Distanz günstig bewegt. Bei langen Positionen wird der Schutzanschlag nach oben verschoben; Bei Short-Positionen sinkt der Preis.
    • Wenn die Position flach wird, werden die gespeicherten Schutzniveaus gelöscht.

Parameter

Name Beschreibung Standard
CandleType Kerzenserien zur Indikatorberechnung und Signalerkennung. 15-minütiger Zeitrahmen
OrderVolume Volumen jeder Marktorder in Lots/Kontrakten. 2
TakeProfitPoints Abstand zum Take-Profit-Niveau, ausgedrückt in Brokerpunkten (Preisschritten). Ein Wert von 0 deaktiviert den Take-Profit. 20
StopLossPoints Abstand zum Stop-Loss-Level, ausgedrückt in Broker-Punkten. Ein Wert von 0 deaktiviert den Stop-Loss. 30
TrailingStopPoints Distanz, die beim Verfolgen der offenen Position verwendet wird. 0 deaktiviert den Trailing Stop. 50
ShortEmaPeriod Länge des schnellen EMA. 5
LongEmaPeriod Länge des langsamen EMA. 60

Implementierungshinweise

  • Die Strategie verwendet SubscribeCandles().Bind(shortEma, longEma, ProcessCandle), um Kerzendaten mit EMA-Indikatoren zu verbinden und folgt dabei dem bevorzugten übergeordneten API-Muster.
  • Indikatorwerte werden im Bindungsrückruf als gebrauchsfertige Dezimalzahlen empfangen, sodass keine manuelle Pufferindizierung erforderlich ist.
  • Schutzabstände werden durch Multiplikation mit dem Instrument PriceStep von MetaTrader Punkten in StockSharp Preise umgerechnet. Wenn das Instrument gebrochene Pip-Preise (3 oder 5 Dezimalstellen) verwendet, berechnet der Helfer die Pip-Größe entsprechend.
  • Stop-Loss-, Take-Profit- und Trailing-Verhalten werden intern bei Marktausstiegen implementiert, da StockSharp nicht den gleichen OrderModify-Workflow wie MetaTrader 4 bereitstellt. Das resultierende Handelsmanagement spiegelt die ursprüngliche Logik wider: Niveaus werden bei jeder Kerze überprüft und Ausstiege erfolgen sofort, sobald sie durchbrochen werden.
  • Die erste Crossover-Auswertung wird absichtlich ignoriert, um die first_time-Schutzmaßnahme zu reproduzieren, die vorzeitige Trades im MQL-Skript verhinderte.

Unterschiede zur MetaTrader-Version

  • Geldmanagement: Das ursprüngliche EA handelte immer mit dem Parameter Lots. Die Konvertierung macht dasselbe Konzept über OrderVolume verfügbar und weist es außerdem der Strategieeigenschaft Volume zu, damit Designer und Optimierer es wiederverwenden können.
  • Auftragserteilung: MetaTrader hat Stop-Loss und Take-Profit direkt innerhalb von OrderSend angewendet. In StockSharp werden diese Niveaus von der Strategie verfolgt und bei Überschreitung mit Marktaufträgen geschlossen.
  • Trailing-Stop-Präzision: Die EA verschobenen Stopps mithilfe von Tick-Daten (Bid/Ask). Der Port aktualisiert die nachgestellte Logik beim Schließen der Kerze. Dies ist die feinste Granularität, die in diesem Beispielprojekt verfügbar ist. Die Abstands- und Aktivierungsregeln bleiben identisch.
  • Fehlerbehandlung und Protokollierung wurden vereinfacht; Die StockSharp-Protokollierung liefert detaillierte Informationen über das Standardstrategieprotokoll.

Nutzungstipps

  • Passen Sie CandleType an den Zeitrahmen an, der bei Backtests des ursprünglichen EA verwendet wurde, um ein vergleichbares Indikatorverhalten aufrechtzuerhalten.
  • Stellen Sie beim Handel mit Symbolen, die mit gebrochenen Pips notiert sind, sicher, dass die konfigurierten Punktabstände die gewünschte Anzahl von Pips widerspiegeln (z. B. entsprechen bei EURUSD 10 Punkte einem Pip).
  • Stellen Sie OrderVolume auf die von Ihrem Ausführungsplatz erwartete Kontraktgröße ein. Die Strategie führt keine automatische Volumenskalierung durch.
  • Verwenden Sie die integrierten Optimierungsschalter für jeden Parameter, um Kombinationen von EMA Zeiträumen und Risikoentfernungen zu erkunden, genau wie Sie Eingaben in MetaTrader optimieren würden.

Dateien

  • CS/EmaCross2Strategy.cs – StockSharp Implementierung der Handelslogik.
  • README.md – Englische Dokumentation (diese Datei).
  • README_zh.md – Chinesische Übersetzung.
  • README_ru.md – Russische Übersetzung.
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>
/// Counter-trend EMA crossover strategy converted from the MetaTrader 4 expert "EMA_CROSS_2".
/// Buys when the long EMA rises above the short EMA, and sells when the short EMA climbs above the long EMA.
/// Incorporates MetaTrader-style risk management with point-based stop-loss, take-profit, and trailing stop levels.
/// </summary>
public class EmaCross2Strategy : Strategy
{
	private readonly StrategyParam<DataType> _candleType;
	private readonly StrategyParam<decimal> _orderVolume;
	private readonly StrategyParam<decimal> _takeProfitPoints;
	private readonly StrategyParam<decimal> _stopLossPoints;
	private readonly StrategyParam<decimal> _trailingStopPoints;
	private readonly StrategyParam<int> _shortEmaPeriod;
	private readonly StrategyParam<int> _longEmaPeriod;

	private ExponentialMovingAverage _shortEma;
	private ExponentialMovingAverage _longEma;

	private bool _skipFirstSignal = true;
	private int _lastDirection;
	private decimal? _stopLossPrice;
	private decimal? _takeProfitPrice;
	private decimal _pointSize;
	private decimal _entryPrice;

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

	/// <summary>
	/// Order volume applied to new market orders.
	/// </summary>
	public decimal OrderVolume
	{
		get => _orderVolume.Value;
		set => _orderVolume.Value = value;
	}

	/// <summary>
	/// Take-profit distance expressed in broker points.
	/// </summary>
	public decimal TakeProfitPoints
	{
		get => _takeProfitPoints.Value;
		set => _takeProfitPoints.Value = value;
	}

	/// <summary>
	/// Stop-loss distance expressed in broker points.
	/// </summary>
	public decimal StopLossPoints
	{
		get => _stopLossPoints.Value;
		set => _stopLossPoints.Value = value;
	}

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

	/// <summary>
	/// Period of the short EMA.
	/// </summary>
	public int ShortEmaPeriod
	{
		get => _shortEmaPeriod.Value;
		set => _shortEmaPeriod.Value = value;
	}

	/// <summary>
	/// Period of the long EMA.
	/// </summary>
	public int LongEmaPeriod
	{
		get => _longEmaPeriod.Value;
		set => _longEmaPeriod.Value = value;
	}

	/// <summary>
	/// Initialize strategy parameters.
	/// </summary>
	public EmaCross2Strategy()
	{
		_candleType = Param(nameof(CandleType), TimeSpan.FromMinutes(15).TimeFrame())
		.SetDisplay("Candle Type", "Time frame used for EMA calculations", "General");

		_orderVolume = Param(nameof(OrderVolume), 2m)
		.SetGreaterThanZero()
		.SetDisplay("Order Volume", "Volume of each market order", "Trading")
		
		.SetOptimize(0.1m, 5m, 0.1m);

		_takeProfitPoints = Param(nameof(TakeProfitPoints), 500m)
		.SetNotNegative()
		.SetDisplay("Take Profit (points)", "Distance from entry to take-profit in broker points", "Risk")
		
		.SetOptimize(0m, 200m, 5m);

		_stopLossPoints = Param(nameof(StopLossPoints), 500m)
		.SetNotNegative()
		.SetDisplay("Stop Loss (points)", "Distance from entry to stop-loss in broker points", "Risk")
		
		.SetOptimize(0m, 200m, 5m);

		_trailingStopPoints = Param(nameof(TrailingStopPoints), 500m)
		.SetNotNegative()
		.SetDisplay("Trailing Stop (points)", "Trailing distance maintained after entry", "Risk")
		
		.SetOptimize(0m, 200m, 5m);

		_shortEmaPeriod = Param(nameof(ShortEmaPeriod), 5)
		.SetGreaterThanZero()
		.SetDisplay("Short EMA", "Length of the fast EMA", "Indicators")
		
		.SetOptimize(2, 40, 1);

		_longEmaPeriod = Param(nameof(LongEmaPeriod), 60)
		.SetGreaterThanZero()
		.SetDisplay("Long EMA", "Length of the slow EMA", "Indicators")
		
		.SetOptimize(10, 200, 5);
	}

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

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

		Volume = OrderVolume;
		_shortEma = null;
		_longEma = null;
		_skipFirstSignal = true;
		_lastDirection = 0;
		_stopLossPrice = null;
		_takeProfitPrice = null;
		_pointSize = 0m;
		_entryPrice = 0m;
	}

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

		Volume = OrderVolume;
		_pointSize = CalculatePointSize();

		_shortEma = new EMA { Length = ShortEmaPeriod };
		_longEma = new EMA { Length = LongEmaPeriod };

		var subscription = SubscribeCandles(CandleType);
		subscription
		.Bind(_shortEma, _longEma, ProcessCandle)
		.Start();

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

	private void ProcessCandle(ICandleMessage candle, decimal shortEmaValue, decimal longEmaValue)
	{
		// Work only with finished candles to avoid repeated signals inside the same bar.
		if (candle.State != CandleStates.Finished)
		return;

		if (_pointSize <= 0m)
		_pointSize = CalculatePointSize();

		if (CheckRisk(candle))
		return;

		if (Position != 0)
		UpdateTrailingStop(candle);
		else if (_stopLossPrice.HasValue || _takeProfitPrice.HasValue)
		ResetRiskLevels();

		var signal = EvaluateCross(longEmaValue, shortEmaValue);

		if (signal == 0)
		return;

		if (!IsFormedAndOnlineAndAllowTrading())
		return;

		if (Position != 0)
		return;

		var volume = OrderVolume;
		if (volume <= 0m)
		volume = 1m;

		if (signal == 1)
		{
			BuyMarket(volume);
			SetRiskLevels(candle.ClosePrice, true);
		}
		else if (signal == 2)
		{
			SellMarket(volume);
			SetRiskLevels(candle.ClosePrice, false);
		}
	}

	private int EvaluateCross(decimal longValue, decimal shortValue)
	{
		var currentDirection = 0;

		if (longValue > shortValue)
		currentDirection = 1;
		else if (longValue < shortValue)
		currentDirection = 2;

		if (_skipFirstSignal)
		{
			_skipFirstSignal = false;
			return 0;
		}

		if (currentDirection != 0 && currentDirection != _lastDirection)
		{
			_lastDirection = currentDirection;
			return _lastDirection;
		}

		return 0;
	}

	private bool CheckRisk(ICandleMessage candle)
	{
		if (Position > 0)
		{
			var size = Math.Abs(Position);

			if (_stopLossPrice.HasValue && candle.LowPrice <= _stopLossPrice.Value)
			{
				SellMarket(size);
				ResetRiskLevels();
				return true;
			}

			if (_takeProfitPrice.HasValue && candle.HighPrice >= _takeProfitPrice.Value)
			{
				SellMarket(size);
				ResetRiskLevels();
				return true;
			}
		}
		else if (Position < 0)
		{
			var size = Math.Abs(Position);

			if (_stopLossPrice.HasValue && candle.HighPrice >= _stopLossPrice.Value)
			{
				BuyMarket(size);
				ResetRiskLevels();
				return true;
			}

			if (_takeProfitPrice.HasValue && candle.LowPrice <= _takeProfitPrice.Value)
			{
				BuyMarket(size);
				ResetRiskLevels();
				return true;
			}
		}
		else if (_stopLossPrice.HasValue || _takeProfitPrice.HasValue)
		{
			ResetRiskLevels();
		}

		return false;
	}

	private void UpdateTrailingStop(ICandleMessage candle)
	{
		if (TrailingStopPoints <= 0m || _pointSize <= 0m)
		return;

		var distance = TrailingStopPoints * _pointSize;
		if (distance <= 0m)
		return;

		var entryPrice = _entryPrice > 0 ? _entryPrice : candle.ClosePrice;

		if (Position > 0)
		{
			var profit = candle.ClosePrice - entryPrice;
			if (profit > distance)
			{
				var candidate = candle.ClosePrice - distance;
				if (!_stopLossPrice.HasValue || _stopLossPrice.Value < candidate)
				_stopLossPrice = candidate;
			}
		}
		else if (Position < 0)
		{
			var profit = entryPrice - candle.ClosePrice;
			if (profit > distance)
			{
				var candidate = candle.ClosePrice + distance;
				if (!_stopLossPrice.HasValue || _stopLossPrice.Value > candidate)
				_stopLossPrice = candidate;
			}
		}
	}

	private void SetRiskLevels(decimal executionPrice, bool isLong)
	{
		if (_pointSize <= 0m)
		{
			ResetRiskLevels();
			return;
		}

		_stopLossPrice = StopLossPoints > 0m
		? executionPrice + (isLong ? -1m : 1m) * StopLossPoints * _pointSize
		: null;

		_takeProfitPrice = TakeProfitPoints > 0m
		? executionPrice + (isLong ? 1m : -1m) * TakeProfitPoints * _pointSize
		: null;
	}

	private void ResetRiskLevels()
	{
		_stopLossPrice = null;
		_takeProfitPrice = null;
	}

	/// <inheritdoc />
	protected override void OnOwnTradeReceived(MyTrade trade)
	{
		base.OnOwnTradeReceived(trade);

		if (Position != 0 && _entryPrice == 0m)
			_entryPrice = trade.Trade.Price;

		if (Position == 0m)
			_entryPrice = 0m;
	}

	private decimal CalculatePointSize()
	{
		var step = Security?.PriceStep ?? 0m;
		return step > 0m ? step : 1m;
	}
}