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:
- 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.
- WaitingForImpulse – verifica os fechamentos históricos para detectar impulsos de alta ou baixa.
- 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.
- 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;
}
}
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()