Эта стратегия — порт советника MetaTrader 4 FxNode - Safe Tunnel на платформу StockSharp. Алгоритм строит «тоннель» из двух трендовых линий по последним экстремумам ZigZag: верхняя линия соединяет последние пики, нижняя — впадины. Сделка открывается, когда цена касается одной из границ тоннеля в пределах допустимого коридора и выполнены все проверки безопасности.
Особенности реализации:
Вся логика запускается по полностью сформированным свечам выбранного таймфрейма.
Пара индикаторов Highest/Lowest имитирует работу ZigZag и позволяет рассчитывать наклон трендовых линий в режиме реального времени.
Индикатор AverageTrueRange воспроизводит исходный расчёт ATRCheck() * 10, который использовался для постановки защитного стоп-лосса.
Обновления котировок Level1 применяются для контроля максимального спреда перед постановкой новой сделки.
Условия входа
По каждой свече пересчитываются экстремумы ZigZag с заданной глубиной, отклонением в пипсах и минимальным числом баров между вершинами.
На основе двух последних максимумов и минимумов строятся текущие значения верхней и нижней трендовых линий, а также высота тоннеля.
Сигнал на покупку формируется, если лучшая цена предложения находится выше нижней линии, но не дальше TouchDistanceBuyPips. Для продаж используется зеркальное условие относительно верхней линии и лучшей цены спроса.
Дополнительный фильтр времени (по умолчанию — интервал 00:00–06:00) должен разрешать торговлю. В соответствии с оригиналом сделки запрещены в пятницу, субботу и воскресенье.
Фактический спред (ask − bid) не должен превышать значение MaxSpreadPips, если доступны котировки.
Параметр MaxOpenPositions ограничивает суммарную нетто-позицию. В среде StockSharp это именно ограничение на общий объём, а не на количество отдельных ордеров.
Условия выхода
Стоп-лосс. Рассчитывается как ATR * 10 с учётом верхнего ограничения MaxStopLossPips.
Тейк-профит. Базовое значение равно высоте тоннеля; при необходимости ограничивается параметром TakeProfitPips.
Фиксированная прибыль. Если FixedTakeProfitPips > 0, позиция закрывается при достижении указанного результата в пипсах.
Трейлинг-стоп. После прохождения ценой расстояния TrailingStopPips в прибыльную сторону стоп подтягивается вслед за ценой.
Выход перед выходными. При включённом флаге CloseBeforeWeekend позиция закрывается после 23:50 по пятницам.
Закрытие сделок выполняется рыночными ордерами, как и в исходном советнике.
Управление рисками и объёмом
Пытаемся посчитать объём исходя из RiskPercentage от стоимости портфеля, если известны шаг цены и стоимость шага.
При невозможности расчёта используется фиксированный объём StaticVolume.
Итоговый объём ограничивается диапазоном MinVolume–MaxVolume.
Из-за неттинговой модели учёт MaxOpenPositions трактуется как ограничение на суммарный объём позиции, а не на количество отдельных заявок.
Параметры
Параметр
Значение по умолчанию
Описание
CandleType
Свечи 30 минут
Таймфрейм анализа и торговли.
TrendPreference
Обе стороны
Направление торговли: только покупки, только продажи или симметричный режим.
TakeProfitPips
800
Максимальная дистанция тейк-профита в пипсах (0 — без ограничений).
MaxStopLossPips
200
Максимальная дистанция стоп-лосса в пипсах (0 — без ограничений).
FixedTakeProfitPips
0
Фиксация прибыли при достижении указанного результата в пипсах.
TouchDistanceBuyPips
20
Допуск цены выше нижней линии для входа в лонг.
TouchDistanceSellPips
20
Допуск цены ниже верхней линии для входа в шорт.
TrailingStopPips
50
Дистанция трейлинг-стопа.
StaticVolume
1
Резервный объём заявки.
MinVolume / MaxVolume
0.02 / 10
Нижняя и верхняя границы объёма.
MaxSpreadPips
15
Предельно допустимый спред для открытия новой позиции.
RiskPercentage
30
Процент капитала, которым можно рискнуть в одной сделке.
MaxOpenPositions
1
Максимальная суммарная нетто-позиция (в объёмах текущих сделок).
UseTimeFilter
true
Включить/выключить торговый интервал.
SessionStart / SessionEnd
00:00 / 06:00
Временное окно торговли. Если начало позже окончания, окно пересекает полночь.
CloseBeforeWeekend
true
Закрывать позиции вечером в пятницу.
AtrPeriod
14
Период ATR.
ZigZagDepth
5
Глубина поиска экстремумов.
ZigZagDeviationPips
3
Минимальное отклонение между соседними экстремумами в пипсах.
ZigZagBackstep
1
Минимальное число баров между экстремумами.
ZigZagHistory
10
Количество запоминаемых экстремумов для построения трендовых линий.
Замечания
Восстановление ZigZag повторяет оригинал, однако параметры могут потребовать подстройки под конкретный инструмент и торговую сессию.
Фильтр по спреду работает только при наличии котировок bid/ask. В бэктестах по свечам ограничение будет пропускаться.
В модуле StockSharp используется неттинг, поэтому для учёта отдельных позиций необходимо расширить стратегию самостоятельным учётом сделок.
Строковые параметры времени из MT4 заменены типом TimeSpan. Чтобы задать ночную сессию, укажите время начала больше времени окончания (например, 23:30–05:30).
Как использовать
Подключите стратегию к инструменту, задайте таймфрейм свечей и параметры.
Убедитесь, что поток Level1 или стакан включён, чтобы корректно контролировать спред.
Перед реальной торговлей протестируйте стратегию и убедитесь в допустимом уровне риска.
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()