Auf GitHub ansehen

Gazonkos Expertenstrategie

Überblick

Diese Strategie ist eine StockSharp-Portierung des MetaTrader 4-Expertenberaters „gazonkos expert“, der für den EUR/USD-H1-Chart entwickelt wurde. Der EA wartet auf eine starke einstündige Momentumbewegung und bewegt sich dann nach einem konfigurierbaren Pullback in die Richtung dieser Bewegung. Schützende Stop-Loss- und Take-Profit-Level werden als feste Distanzen, gemessen in Pips, angewendet.

Ursprüngliche MQL4-Logik

  • Der EA überwacht kontinuierlich die Differenz zwischen zwei historischen Schlusskursen (Close[t2] - Close[t1]). Die Standardwerte sind t1 = 3 und t2 = 2, die den Schlusskursen der Kerzen entsprechen, die vor zwei und drei Stunden endeten.
  • Ein bullischer Impuls wird erkannt, wenn Close[t2] - Close[t1] delta Punkte überschreitet. Ein rückläufiger Impuls wird erkannt, wenn Close[t1] - Close[t2] denselben Schwellenwert überschreitet.
  • Sobald ein Impuls erkannt wird, zeichnet EA das höchste (für bullische) oder niedrigste (für bärische) Gebot auf, das vor Beginn der nächsten Stunde erfolgt. Wenn der Preis innerhalb derselben Stunde um Otkat Punkte von diesem Extremwert zurückgeht, wird eine Marktorder in Impulsrichtung gesendet.
  • Trades werden blockiert, wenn bereits eine offene Position mit derselben magischen Zahl besteht oder wenn in der aktuellen Stunde bereits ein Trade eröffnet wurde.
  • Jede Order wird mit einem festen Take-Profit- (TakeProfit) und Stop-Loss-Abstand (StopLoss) gesendet, ausgedrückt in Punkten.

Zustandsmaschine in der C#-Version

Die StockSharp-Implementierung erstellt die ursprüngliche Zustandsmaschine neu:

  1. WaitingForSlot – überprüft, ob in der aktuellen Stunde kein aktueller Trade eröffnet wurde und dass die konfigurierte maximale Anzahl gleichzeitiger Trades nicht erreicht wurde.
  2. WaitingForImpulse – überprüft die historischen Schlusskurse, um bullische oder bärische Impulse zu erkennen.
  3. MonitoringRetracement – verfolgt die Kerzenhochs/-tiefs nach dem Impuls und wartet auf einen Pullback von RetracementPips (der frühere Otkat-Parameter) innerhalb derselben Stunde.
  4. AwaitingExecution – sendet einen Marktauftrag in Impulsrichtung und wendet sofort schützende Stop-Loss- und Take-Profit-Level an, die aus dem Instrument PriceStep berechnet werden.

Die Strategie verarbeitet nur fertige Kerzen aus dem konfigurierten Zeitrahmen und ignoriert unvollendete Daten. Sie spiegelt wider, wie die ursprüngliche EA die Bedingungen für geschlossene stündliche Balken ausgewertet hat.

Parameter

Parameter Beschreibung
TakeProfitPips Abstand zwischen dem Einstiegspreis und dem Take-Profit-Level.
RetracementPips Erforderlicher Rückzug vom Impulsextrem vor dem Einstieg.
StopLossPips Abstand zwischen dem Einstiegspreis und dem Schutzstopp.
T1Shift Index des älteren Referenzschlusses, der für die Impulserkennung verwendet wird (Standard 3).
T2Shift Index des neueren Referenzschlusses, der für die Impulserkennung verwendet wird (Standard 2).
DeltaPips Minimaler Impulsabstand, der die beiden Referenzschlüsse trennen muss.
LotSize Festes Volumen jeder Bestellung.
MaxActiveTrades Maximale Anzahl gleichzeitiger Trades; Werte über eins erfordern, dass das Brokerkonto additive Nettopositionen unterstützt.
CandleType Zeitrahmen der Kerzen, die zur Bewertung der Handelsregeln verwendet werden (Standard ist 1 Stunde).

Alle Pip-basierten Distanzen werden mit Security.PriceStep in Preisoffsets umgewandelt. Wenn das Instrument keine Preisschrittinformationen hat, wird ein Standardwert von 0,0001 verwendet, der der ursprünglichen EUR/USD-Konfiguration entspricht.

Hinweise zur Implementierung

  • Die Strategie funktioniert mit dem High-Level-Kerzenabonnement API (SubscribeCandles().Bind) von StockSharp.
  • Geschlossene Preise werden in einem kompakten Rollpuffer zwischengespeichert, um Close[i]-Suchvorgänge aus der MQL4-Version zu emulieren.
  • Nachdem ein Handel ausgeführt wurde, zeichnet die Strategie die Kerzenstunde auf und blockiert neue Einträge bis zur nächsten Stunde, wodurch die ursprüngliche LastTradeTime-Absicherung reproduziert wird.
  • MaxActiveTrades wird gegen die aktuelle Nettoposition interpretiert. Bei Netting-Konten wird dadurch das System effektiv auf einen einzelnen offenen Handel beschränkt, was dem Standardverhalten des MQL4-Experten entspricht.
  • Kommentare im Code beschreiben die C#-Zustandsmaschine zur einfacheren Wartung auf Englisch.
namespace StockSharp.Samples.Strategies;

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;

/// <summary>
/// Momentum pullback strategy converted from the MetaTrader 4 "gazonkos expert" EA.
/// </summary>
public class GazonkosExpertStrategy : Strategy
{
	private enum TradeStates
	{
		WaitingForSlot,
		WaitingForImpulse,
		MonitoringRetracement,
		AwaitingExecution,
	}

	private readonly StrategyParam<decimal> _takeProfitPips;
	private readonly StrategyParam<decimal> _retracementPips;
	private readonly StrategyParam<decimal> _stopLossPips;
	private readonly StrategyParam<int> _t1Shift;
	private readonly StrategyParam<int> _t2Shift;
	private readonly StrategyParam<decimal> _deltaPips;
	private readonly StrategyParam<decimal> _lotSize;
	private readonly StrategyParam<int> _maxActiveTrades;
	private readonly StrategyParam<DataType> _candleType;

	private readonly List<decimal> _closeHistory = new();

	private TradeStates _state = TradeStates.WaitingForSlot;
	private Sides? _pendingDirection;
	private decimal _extremePrice;
	private int? _lastTradeHour;
	private int? _lastSignalHour;
	private decimal _pointValue;

	/// <summary>
	/// Initializes a new instance of <see cref="GazonkosExpertStrategy"/>.
	/// </summary>
	public GazonkosExpertStrategy()
	{
		_takeProfitPips = Param(nameof(TakeProfitPips), 16m)
			.SetDisplay("Take Profit (pips)", "Distance between entry and the take profit level", "Risk")
			.SetGreaterThanZero()
			;

		_retracementPips = Param(nameof(RetracementPips), 16m)
			.SetDisplay("Retracement (pips)", "Pullback distance that confirms the entry", "Signals")
			.SetGreaterThanZero()
			;

		_stopLossPips = Param(nameof(StopLossPips), 40m)
			.SetDisplay("Stop Loss (pips)", "Distance between entry and the protective stop", "Risk")
			.SetGreaterThanZero()
			;

		_t1Shift = Param(nameof(T1Shift), 3)
			.SetDisplay("T1 Shift", "Index of the older reference close used for momentum detection", "Signals")
			.SetGreaterThanZero()
			;

		_t2Shift = Param(nameof(T2Shift), 2)
			.SetDisplay("T2 Shift", "Index of the newer reference close used for momentum detection", "Signals")
			.SetGreaterThanZero()
			;

		_deltaPips = Param(nameof(DeltaPips), 40m)
			.SetDisplay("Delta (pips)", "Minimum distance between the reference closes to trigger a signal", "Signals")
			.SetGreaterThanZero()
			;

		_lotSize = Param(nameof(LotSize), 0.1m)
			.SetDisplay("Lot Size", "Fixed volume used for each trade", "Orders")
			.SetGreaterThanZero()
			;

		_maxActiveTrades = Param(nameof(MaxActiveTrades), 1)
			.SetDisplay("Max Active Trades", "Maximum number of simultaneous trades allowed", "Risk")
			.SetGreaterThanZero()
			;

		_candleType = Param(nameof(CandleType), TimeSpan.FromMinutes(30).TimeFrame())
			.SetDisplay("Candle Type", "Timeframe used to evaluate the momentum signal", "General");
	}

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

	/// <summary>
	/// Pullback distance expressed in pips.
	/// </summary>
	public decimal RetracementPips
	{
		get => _retracementPips.Value;
		set => _retracementPips.Value = value;
	}

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

	/// <summary>
	/// Index of the older candle used in the momentum calculation.
	/// </summary>
	public int T1Shift
	{
		get => _t1Shift.Value;
		set => _t1Shift.Value = value;
	}

	/// <summary>
	/// Index of the newer candle used in the momentum calculation.
	/// </summary>
	public int T2Shift
	{
		get => _t2Shift.Value;
		set => _t2Shift.Value = value;
	}

	/// <summary>
	/// Required momentum distance expressed in pips.
	/// </summary>
	public decimal DeltaPips
	{
		get => _deltaPips.Value;
		set => _deltaPips.Value = value;
	}

	/// <summary>
	/// Fixed lot size of every order.
	/// </summary>
	public decimal LotSize
	{
		get => _lotSize.Value;
		set => _lotSize.Value = value;
	}

	/// <summary>
	/// Maximum number of simultaneous trades allowed by the strategy.
	/// </summary>
	public int MaxActiveTrades
	{
		get => _maxActiveTrades.Value;
		set => _maxActiveTrades.Value = value;
	}

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

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

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

		_closeHistory.Clear();
		_state = TradeStates.WaitingForSlot;
		_pendingDirection = null;
		_extremePrice = 0m;
		_lastTradeHour = null;
		_lastSignalHour = null;
		_pointValue = 0m;
	}

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

		_pointValue = Security?.PriceStep ?? 0m;
		if (_pointValue <= 0m)
			_pointValue = 0.0001m;

		SubscribeCandles(CandleType)
			.Bind(ProcessCandle)
			.Start();

		var takeProfit = TakeProfitPips * _pointValue;
		var stopLoss = StopLossPips * _pointValue;

		StartProtection(
			takeProfit: takeProfit > 0m ? new Unit(takeProfit, UnitTypes.Absolute) : null,
			stopLoss: stopLoss > 0m ? new Unit(stopLoss, UnitTypes.Absolute) : null,
			useMarketOrders: true);
	}

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

		StoreClose(candle.ClosePrice);

		if (!IsFormedAndOnlineAndAllowTrading())
			return;

		if (!TryGetClose(T1Shift, out var t1Close) || !TryGetClose(T2Shift, out var t2Close))
			return;

		switch (_state)
		{
			case TradeStates.WaitingForSlot:
				ProcessWaitingForSlot(candle);
				break;
			case TradeStates.WaitingForImpulse:
				ProcessWaitingForImpulse(candle, t1Close, t2Close);
				break;
			case TradeStates.MonitoringRetracement:
				ProcessMonitoringRetracement(candle);
				break;
			case TradeStates.AwaitingExecution:
				ProcessAwaitingExecution(candle);
				break;
		}
	}

	private void ProcessWaitingForSlot(ICandleMessage candle)
	{
		if (CanStartNewCycle(candle.CloseTime))
		{
			_state = TradeStates.WaitingForImpulse;
			LogInfo($"Slot available at {candle.CloseTime:u}.");
		}
	}

	private void ProcessWaitingForImpulse(ICandleMessage candle, decimal t1Close, decimal t2Close)
	{
		var deltaThreshold = DeltaPips * _pointValue;
		if (deltaThreshold <= 0m)
			return;

		var difference = t2Close - t1Close;

		if (difference > deltaThreshold)
		{
			_pendingDirection = Sides.Buy;
			_extremePrice = Math.Max(candle.HighPrice, candle.ClosePrice);
			_lastSignalHour = candle.CloseTime.Hour;
			_state = TradeStates.MonitoringRetracement;
			LogInfo($"Bullish impulse detected at {candle.CloseTime:u} with diff {difference}.");
			return;
		}

		if (-difference > deltaThreshold)
		{
			_pendingDirection = Sides.Sell;
			_extremePrice = candle.LowPrice > 0m ? Math.Min(candle.LowPrice, candle.ClosePrice) : candle.ClosePrice;
			_lastSignalHour = candle.CloseTime.Hour;
			_state = TradeStates.MonitoringRetracement;
			LogInfo($"Bearish impulse detected at {candle.CloseTime:u} with diff {difference}.");
		}
	}

	private void ProcessMonitoringRetracement(ICandleMessage candle)
	{
		if (_pendingDirection == null)
		{
			ResetState();
			return;
		}

		if (_lastSignalHour.HasValue && _lastSignalHour.Value != candle.CloseTime.Hour)
		{
			LogInfo("Signal expired because the hour changed.");
			ResetState();
			return;
		}

		var retracementDistance = RetracementPips * _pointValue;
		if (retracementDistance <= 0m)
		{
			ResetState();
			return;
		}

		if (_pendingDirection == Sides.Buy)
		{
			_extremePrice = Math.Max(_extremePrice, Math.Max(candle.HighPrice, candle.ClosePrice));
			var triggerPrice = _extremePrice - retracementDistance;
			if (candle.ClosePrice <= triggerPrice)
			{
				_state = TradeStates.AwaitingExecution;
				LogInfo($"Bullish pullback confirmed at {candle.CloseTime:u}. Trigger price {triggerPrice}.");
			}
		}
		else if (_pendingDirection == Sides.Sell)
		{
			_extremePrice = _extremePrice <= 0m ? candle.LowPrice : Math.Min(_extremePrice, Math.Min(candle.LowPrice, candle.ClosePrice));
			var triggerPrice = _extremePrice + retracementDistance;
			if (candle.ClosePrice >= triggerPrice)
			{
				_state = TradeStates.AwaitingExecution;
				LogInfo($"Bearish pullback confirmed at {candle.CloseTime:u}. Trigger price {triggerPrice}.");
			}
		}
	}

	private void ProcessAwaitingExecution(ICandleMessage candle)
	{
		if (_pendingDirection == null)
		{
			ResetState();
			return;
		}

		if (!CanStartNewCycle(candle.CloseTime))
		{
			LogInfo("Cannot execute because slot conditions are no longer satisfied.");
			ResetState();
			return;
		}

		var volume = LotSize;
		if (volume <= 0m)
		{
			ResetState();
			return;
		}

		if (_pendingDirection == Sides.Buy)
		{
			BuyMarket(volume);
			_lastTradeHour = candle.CloseTime.Hour;
			LogInfo($"Opened long position at {candle.CloseTime:u} with volume {volume}.");
		}
		else if (_pendingDirection == Sides.Sell)
		{
			SellMarket(volume);
			_lastTradeHour = candle.CloseTime.Hour;
			LogInfo($"Opened short position at {candle.CloseTime:u} with volume {volume}.");
		}

		ResetState();
	}

	private bool CanStartNewCycle(DateTimeOffset time)
	{
		if (_lastTradeHour.HasValue && _lastTradeHour.Value == time.Hour)
			return false;

		if (MaxActiveTrades <= 0)
			return false;

		if (LotSize <= 0m)
			return false;

		var currentTrades = LotSize > 0m ? Math.Abs(Position) / LotSize : 0m;
		return currentTrades < MaxActiveTrades;
	}

	private void ResetState()
	{
		_state = TradeStates.WaitingForSlot;
		_pendingDirection = null;
		_extremePrice = 0m;
		_lastSignalHour = null;
	}

	private void StoreClose(decimal value)
	{
		_closeHistory.Add(value);

		var capacity = Math.Max(T1Shift, T2Shift) + 5;
		if (_closeHistory.Count > capacity)
			_closeHistory.RemoveAt(0);
	}

	private bool TryGetClose(int shift, out decimal value)
	{
		value = 0m;
		if (shift < 0)
			return false;

		var index = _closeHistory.Count - 1 - shift;
		if (index < 0 || index >= _closeHistory.Count)
			return false;

		value = _closeHistory[index];
		return true;
	}
}