GitHub で見る

Gazonkos Expert Strategy

Overview

This strategy is a StockSharp port of the MetaTrader 4 expert advisor "gazonkos expert" that was designed for the EUR/USD H1 chart. The EA waits for a strong one-hour momentum move, then enters in the direction of that move after a configurable pullback. Protective stop loss and take profit levels are applied as fixed distances measured in pips.

Original MQL4 logic

  • The EA continuously monitors the difference between two historical closes (Close[t2] - Close[t1]). The defaults are t1 = 3 and t2 = 2, which correspond to the closes of the candles that finished two and three hours ago.
  • A bullish impulse is detected when Close[t2] - Close[t1] exceeds delta points. A bearish impulse is detected when Close[t1] - Close[t2] exceeds the same threshold.
  • Once an impulse is detected the EA records the highest (for bullish) or lowest (for bearish) bid that occurs before the next hour starts. If price retraces by Otkat points from that extreme within the same hour, a market order is sent in the impulse direction.
  • Trades are blocked when there is already an open position with the same magic number or when a trade was already opened during the current hour.
  • Every order is sent with a fixed take profit (TakeProfit) and stop loss (StopLoss) distance expressed in points.

State machine in the C# version

The StockSharp implementation recreates the original state machine:

  1. WaitingForSlot – verifies that no recent trade was opened in the current hour and that the configured maximum number of simultaneous trades has not been reached.
  2. WaitingForImpulse – checks the historical closes to detect bullish or bearish impulses.
  3. MonitoringRetracement – keeps track of the candle highs/lows after the impulse and waits for a pullback of RetracementPips (the former Otkat parameter) within the same hour.
  4. AwaitingExecution – submits a market order in the impulse direction and immediately applies protective stop-loss and take-profit levels calculated from the instrument PriceStep.

The strategy only processes finished candles from the configured timeframe and ignores unfinished data, mirroring how the original EA evaluated conditions on closed hourly bars.

Parameters

Parameter Description
TakeProfitPips Distance between the entry price and the take profit level.
RetracementPips Required pullback from the impulse extreme before entering.
StopLossPips Distance between the entry price and the protective stop.
T1Shift Index of the older reference close used for impulse detection (default 3).
T2Shift Index of the newer reference close used for impulse detection (default 2).
DeltaPips Minimum momentum distance that must separate the two reference closes.
LotSize Fixed volume of every order.
MaxActiveTrades Maximum number of simultaneous trades; values above one require that the broker account supports additive net positions.
CandleType Timeframe of the candles used to evaluate the trading rules (default is 1 hour).

All pip-based distances are converted to price offsets using Security.PriceStep. When the instrument has no price step information a default value of 0.0001 is used, matching the original EUR/USD configuration.

Implementation notes

  • The strategy works with StockSharp's high-level candle subscription API (SubscribeCandles().Bind).
  • Closed prices are cached in a lightweight rolling buffer to emulate Close[i] lookups from the MQL4 version.
  • After a trade is executed the strategy records the candle hour and blocks new entries until the next hour, reproducing the original LastTradeTime safeguard.
  • MaxActiveTrades is interpreted against the current net position. On netting accounts this effectively limits the system to a single open trade, which matches the default behaviour of the MQL4 expert.
  • Comments inside the code describe the C# state machine in English for easier maintenance.
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;
	}
}