Стратегия 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#
Порт воспроизводит исходный конечный автомат:
- WaitingForSlot – проверяет, что в текущем часу не было сделок и лимит по количеству одновременно открытых позиций не превышен.
- WaitingForImpulse – ищет бычьи или медвежьи импульсы по значениям
Close[t2] и Close[t1].
- MonitoringRetracement – фиксирует экстремумы после импульса и ожидает откат на
RetracementPips (ранее Otkat) в течение того же часа.
- 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;
}
}
import clr
clr.AddReference("StockSharp.Messages")
clr.AddReference("StockSharp.Algo")
clr.AddReference("StockSharp.Algo.Indicators")
clr.AddReference("StockSharp.Algo.Strategies")
from System import TimeSpan
from StockSharp.Messages import DataType, CandleStates, Unit, UnitTypes
from StockSharp.Algo.Strategies import Strategy
class gazonkos_expert_strategy(Strategy):
"""
Gazonkos Expert: momentum pullback strategy.
Detects impulse via close difference, waits for retracement,
then enters. Uses StartProtection for SL/TP.
"""
def __init__(self):
super(gazonkos_expert_strategy, self).__init__()
self._take_profit_pips = self.Param("TakeProfitPips", 16.0) \
.SetDisplay("Take Profit (pips)", "Distance to take profit level", "Risk")
self._retracement_pips = self.Param("RetracementPips", 16.0) \
.SetDisplay("Retracement (pips)", "Pullback distance for confirmation", "Signals")
self._stop_loss_pips = self.Param("StopLossPips", 40.0) \
.SetDisplay("Stop Loss (pips)", "Distance to protective stop", "Risk")
self._t1_shift = self.Param("T1Shift", 3) \
.SetDisplay("T1 Shift", "Older reference close index", "Signals")
self._t2_shift = self.Param("T2Shift", 2) \
.SetDisplay("T2 Shift", "Newer reference close index", "Signals")
self._delta_pips = self.Param("DeltaPips", 40.0) \
.SetDisplay("Delta (pips)", "Minimum distance between reference closes", "Signals")
self._candle_type = self.Param("CandleType", DataType.TimeFrame(TimeSpan.FromMinutes(30))) \
.SetDisplay("Candle Type", "Timeframe for momentum signal", "General")
self._close_history = []
self._state = 0 # 0=WaitSlot, 1=WaitImpulse, 2=MonitorRetracement, 3=Execute
self._pending_direction = None
self._extreme_price = 0.0
self._last_trade_hour = None
self._last_signal_hour = None
self._point_value = 0.0001
@property
def candle_type(self):
return self._candle_type.Value
def OnReseted(self):
super(gazonkos_expert_strategy, self).OnReseted()
self._close_history = []
self._state = 0
self._pending_direction = None
self._extreme_price = 0.0
self._last_trade_hour = None
self._last_signal_hour = None
def OnStarted2(self, time):
super(gazonkos_expert_strategy, self).OnStarted2(time)
ps = 0.0001
if self.Security is not None and self.Security.PriceStep is not None:
ps = float(self.Security.PriceStep)
if ps <= 0:
ps = 0.0001
self._point_value = ps
tp = self._take_profit_pips.Value * ps
sl = self._stop_loss_pips.Value * ps
if tp > 0 and sl > 0:
self.StartProtection(
Unit(tp, UnitTypes.Absolute),
Unit(sl, UnitTypes.Absolute))
subscription = self.SubscribeCandles(self.candle_type)
subscription.Bind(self._process_candle).Start()
def _process_candle(self, candle):
if candle.State != CandleStates.Finished:
return
close = float(candle.ClosePrice)
high = float(candle.HighPrice)
low = float(candle.LowPrice)
capacity = max(self._t1_shift.Value, self._t2_shift.Value) + 5
self._close_history.append(close)
while len(self._close_history) > capacity:
self._close_history.pop(0)
t1 = self._t1_shift.Value
t2 = self._t2_shift.Value
if len(self._close_history) - 1 - t1 < 0 or len(self._close_history) - 1 - t2 < 0:
return
t1_close = self._close_history[len(self._close_history) - 1 - t1]
t2_close = self._close_history[len(self._close_history) - 1 - t2]
hour = candle.CloseTime.Hour
if self._state == 0:
if self._can_start_new_cycle(hour):
self._state = 1
if self._state == 1:
delta_threshold = self._delta_pips.Value * self._point_value
if delta_threshold <= 0:
return
diff = t2_close - t1_close
if diff > delta_threshold:
self._pending_direction = 1 # buy
self._extreme_price = max(high, close)
self._last_signal_hour = hour
self._state = 2
elif -diff > delta_threshold:
self._pending_direction = -1 # sell
self._extreme_price = min(low, close) if low > 0 else close
self._last_signal_hour = hour
self._state = 2
if self._state == 2:
if self._pending_direction is None:
self._reset_state()
return
if self._last_signal_hour is not None and self._last_signal_hour != hour:
self._reset_state()
return
retracement = self._retracement_pips.Value * self._point_value
if retracement <= 0:
self._reset_state()
return
if self._pending_direction == 1:
self._extreme_price = max(self._extreme_price, max(high, close))
if close <= self._extreme_price - retracement:
self._state = 3
elif self._pending_direction == -1:
if self._extreme_price <= 0:
self._extreme_price = low
self._extreme_price = min(self._extreme_price, min(low, close))
if close >= self._extreme_price + retracement:
self._state = 3
if self._state == 3:
if self._pending_direction is None:
self._reset_state()
return
if not self._can_start_new_cycle(hour):
self._reset_state()
return
if self._pending_direction == 1:
self.BuyMarket()
self._last_trade_hour = hour
elif self._pending_direction == -1:
self.SellMarket()
self._last_trade_hour = hour
self._reset_state()
def _can_start_new_cycle(self, hour):
if self._last_trade_hour is not None and self._last_trade_hour == hour:
return False
return True
def _reset_state(self):
self._state = 0
self._pending_direction = None
self._extreme_price = 0.0
self._last_signal_hour = None
def CreateClone(self):
return gazonkos_expert_strategy()