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:
- 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.
- WaitingForImpulse – checks the historical closes to detect bullish or bearish impulses.
- 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.
- 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;
}
}
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()