Ver no GitHub

Estratégia especializada Gazonkos

Visão geral

Esta estratégia é uma versão StockSharp do MetaTrader 4 consultor especialista "gazonkos expert" que foi projetada para o gráfico EUR/USD H1. O EA espera por um forte movimento de impulso de uma hora e, em seguida, entra na direção desse movimento após um retrocesso configurável. Os níveis protetores de stop loss e takeprofit são aplicados como distâncias fixas medidas em pips.

Lógica MQL4 original

  • O EA monitora continuamente a diferença entre dois fechamentos históricos (Close[t2] - Close[t1]). Os padrões são t1 = 3 e t2 = 2, que correspondem aos fechamentos das velas que terminaram há duas e três horas.
  • Um impulso de alta é detectado quando Close[t2] - Close[t1] excede delta pontos. Um impulso de baixa é detectado quando Close[t1] - Close[t2] excede o mesmo limite.
  • Assim que um impulso é detectado, o EA registra o lance mais alto (para alta) ou mais baixo (para baixa) que ocorre antes do início da próxima hora. Se o preço retroceder Otkat pontos a partir desse extremo na mesma hora, uma ordem de mercado será enviada na direção do impulso.
  • As negociações são bloqueadas quando já existe uma posição aberta com o mesmo número mágico ou quando uma negociação já foi aberta durante a hora atual.
  • Cada pedido é enviado com uma distância fixa de take-profit (TakeProfit) e stop loss (StopLoss) expressa em pontos.

Máquina de estado na versão C#

A implementação StockSharp recria a máquina de estado original:

  1. WaitingForSlot – verifica se nenhuma negociação recente foi aberta na hora atual e se o número máximo configurado de negociações simultâneas não foi atingido.
  2. WaitingForImpulse – verifica os fechamentos históricos para detectar impulsos de alta ou baixa.
  3. MonitoringRetracement – monitora os máximos/mínimos da vela após o impulso e aguarda um retrocesso de RetracementPips (o antigo parâmetro Otkat) na mesma hora.
  4. AwaitingExecution – envia uma ordem de mercado na direção do impulso e aplica imediatamente níveis protetores de stop-loss e take-profit calculados a partir do instrumento PriceStep.

A estratégia processa apenas velas concluídas do período configurado e ignora dados inacabados, refletindo como o EA original avaliou as condições nas barras horárias fechadas.

Parâmetros

Parâmetro Descrição
TakeProfitPips Distância entre o preço de entrada e o nível de lucro.
RetracementPips Recuo necessário do extremo do impulso antes de entrar.
StopLossPips Distância entre o preço de entrada e o stop de proteção.
T1Shift Índice do fechamento de referência mais antigo usado para detecção de impulso (padrão 3).
T2Shift Índice do fechamento de referência mais recente usado para detecção de impulso (padrão 2).
DeltaPips Distância mínima do momento que deve separar os dois fechamentos de referência.
LotSize Volume fixo de cada pedido.
MaxActiveTrades Número máximo de negociações simultâneas; valores acima de um exigem que a conta da corretora suporte posições líquidas aditivas.
CandleType Prazo das velas utilizadas para avaliar as regras de negociação (o padrão é 1 hora).

Todas as distâncias baseadas em pip são convertidas em compensações de preço usando Security.PriceStep. Quando o instrumento não possui informações de variação de preço, um valor padrão de 0,0001 é usado, correspondendo à configuração original do EUR/USD.

Notas de implementação

  • A estratégia funciona com a assinatura de vela de alto nível de StockSharp API (SubscribeCandles().Bind).
  • Os preços fechados são armazenados em cache em um buffer contínuo leve para emular pesquisas Close[i] da versão MQL4.
  • Após a execução de uma negociação, a estratégia registra a hora da vela e bloqueia novas entradas até a próxima hora, reproduzindo a salvaguarda LastTradeTime original.
  • MaxActiveTrades é interpretado em relação à posição líquida atual. Nas contas de compensação, isso limita efetivamente o sistema a uma única negociação aberta, que corresponde ao comportamento padrão do especialista MQL4.
  • Os comentários dentro do código descrevem a máquina de estado C# em inglês para facilitar a manutenção.
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;
	}
}