Estrategia experta de Gazonkos
Descripción general
Esta estrategia es una StockSharp versión del MetaTrader 4 asesores expertos "gazonkos expert" que fue diseñado para el gráfico EUR/USD H1. El EA espera un fuerte movimiento de impulso de una hora y luego ingresa en la dirección de ese movimiento después de un retroceso configurable. Los niveles protectores de stop loss y takeprofit se aplican como distancias fijas medidas en pips.
Lógica original MQL4
- El EA monitorea continuamente la diferencia entre dos cierres históricos (
Close[t2] - Close[t1]). Los valores predeterminados son t1 = 3 y t2 = 2, que corresponden a los cierres de las velas que terminaron hace dos y tres horas.
- Se detecta un impulso alcista cuando
Close[t2] - Close[t1] supera los delta puntos. Se detecta un impulso bajista cuando Close[t1] - Close[t2] supera el mismo umbral.
- Una vez que se detecta un impulso, el EA registra la oferta más alta (para alcistas) o más baja (para bajistas) que se produce antes de que comience la siguiente hora. Si el precio retrocede
Otkat puntos desde ese extremo en la misma hora, se envía una orden de mercado en la dirección de impulso.
- Las operaciones se bloquean cuando ya existe una posición abierta con el mismo número mágico o cuando ya se abrió una operación durante la hora actual.
- Cada orden se envía con una distancia fija de takeprofit (
TakeProfit) y stop loss (StopLoss) expresada en puntos.
Máquina de estados en la versión C#
La implementación StockSharp recrea la máquina de estado original:
- WaitingForSlot: verifica que no se haya abierto ninguna operación reciente en la hora actual y que no se haya alcanzado el número máximo configurado de operaciones simultáneas.
- WaitingForImpulse: verifica los cierres históricos para detectar impulsos alcistas o bajistas.
- MonitoringRetracement: realiza un seguimiento de los máximos y mínimos de las velas después del impulso y espera un retroceso de
RetracementPips (el antiguo parámetro Otkat) dentro de la misma hora.
- AwaitingExecution: envía una orden de mercado en la dirección de impulso e inmediatamente aplica niveles protectores de stop-loss y take-profit calculados a partir del instrumento
PriceStep.
La estrategia solo procesa velas terminadas del período de tiempo configurado e ignora los datos no terminados, reflejando cómo el EA original evaluó las condiciones en las barras horarias cerradas.
Parámetros
| Parámetro |
Descripción |
TakeProfitPips |
Distancia entre el precio de entrada y el nivel de toma de ganancias. |
RetracementPips |
Se requiere retroceso desde el extremo del impulso antes de entrar. |
StopLossPips |
Distancia entre el precio de entrada y el stop de protección. |
T1Shift |
Índice del cierre de referencia más antiguo utilizado para la detección de impulsos (predeterminado 3). |
T2Shift |
Índice del cierre de referencia más nuevo utilizado para la detección de impulsos (predeterminado 2). |
DeltaPips |
Distancia mínima de impulso que debe separar los dos cierres de referencia. |
LotSize |
Volumen fijo de cada pedido. |
MaxActiveTrades |
Número máximo de operaciones simultáneas; Los valores superiores a uno requieren que la cuenta del corredor admita posiciones netas aditivas. |
CandleType |
Marco de tiempo de las velas utilizadas para evaluar las reglas comerciales (el valor predeterminado es 1 hora). |
Todas las distancias basadas en pips se convierten en compensaciones de precios usando Security.PriceStep. Cuando el instrumento no tiene información de paso de precio, se utiliza un valor predeterminado de 0,0001, que coincide con la configuración EUR/USD original.
Notas de implementación
- La estrategia funciona con la suscripción de vela de alto nivel de StockSharp API (
SubscribeCandles().Bind).
- Los precios cerrados se almacenan en caché en un búfer móvil ligero para emular las búsquedas
Close[i] de la versión MQL4.
- Después de ejecutar una operación, la estrategia registra la hora de la vela y bloquea nuevas entradas hasta la hora siguiente, reproduciendo la protección
LastTradeTime original.
MaxActiveTrades se interpreta en función de la posición neta actual. En la compensación de cuentas, esto limita efectivamente el sistema a una única operación abierta, que coincide con el comportamiento predeterminado del experto MQL4.
- Los comentarios dentro del código describen la máquina de estado de C# en inglés para facilitar el mantenimiento.
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()