Открыть на GitHub

Стратегия FxNode Safe Tunnel

Описание

Эта стратегия — порт советника MetaTrader 4 FxNode - Safe Tunnel на платформу StockSharp. Алгоритм строит «тоннель» из двух трендовых линий по последним экстремумам ZigZag: верхняя линия соединяет последние пики, нижняя — впадины. Сделка открывается, когда цена касается одной из границ тоннеля в пределах допустимого коридора и выполнены все проверки безопасности.

Особенности реализации:

  • Вся логика запускается по полностью сформированным свечам выбранного таймфрейма.
  • Пара индикаторов Highest/Lowest имитирует работу ZigZag и позволяет рассчитывать наклон трендовых линий в режиме реального времени.
  • Индикатор AverageTrueRange воспроизводит исходный расчёт ATRCheck() * 10, который использовался для постановки защитного стоп-лосса.
  • Обновления котировок Level1 применяются для контроля максимального спреда перед постановкой новой сделки.

Условия входа

  1. По каждой свече пересчитываются экстремумы ZigZag с заданной глубиной, отклонением в пипсах и минимальным числом баров между вершинами.
  2. На основе двух последних максимумов и минимумов строятся текущие значения верхней и нижней трендовых линий, а также высота тоннеля.
  3. Сигнал на покупку формируется, если лучшая цена предложения находится выше нижней линии, но не дальше TouchDistanceBuyPips. Для продаж используется зеркальное условие относительно верхней линии и лучшей цены спроса.
  4. Дополнительный фильтр времени (по умолчанию — интервал 00:00–06:00) должен разрешать торговлю. В соответствии с оригиналом сделки запрещены в пятницу, субботу и воскресенье.
  5. Фактический спред (ask − bid) не должен превышать значение MaxSpreadPips, если доступны котировки.
  6. Параметр MaxOpenPositions ограничивает суммарную нетто-позицию. В среде StockSharp это именно ограничение на общий объём, а не на количество отдельных ордеров.

Условия выхода

  • Стоп-лосс. Рассчитывается как ATR * 10 с учётом верхнего ограничения MaxStopLossPips.
  • Тейк-профит. Базовое значение равно высоте тоннеля; при необходимости ограничивается параметром TakeProfitPips.
  • Фиксированная прибыль. Если FixedTakeProfitPips > 0, позиция закрывается при достижении указанного результата в пипсах.
  • Трейлинг-стоп. После прохождения ценой расстояния TrailingStopPips в прибыльную сторону стоп подтягивается вслед за ценой.
  • Выход перед выходными. При включённом флаге CloseBeforeWeekend позиция закрывается после 23:50 по пятницам.

Закрытие сделок выполняется рыночными ордерами, как и в исходном советнике.

Управление рисками и объёмом

  1. Пытаемся посчитать объём исходя из RiskPercentage от стоимости портфеля, если известны шаг цены и стоимость шага.
  2. При невозможности расчёта используется фиксированный объём StaticVolume.
  3. Итоговый объём ограничивается диапазоном MinVolumeMaxVolume.

Из-за неттинговой модели учёт 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).

Как использовать

  1. Подключите стратегию к инструменту, задайте таймфрейм свечей и параметры.
  2. Убедитесь, что поток Level1 или стакан включён, чтобы корректно контролировать спред.
  3. Перед реальной торговлей протестируйте стратегию и убедитесь в допустимом уровне риска.
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;
		}
	}
}