Открыть на GitHub

Стратегия Gazonkos Expert

Общее описание

Эта стратегия представляет собой порт советника MetaTrader 4 «gazonkos expert», рассчитанного на часовой график EUR/USD. Алгоритм ищет сильные импульсы на часовом таймфрейме и после отката входит в направлении движения. Стоп-лосс и тейк-профит задаются фиксированными расстояниями в пунктах.

Логика оригинального MQL4-советника

  • Непрерывно вычисляется разница между двумя прошлыми ценами закрытия (Close[t2] - Close[t1]). По умолчанию t1 = 3, t2 = 2, что соответствует барам, завершившимся два и три часа назад.
  • Если Close[t2] - Close[t1] превышает порог delta, фиксируется бычий импульс; если Close[t1] - Close[t2] превышает тот же порог, фиксируется медвежий импульс.
  • После импульса эксперт отслеживает максимум (для покупок) или минимум (для продаж), достигнутый до смены часа. Если цена откатывает от этого экстремума на Otkat пунктов в пределах того же часа, открывается рыночная сделка в сторону импульса.
  • Новые сделки блокируются, если уже есть позиция с тем же magic-номером или если сделка уже открывалась в текущем часу.
  • Каждая заявка сопровождается фиксированными уровнями тейк-профита (TakeProfit) и стоп-лосса (StopLoss), измеряемыми в пунктах.

Состояния в реализации на C#

Порт воспроизводит исходный конечный автомат:

  1. WaitingForSlot – проверяет, что в текущем часу не было сделок и лимит по количеству одновременно открытых позиций не превышен.
  2. WaitingForImpulse – ищет бычьи или медвежьи импульсы по значениям Close[t2] и Close[t1].
  3. MonitoringRetracement – фиксирует экстремумы после импульса и ожидает откат на RetracementPips (ранее Otkat) в течение того же часа.
  4. AwaitingExecution – отправляет рыночный ордер в сторону импульса и сразу же устанавливает защитные уровни на основе PriceStep инструмента.

Анализ ведётся только по закрытым свечам выбранного таймфрейма, что повторяет принцип работы оригинального советника.

Параметры

Параметр Описание
TakeProfitPips Расстояние от точки входа до тейк-профита.
RetracementPips Величина отката, необходимая перед входом.
StopLossPips Расстояние от точки входа до стоп-лосса.
T1Shift Индекс более старой свечи для оценки импульса (по умолчанию 3).
T2Shift Индекс более новой свечи для оценки импульса (по умолчанию 2).
DeltaPips Минимальная разница между сравниваемыми закрытиями.
LotSize Фиксированный объём каждой сделки.
MaxActiveTrades Максимальное число одновременно открытых сделок; значения больше единицы требуют учёта неттинга у брокера.
CandleType Тип свечей, используемых для анализа (по умолчанию 1 час).

Все расстояния в пунктах переводятся в ценовые смещения через Security.PriceStep. Если шаг цены не задан, используется значение 0.0001, соответствующее базовой настройке EUR/USD.

Особенности реализации

  • Используется высокоуровневый API StockSharp для подписки на свечи (SubscribeCandles().Bind).
  • Закрытия свечей сохраняются в компактном буфере, что эмулирует обращения Close[i] из MQL4.
  • После открытия сделки фиксируется час свечи, и до следующего часа новые входы блокируются – аналог оригинального поля LastTradeTime.
  • Параметр MaxActiveTrades сопоставляется с текущей чистой позицией. На неттинговых счетах это фактически ограничивает стратегию одной сделкой, что соответствует поведению советника по умолчанию.
  • Комментарии в коде подробно описывают логику состояний на английском языке для удобства сопровождения.
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;
	}
}