Auf GitHub ansehen

FarhadCrab1 Strategie (C#)

Überblick

Die FarhadCrab1-Strategie ist ein Trendfolge-System, das Trades auf Pullbacks zu einem exponentiellen gleitenden Durchschnitt (EMA) eingeht und Ausstiege mit festen Stops, Take-Profits, einem Parabolic SAR-inspirierten Trailing-Stop und einem höheren Zeitrahmen-Filter verwaltet. Der originale MetaTrader 5 Expert Advisor stützt sich auf stündliche Kerzen für die Ausführung, während er auf tägliche Daten verweist, um zu entscheiden, wann offene Positionen geschlossen werden sollen. Dieser C#-Port behält dieselbe Kernlogik bei, indem er einen EMA-Filter des Arbeitszeitrahmens mit einer täglichen EMA-Kreuzungs-Ausstiegsregel kombiniert.

Kernkonzepte

  • Trendfilter: Ein EMA, der auf dem Arbeitszeitrahmen berechnet wird (standardmäßig 15-Perioden-EMA auf 1-Stunden-Kerzen). Nur Long-Signale sind erlaubt, wenn das Tief der vorherigen Kerze über dem EMA bleibt, und nur Short-Signale sind erlaubt, wenn das Hoch der vorherigen Kerze unter dem EMA bleibt.
  • Tagesfilter: Ein separater EMA, der auf täglichen Kerzen berechnet wird. Wenn der tägliche EMA über den täglichen Schlusskurs kreuzt, werden alle Long-Positionen geschlossen. Wenn er darunter kreuzt, werden alle Short-Positionen geschlossen. Dies imitiert die ursprüngliche ClosePositions-Logik aus dem MQL5-Code.
  • Risikokontrollen: Feste Stop-Loss- und Take-Profit-Niveaus werden aus Pip-Abständen abgeleitet. Ein Trailing-Stop verschiebt den Schutz-Stop, sobald die Position genug Gewinn erzielt, und emuliert die MT5-Trailing-Funktion, die TrailingStop- und TrailingStep-Einstellungen kombiniert.
  • Einzelpositions-Management: Die Strategie handelt eine einzelne Nettoposition. Einen Long-Einstieg zu tätigen, während ein Short gehalten wird (oder umgekehrt), schließt zuerst die entgegengesetzte Exposure, bevor der neue Trade eröffnet wird.

Handelsregeln

  1. Signalerkennung (Arbeitszeitrahmen):
    • Long-Einstieg, wenn das Tief der vorherigen Kerze größer ist als der EMA-Wert (nach Anwendung des konfigurierten Shifts).
    • Short-Einstieg, wenn das Hoch der vorherigen Kerze kleiner ist als der EMA-Wert.
  2. Positionsgrößenbestimmung: Der Volume-Parameter setzt die Basis-Ordergröße. Beim Umkehren von Short zu Long (oder umgekehrt) sendet die Engine automatisch die zusätzlich benötigte Menge, um die Nettoposition umzukehren.
  3. Stop-Loss und Take-Profit:
    • Abstände werden in Pips definiert. Die Pip-Größe passt sich automatisch an die Tick-Größe des Instruments an, wobei FX-Symbole mit fünf und drei Stellen einen 10x-Multiplikator verwenden, um das MT5-Verhalten zu entsprechen.
    • Stop-Loss oder Take-Profit kann durch Setzen des jeweiligen Pip-Abstands auf null deaktiviert werden.
  4. Trailing-Stop:
    • Aktiviert sich nur, wenn TrailingStopPips größer als null ist.
    • Der Stop wird auf aktueller_Preis - TrailingStopPips (für Longs) oder aktueller_Preis + TrailingStopPips (für Shorts) verschoben, sobald der Positionsgewinn TrailingStopPips + TrailingStepPips überschreitet.
    • Der zusätzliche Trailing-Schritt verhindert häufige Modifikationen.
  5. Tages-Austrittsfilter:
    • Verwendet die letzten zwei abgeschlossenen Tageskerzen.
    • Long-Positionen werden geschlossen, wenn der tägliche EMA vor zwei Tagen unter dem täglichen Schlusskurs lag und am aktuellsten Tag über dem täglichen Schlusskurs liegt (bärische Kreuzung).
    • Short-Positionen werden geschlossen, wenn die entgegengesetzte Kreuzung auftritt.

Parameter

Name Typ Standard Beschreibung
CandleType DataType 1-Stunden-Zeitrahmen Arbeitszeitrahmen für den Ausführungs-EMA und die Einstiegslogik.
MaLength int 15 Periode des EMA auf dem Arbeitszeitrahmen.
MaShift int 0 Anzahl der abgeschlossenen Kerzen, die verwendet werden, um den EMA rückwärts zu verschieben.
DailyMaLength int 15 Periode des täglichen EMA, der den Kreuzungs-Austrittsfilter liefert.
StopLossPips decimal 50 Stop-Loss-Abstand in Pips. Auf 0 setzen zum Deaktivieren.
TakeProfitPips decimal 50 Take-Profit-Abstand in Pips. Auf 0 setzen zum Deaktivieren.
TrailingStopPips decimal 10 Trailing-Stop-Abstand in Pips. Auf 0 setzen zum Deaktivieren des Trailings.
TrailingStepPips decimal 5 Minimaler zusätzlicher Gewinn in Pips, bevor der Trailing-Stop wieder aktualisiert wird.
Volume decimal 0.1 Basis-Handelsgröße in Lots/Kontrakten.

Hinweise und Unterschiede zur MQL-Version

  • Dieser Port verwendet immer exponentielle gleitende Durchschnitte, entsprechend dem ursprünglichen Standard (MODE_EMA). Andere MT5-Glättungsmodi werden nicht unterstützt.
  • Der MT5 Expert Advisor arbeitet mit Geld-/Briefkursen bei jedem Tick. Diese Übersetzung operiert auf abgeschlossenen Kerzen, sodass Stop-Loss- und Take-Profit-Prüfungen auf Kerzenhochs/-tiefs ausgewertet werden.
  • Der Parabolic SAR-Indikator, der in der Originaldatei vorhanden war, beeinflusste keine Handelsentscheidungen und ist daher aus der C#-Implementierung ausgelassen.
  • Die Trailing-Logik passt das gespeicherte Stop-Niveau an, sendet aber keine Broker-Stop-Orders. Der Ausstieg erfolgt, wenn der Kerzenbereich das berechnete Stop- oder Take-Profit-Niveau berührt.

Verwendungstipps

  • Einen Kerzentyp wählen, der dem gewünschten Handelshorizont entspricht. Die standardmäßigen Ein-Stunden-Kerzen replizieren das Verhalten des Quellskripts.
  • MaLength und DailyMaLength zusammen anpassen, um die Reaktionsfähigkeit zwischen Intraday-Einstiegen und höheren Zeitrahmen-Trendfiltern abzustimmen.
  • Für FX-Symbole, die mit fünf Stellen notiert werden (z.B. EURUSD), werden Pip-Abstände automatisch skaliert, sodass 1 Pip 0.0001 entspricht.
  • Beim Backtesting sicherstellen, dass der tägliche Datenstrom verfügbar ist, damit der Austrittsfilter korrekt funktionieren kann.
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>
/// Trend-following strategy converted from the FarhadCrab1 MT5 expert advisor.
/// The strategy enters on pullbacks to an EMA, manages risk with pip-based levels,
/// and closes positions based on a daily EMA crossover filter.
/// </summary>
public class FarhadCrab1Strategy : Strategy
{
	private readonly StrategyParam<DataType> _candleType;
	private readonly StrategyParam<int> _maLength;
	private readonly StrategyParam<int> _maShift;
	private readonly StrategyParam<int> _dailyMaLength;
	private readonly StrategyParam<decimal> _stopLossPips;
	private readonly StrategyParam<decimal> _takeProfitPips;
	private readonly StrategyParam<decimal> _trailingStopPips;
	private readonly StrategyParam<decimal> _trailingStepPips;
	private readonly StrategyParam<decimal> _orderVolume;

	private readonly Queue<decimal> _maValues = new();

	private readonly DataType _dailyCandleType = TimeSpan.FromHours(1).TimeFrame();

	private decimal? _stopLossPrice;
	private decimal? _takeProfitPrice;

	private decimal? _prevDailyClose;
	private decimal? _prevDailyMa;
	private decimal? _prevPrevDailyClose;
	private decimal? _prevPrevDailyMa;

	private ICandleMessage _previousCandle;
	private decimal _entryPrice;

	/// <summary>
	/// Working candle type for the execution timeframe.
	/// </summary>
	public DataType CandleType
	{
		get => _candleType.Value;
		set => _candleType.Value = value;
	}

	/// <summary>
	/// Period of the EMA used on the working timeframe.
	/// </summary>
	public int MaLength
	{
		get => _maLength.Value;
		set => _maLength.Value = value;
	}

	/// <summary>
	/// Number of completed candles to shift the EMA backwards.
	/// </summary>
	public int MaShift
	{
		get => _maShift.Value;
		set => _maShift.Value = value;
	}

	/// <summary>
	/// Period of the daily EMA that closes positions on crossovers.
	/// </summary>
	public int DailyMaLength
	{
		get => _dailyMaLength.Value;
		set => _dailyMaLength.Value = value;
	}

	/// <summary>
	/// Stop-loss distance expressed in pips.
	/// </summary>
	public decimal StopLossPips
	{
		get => _stopLossPips.Value;
		set => _stopLossPips.Value = value;
	}

	/// <summary>
	/// Take-profit distance expressed in pips.
	/// </summary>
	public decimal TakeProfitPips
	{
		get => _takeProfitPips.Value;
		set => _takeProfitPips.Value = value;
	}

	/// <summary>
	/// Trailing stop distance in pips.
	/// </summary>
	public decimal TrailingStopPips
	{
		get => _trailingStopPips.Value;
		set => _trailingStopPips.Value = value;
	}

	/// <summary>
	/// Additional pip distance required before updating the trailing stop again.
	/// </summary>
	public decimal TrailingStepPips
	{
		get => _trailingStepPips.Value;
		set => _trailingStepPips.Value = value;
	}

	/// <summary>
	/// Base order volume in lots/contracts.
	/// </summary>
	public decimal OrderVolume
	{
		get => _orderVolume.Value;
		set => _orderVolume.Value = value;
	}

	/// <summary>
	/// Initializes a new instance of the <see cref="FarhadCrab1Strategy"/> class.
	/// </summary>
	public FarhadCrab1Strategy()
	{
		_candleType = Param(nameof(CandleType), TimeSpan.FromHours(4).TimeFrame())
			.SetDisplay("Candle Type", "Execution timeframe", "General");

		_maLength = Param(nameof(MaLength), 15)
			.SetGreaterThanZero()
			.SetDisplay("EMA Length", "EMA period on the working timeframe", "Indicators");

		_maShift = Param(nameof(MaShift), 0)
			.SetRange(0, 100)
			.SetDisplay("EMA Shift", "Shift EMA value backwards by N candles", "Indicators");

		_dailyMaLength = Param(nameof(DailyMaLength), 15)
			.SetGreaterThanZero()
			.SetDisplay("Daily EMA Length", "EMA period used on daily candles", "Indicators");

		_stopLossPips = Param(nameof(StopLossPips), 50m)
			.SetRange(0m, 500m)
			.SetDisplay("Stop Loss (pips)", "Stop-loss distance in pips", "Protection");

		_takeProfitPips = Param(nameof(TakeProfitPips), 50m)
			.SetRange(0m, 500m)
			.SetDisplay("Take Profit (pips)", "Take-profit distance in pips", "Protection");

		_trailingStopPips = Param(nameof(TrailingStopPips), 10m)
			.SetRange(0m, 500m)
			.SetDisplay("Trailing Stop (pips)", "Trailing stop distance in pips", "Protection");

		_trailingStepPips = Param(nameof(TrailingStepPips), 5m)
			.SetRange(0m, 500m)
			.SetDisplay("Trailing Step (pips)", "Extra gain in pips before updating the trailing stop", "Protection");

		_orderVolume = Param(nameof(OrderVolume), 0.1m)
			.SetGreaterThanZero()
			.SetDisplay("Volume", "Base order volume", "Trading");
	}

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

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

		_maValues.Clear();
		_stopLossPrice = null;
		_takeProfitPrice = null;
		_prevDailyClose = null;
		_prevDailyMa = null;
		_prevPrevDailyClose = null;
		_prevPrevDailyMa = null;
		_previousCandle = null;
		_entryPrice = 0m;
	}

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

		// Ensure the base strategy volume reflects the configured parameter.
		base.Volume = OrderVolume;

		// Subscribe to the working timeframe candles with an EMA for entry decisions.
		var ema = new ExponentialMovingAverage { Length = MaLength };
		var candleSubscription = SubscribeCandles(CandleType);
		candleSubscription
			.Bind(ema, ProcessWorkingCandle)
			.Start();

		// Subscribe to daily candles with another EMA for exit filtering.
		var dailyEma = new ExponentialMovingAverage { Length = DailyMaLength };
		var dailySubscription = SubscribeCandles(_dailyCandleType);
		dailySubscription
			.Bind(dailyEma, ProcessDailyCandle)
			.Start();

		// Draw candles, indicator, and trades on the chart if charting is available.
		var area = CreateChartArea();
		if (area != null)
		{
			DrawCandles(area, candleSubscription);
			DrawOwnTrades(area);
			DrawIndicator(area, ema);
		}
	}

	private void ProcessDailyCandle(ICandleMessage candle, decimal emaValue)
	{
		// Process only finished daily candles.
		if (candle.State != CandleStates.Finished)
			return;

		_prevPrevDailyClose = _prevDailyClose;
		_prevPrevDailyMa = _prevDailyMa;
		_prevDailyClose = candle.ClosePrice;
		_prevDailyMa = emaValue;
	}

	private void ProcessWorkingCandle(ICandleMessage candle, decimal emaValue)
	{
		// Use only completed candles for decision making.
		if (candle.State != CandleStates.Finished)
			return;

		// Store EMA values so we can apply the configured shift.
		UpdateMaBuffer(emaValue);
		var shiftedMa = GetShiftedMaValue();
		if (shiftedMa == null)
		{
			_previousCandle = candle;
			return;
		}

		// Require at least one previous candle to evaluate entry conditions.
		if (_previousCandle == null)
		{
			_previousCandle = candle;
			return;
		}

		// Close positions when the daily EMA filter signals a crossover against us.
		if (TryCloseByDailyFilter())
		{
			_previousCandle = candle;
			return;
		}

		var pipSize = GetPipSize();

		// Check for stop-loss or take-profit triggers before adjusting trailing stops.
		if (CheckStopsAndTargets(candle))
		{
			_previousCandle = candle;
			return;
		}

		// Update trailing stop levels if the position has moved far enough.
		ApplyTrailingStop(candle, pipSize);

		// Evaluate long entry condition: previous low above the EMA.
		if (Position <= 0 && _previousCandle.LowPrice > shiftedMa.Value)
		{
			var volume = OrderVolume + (Position < 0 ? -Position : 0m);
			if (volume > 0)
			{
				BuyMarket(volume);
				SetRiskLevels(candle.ClosePrice, pipSize, true);
			}
			_previousCandle = candle;
			return;
		}

		// Evaluate short entry condition: previous high below the EMA.
		if (Position >= 0 && _previousCandle.HighPrice < shiftedMa.Value)
		{
			var volume = OrderVolume + (Position > 0 ? Position : 0m);
			if (volume > 0)
			{
				SellMarket(volume);
				SetRiskLevels(candle.ClosePrice, pipSize, false);
			}
			_previousCandle = candle;
			return;
		}

		// Store the current candle for the next iteration.
		_previousCandle = candle;
	}

	private bool TryCloseByDailyFilter()
	{
		if (_prevDailyClose == null || _prevDailyMa == null || _prevPrevDailyClose == null || _prevPrevDailyMa == null)
			return false;

		var prevClose = _prevDailyClose.Value;
		var prevMa = _prevDailyMa.Value;
		var prev2Close = _prevPrevDailyClose.Value;
		var prev2Ma = _prevPrevDailyMa.Value;

		// Bearish crossover: EMA moved above the daily close -> exit long positions.
		if (Position > 0 && prevMa > prevClose && prev2Ma < prev2Close)
		{
			SellMarket(Position);
			ResetRiskLevels();
			return true;
		}

		// Bullish crossover: EMA moved below the daily close -> exit short positions.
		if (Position < 0 && prevMa < prevClose && prev2Ma > prev2Close)
		{
			BuyMarket(-Position);
			ResetRiskLevels();
			return true;
		}

		return false;
	}

	private bool CheckStopsAndTargets(ICandleMessage candle)
	{
		if (Position > 0)
		{
			if (_stopLossPrice.HasValue && candle.LowPrice <= _stopLossPrice.Value)
			{
				SellMarket(Position);
				ResetRiskLevels();
				return true;
			}

			if (_takeProfitPrice.HasValue && candle.HighPrice >= _takeProfitPrice.Value)
			{
				SellMarket(Position);
				ResetRiskLevels();
				return true;
			}
		}
		else if (Position < 0)
		{
			if (_stopLossPrice.HasValue && candle.HighPrice >= _stopLossPrice.Value)
			{
				BuyMarket(-Position);
				ResetRiskLevels();
				return true;
			}

			if (_takeProfitPrice.HasValue && candle.LowPrice <= _takeProfitPrice.Value)
			{
				BuyMarket(-Position);
				ResetRiskLevels();
				return true;
			}
		}
		else if (_stopLossPrice.HasValue || _takeProfitPrice.HasValue)
		{
			// Clear stored levels when the position is flat.
			ResetRiskLevels();
		}

		return false;
	}

	private void ApplyTrailingStop(ICandleMessage candle, decimal pipSize)
	{
		if (TrailingStopPips <= 0m)
			return;

		var entryPrice = _entryPrice;
		var threshold = (TrailingStopPips + TrailingStepPips) * pipSize;

		if (Position > 0)
		{
			var profit = candle.ClosePrice - entryPrice;
			if (profit > threshold)
			{
				var minStop = candle.ClosePrice - threshold;
				var candidate = candle.ClosePrice - TrailingStopPips * pipSize;
				if (!_stopLossPrice.HasValue || _stopLossPrice.Value < minStop)
					_stopLossPrice = candidate;
			}
		}
		else if (Position < 0)
		{
			var profit = entryPrice - candle.ClosePrice;
			if (profit > threshold)
			{
				var maxStop = candle.ClosePrice + threshold;
				var candidate = candle.ClosePrice + TrailingStopPips * pipSize;
				if (!_stopLossPrice.HasValue || _stopLossPrice.Value > maxStop)
					_stopLossPrice = candidate;
			}
		}
	}

	private void SetRiskLevels(decimal executionPrice, decimal pipSize, bool isLong)
	{
		_entryPrice = executionPrice;
		if (StopLossPips > 0m && pipSize > 0m)
			_stopLossPrice = isLong
				? executionPrice - StopLossPips * pipSize
				: executionPrice + StopLossPips * pipSize;
		else
			_stopLossPrice = null;

		if (TakeProfitPips > 0m && pipSize > 0m)
			_takeProfitPrice = isLong
				? executionPrice + TakeProfitPips * pipSize
				: executionPrice - TakeProfitPips * pipSize;
		else
			_takeProfitPrice = null;
	}

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

	private decimal GetPipSize()
	{
		var security = Security;
		if (security == null)
			return 0.0001m;

		var step = security.PriceStep ?? 0.0001m;
		var decimals = security.Decimals;

		if (decimals == 3 || decimals == 5)
			return step * 10m;

		return step;
	}

	private void UpdateMaBuffer(decimal emaValue)
	{
		_maValues.Enqueue(emaValue);

		var maxCount = Math.Max(MaShift + 1, 1);
		while (_maValues.Count > maxCount)
			_maValues.Dequeue();
	}

	private decimal? GetShiftedMaValue()
	{
		var count = _maValues.Count;
		var targetIndex = count - MaShift - 1;
		if (targetIndex < 0)
			return null;

		var index = 0;
		foreach (var value in _maValues)
		{
			if (index == targetIndex)
				return value;
			index++;
		}

		return null;
	}
}