Esta estrategia es un port de StockSharp del asesor experto de MetaTrader 4 FxNode - Safe Tunnel. El sistema utiliza un canal de tendencia basado en ZigZag: los máximos de oscilación más recientes se conectan para formar una línea de resistencia, mientras que los mínimos crean una línea de soporte. Se abre una posición cuando el precio de mercado toca uno de los límites del canal dentro de una tolerancia configurable y pasan todas las verificaciones de seguridad.
La conversión sigue el flujo de trabajo original pero lo adapta a la API de alto nivel de StockSharp:
La suscripción a velas impulsa la lógica. Solo se procesan velas completamente formadas.
Un par Highest/Lowest emula el detector ZigZag utilizado para trazar las líneas de tendencia del túnel.
Un indicador AverageTrueRange proporciona el ancla de stop basada en volatilidad que la versión MQL producía con ATRCheck() * 10.
Las cotizaciones Level1 se monitorean para que la estrategia pueda aplicar un spread máximo antes de permitir nuevas operaciones.
Lógica de entrada
Detectar máximos y mínimos de oscilación con una profundidad ZigZag configurable, desviación (en pips) y backstep. Los dos máximos y dos mínimos más recientes definen las líneas de tendencia.
Calcular el precio de cada línea de tendencia en el tiempo de cierre de la vela actual y medir la distancia vertical entre el último máximo y mínimo de oscilación.
Configuración larga: el mejor precio ask debe permanecer por encima de la línea de tendencia inferior pero no más lejos que el buffer TouchDistanceBuyPips. Los cortos replican la condición alrededor de la línea de tendencia superior y el mejor bid.
El filtro de sesión opcional (por defecto medianoche–06:00) debe permitir el trading. La estrategia también bloquea nuevas órdenes el viernes, sábado y domingo, imitando las restricciones originales de AllowToOrder().
El spread actual (ask – bid) no debe exceder MaxSpreadPips cuando hay cotizaciones disponibles.
MaxOpenPositions controla la exposición neta máxima. Dado que StockSharp usa netting, este valor actúa como tope en el volumen total de posición en lugar de en tickets separados.
Lógica de salida
Stop-loss inicial: el EA original lo colocaba en ATR * 10. El port mantiene el mismo multiplicador respetando el tope MaxStopLossPips.
Take-profit inicial: por defecto es la distancia entre el último máximo y mínimo de oscilación, pero está limitado por TakeProfitPips cuando está configurado.
Objetivo de beneficio fijo: si FixedTakeProfitPips es mayor que cero, la posición se cierra una vez que el precio gana al menos esa cantidad de pips desde la entrada.
Trailing stop: una vez que el cierre de la vela se mueve más de TrailingStopPips a favor de la operación, el stop-loss se ajusta para asegurar beneficios.
Salida de fin de semana: cuando CloseBeforeWeekend está habilitado, cualquier posición abierta se cierra después de las 23:50 del viernes.
Todas las salidas se ejecutan con órdenes de mercado para mantener la consistencia con el comportamiento original.
Riesgo y dimensionamiento
El tamaño del lote se calcula en tres etapas:
Intentar arriesgar RiskPercentage del valor del portafolio, asumiendo que se conocen tanto el paso de precio del instrumento como el valor monetario del paso.
Si el dimensionamiento por riesgo no puede calcularse, recurrir a StaticVolume.
Limitar el volumen final entre MinVolume y MaxVolume.
Dado que StockSharp reporta una sola posición neta por instrumento, el límite original de MaxOpenPosition se interpreta como una exposición total máxima en lugar de un conteo de tickets independientes.
Parámetros
Nombre
Predeterminado
Descripción
CandleType
Velas de 30 minutos
Marco temporal principal para análisis y trading.
TrendPreference
Ambos
Elegir trading solo largo, solo corto o simétrico.
TakeProfitPips
800
Distancia máxima de take-profit en pips (0 desactiva el límite).
MaxStopLossPips
200
Distancia máxima de stop-loss en pips (0 desactiva el límite).
FixedTakeProfitPips
0
Distancia de salida anticipada expresada en pips.
TouchDistanceBuyPips
20
Las entradas largas requieren que el precio ask permanezca dentro de este buffer por encima de la línea de tendencia inferior.
TouchDistanceSellPips
20
Las entradas cortas replican el requisito de buffer cerca de la línea de tendencia superior.
TrailingStopPips
50
Distancia de trailing aplicada después de que la operación se vuelve rentable.
StaticVolume
1
Volumen de orden alternativo cuando el dimensionamiento basado en riesgo no es posible.
MinVolume / MaxVolume
0.02 / 10
Límites para el volumen final de la orden.
MaxSpreadPips
15
Spread máximo permitido en pips para nuevas entradas.
RiskPercentage
30
Porcentaje del portafolio arriesgado por operación. Establecer en 0 para usar siempre StaticVolume.
MaxOpenPositions
1
Exposición neta máxima (en múltiplos del volumen de orden actual).
UseTimeFilter
true
Habilita la ventana de trading.
SessionStart / SessionEnd
00:00 / 06:00
Ventana de trading. Cuando el inicio es posterior al fin, la ventana cruza la medianoche.
CloseBeforeWeekend
true
Cerrar cualquier posición después de las 23:50 del viernes.
AtrPeriod
14
Período de lookback del ATR utilizado para el cálculo del stop.
ZigZagDepth
5
Profundidad de lookback del ZigZag.
ZigZagDeviationPips
3
Distancia mínima entre pivotes consecutivos (en pips).
ZigZagBackstep
1
Barras entre pivotes elegibles.
ZigZagHistory
10
Número de pivotes almacenados para la proyección de líneas de tendencia.
Notas y limitaciones
La reconstrucción del ZigZag replica el comportamiento MQL combinando los indicadores Highest/Lowest con filtros de desviación y backstep. Si el instrumento opera en una sesión personalizada, considere ajustar los parámetros para alinearlos con el indicador original.
El filtrado de spread requiere cotizaciones bid/ask en tiempo real. Cuando las cotizaciones están ausentes (por ejemplo durante el backtesting con datos solo de velas) el filtro de spread se omite.
El port opera con posiciones netas. Los entornos que requieren gestión independiente de tickets deben extender la estrategia para rastrear cada ejecución por separado.
Las cadenas de tiempo de la versión MQL (p. ej., "24:00") se reemplazan con parámetros TimeSpan. Para reproducir una sesión nocturna, establezca el inicio posterior al fin, por ejemplo 23:30 a 05:30.
Uso
Adjuntar la estrategia a un instrumento, configurar el tipo de vela y los parámetros, y ejecutarla en modo simulación o en vivo.
Asegurarse de que las suscripciones de profundidad de mercado o Level1 estén habilitadas para aplicar el filtro de spread con precisión.
Revisar y ajustar los controles de riesgo antes de operar con capital real.
using System;
using System.Collections.Generic;
using Ecng.Common;
using StockSharp.Algo.Indicators;
using StockSharp.Algo.Strategies;
using StockSharp.BusinessEntities;
using StockSharp.Messages;
namespace StockSharp.Samples.Strategies;
/// <summary>
/// Simplified conversion of the FxNode Safe Tunnel EA.
/// Uses Highest/Lowest channel (tunnel) with ATR-based stops.
/// Buys near the lower boundary and sells near the upper boundary.
/// </summary>
public class FxNodeSafeTunnelStrategy : Strategy
{
private readonly StrategyParam<DataType> _candleType;
private readonly StrategyParam<int> _channelPeriod;
private readonly StrategyParam<int> _atrPeriod;
private readonly StrategyParam<decimal> _touchPct;
private decimal _entryPrice;
private int _cooldown;
public DataType CandleType { get => _candleType.Value; set => _candleType.Value = value; }
public int ChannelPeriod { get => _channelPeriod.Value; set => _channelPeriod.Value = value; }
public int AtrPeriod { get => _atrPeriod.Value; set => _atrPeriod.Value = value; }
public decimal TouchPct { get => _touchPct.Value; set => _touchPct.Value = value; }
public FxNodeSafeTunnelStrategy()
{
_candleType = Param(nameof(CandleType), TimeSpan.FromMinutes(5).TimeFrame())
.SetDisplay("Candle Type", "Timeframe", "General");
_channelPeriod = Param(nameof(ChannelPeriod), 100)
.SetGreaterThanZero()
.SetDisplay("Channel Period", "Lookback for Highest/Lowest channel", "Indicator");
_atrPeriod = Param(nameof(AtrPeriod), 14)
.SetGreaterThanZero()
.SetDisplay("ATR Period", "ATR lookback for stops", "Indicator");
_touchPct = Param(nameof(TouchPct), 0.02m)
.SetDisplay("Touch %", "How close price must be to channel boundary (0-1)", "Indicator");
}
/// <inheritdoc />
public override IEnumerable<(Security sec, DataType dt)> GetWorkingSecurities()
{
yield return (Security, CandleType);
}
/// <inheritdoc />
protected override void OnReseted()
{
base.OnReseted();
_entryPrice = 0;
_cooldown = 0;
}
/// <inheritdoc />
protected override void OnStarted2(DateTime time)
{
base.OnStarted2(time);
_entryPrice = 0;
var highest = new Highest { Length = ChannelPeriod };
var lowest = new Lowest { Length = ChannelPeriod };
var atr = new AverageTrueRange { Length = AtrPeriod };
var subscription = SubscribeCandles(CandleType);
subscription
.Bind(highest, lowest, atr, ProcessCandle)
.Start();
var area = CreateChartArea();
if (area != null)
{
DrawCandles(area, subscription);
DrawIndicator(area, highest);
DrawIndicator(area, lowest);
DrawOwnTrades(area);
}
}
private void ProcessCandle(ICandleMessage candle, decimal high, decimal low, decimal atrVal)
{
if (candle.State != CandleStates.Finished)
return;
if (_cooldown > 0)
{
_cooldown--;
return;
}
var channelWidth = high - low;
if (channelWidth <= 0)
return;
var touchZone = channelWidth * TouchPct;
var close = candle.ClosePrice;
// Check stop/take for active positions
if (Position > 0)
{
// Exit long: price near upper channel or stop loss
if (close >= high - touchZone || (_entryPrice > 0 && close < _entryPrice - atrVal * 2))
{
SellMarket();
_entryPrice = 0;
_cooldown = 10;
return;
}
}
else if (Position < 0)
{
// Exit short: price near lower channel or stop loss
if (close <= low + touchZone || (_entryPrice > 0 && close > _entryPrice + atrVal * 2))
{
BuyMarket();
_entryPrice = 0;
_cooldown = 10;
return;
}
}
// Entry signals
if (Position <= 0 && close <= low + touchZone)
{
// Price near lower boundary - buy
if (Position < 0) BuyMarket(); // close short first
BuyMarket();
_entryPrice = close;
_cooldown = 10;
}
else if (Position >= 0 && close >= high - touchZone)
{
// Price near upper boundary - sell
if (Position > 0) SellMarket(); // close long first
SellMarket();
_entryPrice = close;
_cooldown = 10;
}
}
}
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
from StockSharp.Algo.Indicators import Highest, Lowest, AverageTrueRange
from StockSharp.Algo.Strategies import Strategy
class fx_node_safe_tunnel_strategy(Strategy):
def __init__(self):
super(fx_node_safe_tunnel_strategy, self).__init__()
self._candle_type = self.Param("CandleType", DataType.TimeFrame(TimeSpan.FromMinutes(5))) \
.SetDisplay("Candle Type", "Timeframe", "General")
self._channel_period = self.Param("ChannelPeriod", 100) \
.SetGreaterThanZero() \
.SetDisplay("Channel Period", "Lookback for Highest/Lowest channel", "Indicator")
self._atr_period = self.Param("AtrPeriod", 14) \
.SetGreaterThanZero() \
.SetDisplay("ATR Period", "ATR lookback for stops", "Indicator")
self._touch_pct = self.Param("TouchPct", 0.02) \
.SetDisplay("Touch %", "How close price must be to channel boundary (0-1)", "Indicator")
self._entry_price = 0.0
self._cooldown = 0
@property
def candle_type(self):
return self._candle_type.Value
@property
def channel_period(self):
return self._channel_period.Value
@property
def atr_period(self):
return self._atr_period.Value
@property
def touch_pct(self):
return self._touch_pct.Value
def OnReseted(self):
super(fx_node_safe_tunnel_strategy, self).OnReseted()
self._entry_price = 0.0
self._cooldown = 0
def OnStarted2(self, time):
super(fx_node_safe_tunnel_strategy, self).OnStarted2(time)
self._entry_price = 0.0
highest = Highest()
highest.Length = self.channel_period
lowest = Lowest()
lowest.Length = self.channel_period
atr = AverageTrueRange()
atr.Length = self.atr_period
subscription = self.SubscribeCandles(self.candle_type)
subscription.Bind(highest, lowest, atr, self.process_candle).Start()
area = self.CreateChartArea()
if area is not None:
self.DrawCandles(area, subscription)
self.DrawIndicator(area, highest)
self.DrawIndicator(area, lowest)
self.DrawOwnTrades(area)
def process_candle(self, candle, high, low, atr_val):
if candle.State != CandleStates.Finished:
return
if self._cooldown > 0:
self._cooldown -= 1
return
high = float(high)
low = float(low)
atr_val = float(atr_val)
channel_width = high - low
if channel_width <= 0:
return
touch_zone = channel_width * float(self.touch_pct)
close = float(candle.ClosePrice)
if self.Position > 0:
if close >= high - touch_zone or (self._entry_price > 0 and close < self._entry_price - atr_val * 2):
self.SellMarket()
self._entry_price = 0.0
self._cooldown = 10
return
elif self.Position < 0:
if close <= low + touch_zone or (self._entry_price > 0 and close > self._entry_price + atr_val * 2):
self.BuyMarket()
self._entry_price = 0.0
self._cooldown = 10
return
if self.Position <= 0 and close <= low + touch_zone:
if self.Position < 0:
self.BuyMarket()
self.BuyMarket()
self._entry_price = close
self._cooldown = 10
elif self.Position >= 0 and close >= high - touch_zone:
if self.Position > 0:
self.SellMarket()
self.SellMarket()
self._entry_price = close
self._cooldown = 10
def CreateClone(self):
return fx_node_safe_tunnel_strategy()