Diese Strategie ist ein StockSharp-Port des MetaTrader 4 Expert Advisors FxNode - Safe Tunnel. Das System verwendet einen ZigZag-basierten Trendkanal: Die jüngsten Swing-Hochs werden verbunden, um eine Widerstandslinie zu bilden, während Swing-Tiefs eine Unterstützungslinie erzeugen. Eine Position wird eröffnet, wenn der Marktpreis eine der Kanalgrenzen innerhalb einer konfigurierbaren Toleranz berührt und alle Sicherheitsprüfungen bestanden werden.
Die Konvertierung folgt dem originalen Workflow, passt ihn jedoch an die High-Level-API von StockSharp an:
Die Kerzen-Subscription treibt die Logik an. Es werden nur vollständig geformte Kerzen verarbeitet.
Ein Highest/Lowest-Paar emuliert den ZigZag-Detektor, der zum Zeichnen der Tunnel-Trendlinien verwendet wird.
Ein AverageTrueRange-Indikator liefert den volatilitätsbasierten Stop-Anker, den die MQL-Version mit ATRCheck() * 10 erzeugte.
Level1-Kurse werden überwacht, damit die Strategie vor dem Zulassen neuer Trades einen maximalen Spread durchsetzen kann.
Einstiegslogik
Swing-Hochs und -Tiefs mit einer konfigurierbaren ZigZag-Tiefe, Abweichung (in Pips) und Backstep erkennen. Die neuesten zwei Hochs und zwei Tiefs definieren die Trendlinien.
Den Preis jeder Trendlinie zum aktuellen Kerzenschlusskurs berechnen und den vertikalen Abstand zwischen dem letzten Swing-Hoch und -Tief messen.
Long-Setup: Der beste Ask-Preis muss oberhalb der unteren Trendlinie bleiben, aber nicht weiter als der TouchDistanceBuyPips-Puffer entfernt. Shorts spiegeln die Bedingung um die obere Trendlinie und den besten Bid.
Der optionale Sessionsfilter (Standardmäßig Mitternacht–06:00) muss den Handel erlauben. Die Strategie blockiert auch neue Aufträge am Freitag, Samstag und Sonntag und ahmt die ursprünglichen AllowToOrder()-Beschränkungen nach.
Der aktuelle Spread (Ask – Bid) darf MaxSpreadPips nicht überschreiten, wenn Kurse verfügbar sind.
MaxOpenPositions kontrolliert das maximale Netto-Engagement. Da StockSharp Netting verwendet, wirkt dieser Wert als Obergrenze für das Gesamtpositionsvolumen statt für separate Tickets.
Ausstiegslogik
Anfänglicher Stop-Loss: Der originale EA platzierte ihn bei ATR * 10. Der Port behält denselben Multiplikator bei und respektiert dabei die MaxStopLossPips-Obergrenze.
Anfänglicher Take-Profit: Standardmäßig der Abstand zwischen dem letzten Swing-Hoch und -Tief, aber durch TakeProfitPips begrenzt, wenn konfiguriert.
Festes Gewinnziel: Wenn FixedTakeProfitPips größer als null ist, wird die Position geschlossen, sobald der Preis mindestens so viele Pips vom Einstieg gewinnt.
Trailing Stop: Sobald sich der Kerzenschluss um mehr als TrailingStopPips zugunsten des Trades bewegt, wird der Stop-Loss angepasst, um Gewinne zu sichern.
Wochenendausstieg: Wenn CloseBeforeWeekend aktiviert ist, wird jede offene Position nach 23:50 am Freitag geschlossen.
Alle Ausstiege werden mit Market-Orders ausgeführt, um konsistent mit dem ursprünglichen Verhalten zu bleiben.
Risiko und Positionsgröße
Die Losgröße wird in drei Stufen berechnet:
Versuchen, RiskPercentage des Portfoliowertes zu riskieren, vorausgesetzt, der Preisschritt des Instruments und der monetäre Schrittwert sind bekannt.
Wenn die risikobasierte Dimensionierung nicht berechnet werden kann, auf StaticVolume zurückgreifen.
Das endgültige Volumen zwischen MinVolume und MaxVolume begrenzen.
Da StockSharp eine einzige Nettoposition pro Instrument meldet, wird das ursprüngliche MaxOpenPosition-Limit als maximales Gesamtengagement und nicht als Anzahl unabhängiger Tickets interpretiert.
Parameter
Name
Standard
Beschreibung
CandleType
30-Minuten-Kerzen
Primärer Zeitrahmen für Analyse und Trading.
TrendPreference
Beide
Nur Long, nur Short oder symmetrisches Trading wählen.
TakeProfitPips
800
Maximale Take-Profit-Distanz in Pips (0 deaktiviert das Limit).
MaxStopLossPips
200
Maximale Stop-Loss-Distanz in Pips (0 deaktiviert das Limit).
FixedTakeProfitPips
0
Frühzeitige Ausstiegsdistanz in Pips.
TouchDistanceBuyPips
20
Long-Einstiege erfordern, dass der Ask-Preis innerhalb dieses Puffers oberhalb der unteren Trendlinie bleibt.
TouchDistanceSellPips
20
Short-Einstiege spiegeln die Pufferanforderung nahe der oberen Trendlinie.
TrailingStopPips
50
Trailing-Distanz, die angewendet wird, nachdem der Trade profitabel wird.
StaticVolume
1
Ausweich-Ordervolumen, wenn risikobasierte Dimensionierung nicht möglich ist.
MinVolume / MaxVolume
0.02 / 10
Grenzen für das endgültige Ordervolumen.
MaxSpreadPips
15
Maximal erlaubter Spread in Pips für neue Einstiege.
RiskPercentage
30
Portfolio-Prozentsatz, der pro Trade riskiert wird. Auf 0 setzen, um immer StaticVolume zu verwenden.
MaxOpenPositions
1
Maximales Netto-Engagement (in Vielfachen des aktuellen Ordervolumens).
UseTimeFilter
true
Aktiviert das Handelsfenster.
SessionStart / SessionEnd
00:00 / 06:00
Handelsfenster. Wenn der Start später als das Ende ist, erstreckt sich das Fenster über Mitternacht.
CloseBeforeWeekend
true
Jede Position nach 23:50 am Freitag schließen.
AtrPeriod
14
ATR-Lookback für die Stop-Berechnung.
ZigZagDepth
5
ZigZag-Lookback-Tiefe.
ZigZagDeviationPips
3
Mindestabstand zwischen aufeinanderfolgenden Pivots (in Pips).
ZigZagBackstep
1
Balken zwischen zulässigen Pivots.
ZigZagHistory
10
Anzahl gespeicherter Pivots für die Trendlinienprojektion.
Hinweise und Einschränkungen
Die ZigZag-Rekonstruktion spiegelt das MQL-Verhalten durch die Kombination der Highest/Lowest-Indikatoren mit Abweichungs- und Backstep-Filtern. Wenn das Instrument in einer benutzerdefinierten Session handelt, sollten die Parameter angepasst werden, um sie mit dem ursprünglichen Indikator auszurichten.
Die Spread-Filterung erfordert Live-Bid/Ask-Kurse. Wenn Kurse fehlen (z. B. beim Backtesting mit nur Kerzendaten) wird der Spread-Filter übersprungen.
Der Port arbeitet mit Nettopositionen. Umgebungen, die unabhängiges Ticket-Management erfordern, sollten die Strategie erweitern, um jede Ausführung separat zu verfolgen.
Zeitstrings aus der MQL-Version (z. B. "24:00") werden durch TimeSpan-Parameter ersetzt. Um eine Nacht-Session zu reproduzieren, setzen Sie den Start später als das Ende, zum Beispiel 23:30 bis 05:30.
Verwendung
Die Strategie an ein Instrument anhängen, den Kerzentyp und die Parameter konfigurieren und im Simulations- oder Live-Modus ausführen.
Sicherstellen, dass Markttiefe oder Level1-Subscriptions aktiviert sind, um den Spread-Filter genau durchzusetzen.
Die Risikokontrollen vor dem Handel mit echtem Kapital überprüfen und anpassen.
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()