Открыть на GitHub

Стратегия Night Flat Trade

Стратегия Night Flat Trade повторяет оригинальный советник MQL5, который отслеживает узкие ночные диапазоны по EURUSD на часовом таймфрейме. Она концентрируется на периоде вокруг смены торгового дня: ожидает возврата цены к границам диапазона и пытается присоединиться к прорыву. В реализации для StockSharp используется высокоуровневое API: подписка на свечи, индикаторы Highest/Lowest и параметры StrategyParam<T> для удобного тестирования и оптимизации.

Краткое описание

  • Рынок и таймфрейм: изначально EURUSD, H1. Можно применять к другим инструментам с корректно заданным шагом цены.
  • Торговое окно: сигналы разрешены только в течение двух часов — с OpenHour по OpenHour + 1 (по времени биржи).
  • Фильтр по диапазону: разница между максимумом и минимумом последних трёх свечей должна находиться между DiffMinPips и DiffMaxPips (значения пересчитываются в цену по шагу инструмента).
  • Направление сделки: определяется положением закрытия внутри диапазона — в нижней четверти ищутся покупки, в верхней четверти продажи.

Логика работы

  1. Определение диапазона

    • Подписка на индикаторы Highest и Lowest с длиной 3 предоставляет максимум и минимум по трём последним свечам.
    • Полученный размах используется для проверки условий входа и расчёта защитных уровней.
  2. Условия входа

    • Покупка: во время разрешённого окна закрытие находится выше минимума, но не выходит за пределы нижней четверти диапазона. Стоп выставляется на уровне lowest - range/3.
    • Продажа: симметрично, закрытие ниже максимума и внутри верхней четверти. Стоп задаётся как highest + range/3.
  3. Сопровождение позиции

    • Стоп-лосс: уровень хранится во внутренней переменной, при пробое следующей свечой выполняется рыночный выход.
    • Тейк-профит: при TakeProfitPips > 0 рассчитывается фиксированная цель в пунктах от цены входа.
    • Трейлинг: при положительных TrailingStopPips и TrailingStepPips стоп подтягивается только после движения в пользу позиции на TrailingStop + TrailingStep пунктов. Далее для каждого обновления требуется дополнительный прирост на величину TrailingStepPips, что повторяет ступенчатый алгоритм из MQ5.
  4. Повторные входы

    • Пока активна позиция, стратегия не ищет новых сигналов, сохраняя режим «одна сделка — одно направление» как в исходном советнике.

Параметры

Параметр Описание Значение по умолчанию
CandleType Тип свечей для расчётов (по умолчанию H1). Часовые свечи
TakeProfitPips Дистанция до тейк-профита в пунктах (0 — без цели). 50
TrailingStopPips Базовое плечо трейлинг-стопа в пунктах (0 отключает трейлинг). 15
TrailingStepPips Дополнительное движение для каждого пересчёта трейлинга. 5
DiffMinPips Минимальный допустимый диапазон трёх свечей (в пунктах). 18
DiffMaxPips Максимальный допустимый диапазон трёх свечей (в пунктах). 28
OpenHour Час начала торгового окна (по времени площадки). 0

Используемые индикаторы

  • Highest с длиной 3 — отслеживает локальные максимумы.
  • Lowest с длиной 3 — отслеживает локальные минимумы.

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

  • Пересчёт пунктов учитывает инструменты с 3 или 5 знаками после запятой (шаг цены умножается на 10, как и в оригинальном MQ5 коде).
  • В версии для StockSharp анализ ведётся по закрытым свечам, поэтому внутрисвечные условия входа аппроксимируются ценой закрытия. Это обеспечивает детерминированность и сохраняет логику исходного алгоритма.
  • Все параметры объявлены через StrategyParam<T>, благодаря чему отображаются в интерфейсе, участвуют в оптимизации и легко интегрируются в батчевые эксперименты.
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;

namespace StockSharp.Samples.Strategies;

/// <summary>
/// Night session flat trading strategy that enters near range extremes.
/// </summary>
public class NightFlatTradeStrategy : Strategy
{
	private readonly StrategyParam<int> _rangeLength;

	private readonly StrategyParam<DataType> _candleType;
	private readonly StrategyParam<decimal> _takeProfitPips;
	private readonly StrategyParam<decimal> _trailingStopPips;
	private readonly StrategyParam<decimal> _trailingStepPips;
	private readonly StrategyParam<decimal> _diffMinPips;
	private readonly StrategyParam<decimal> _diffMaxPips;
	private readonly StrategyParam<int> _openHour;

	private Highest _highest = null!;
	private Lowest _lowest = null!;

	private decimal _pipSize;
	private decimal _entryPrice;
	private decimal? _stopPrice;
	private decimal? _takeProfitPrice;

	public DataType CandleType
	{
		get => _candleType.Value;
		set => _candleType.Value = value;
	}

	public decimal TakeProfitPips
	{
		get => _takeProfitPips.Value;
		set => _takeProfitPips.Value = value;
	}

	public decimal TrailingStopPips
	{
		get => _trailingStopPips.Value;
		set => _trailingStopPips.Value = value;
	}

	public decimal TrailingStepPips
	{
		get => _trailingStepPips.Value;
		set => _trailingStepPips.Value = value;
	}

	public decimal DiffMinPips
	{
		get => _diffMinPips.Value;
		set => _diffMinPips.Value = value;
	}

	public decimal DiffMaxPips
	{
		get => _diffMaxPips.Value;
		set => _diffMaxPips.Value = value;
	}

	public int OpenHour
	{
		get => _openHour.Value;
		set => _openHour.Value = value;
	}

	/// <summary>
	/// Number of candles used to form the overnight range.
	/// </summary>
	public int RangeLength
	{
		get => _rangeLength.Value;
		set => _rangeLength.Value = value;
	}

	public NightFlatTradeStrategy()
	{
		_candleType = Param(nameof(CandleType), TimeSpan.FromMinutes(5).TimeFrame())
			.SetDisplay("Candle Type", "Type of candles used for the setup", "General");

		_takeProfitPips = Param(nameof(TakeProfitPips), 50m)
			.SetRange(0m, 500m)
			.SetDisplay("Take Profit (pips)", "Take profit distance in pips", "Risk");

		_trailingStopPips = Param(nameof(TrailingStopPips), 15m)
			.SetRange(0m, 200m)
			.SetDisplay("Trailing Stop (pips)", "Trailing stop distance in pips", "Risk");

		_trailingStepPips = Param(nameof(TrailingStepPips), 5m)
			.SetRange(0m, 200m)
			.SetDisplay("Trailing Step (pips)", "Extra advance required to shift the trailing stop", "Risk");

		_diffMinPips = Param(nameof(DiffMinPips), 18m)
			.SetGreaterThanZero()
			.SetDisplay("Min Range (pips)", "Minimum three-candle range in pips", "Setup");

		_diffMaxPips = Param(nameof(DiffMaxPips), 28m)
			.SetGreaterThanZero()
			.SetDisplay("Max Range (pips)", "Maximum three-candle range in pips", "Setup");

		_openHour = Param(nameof(OpenHour), 0)
			.SetRange(0, 23)
			.SetDisplay("Open Hour", "Hour (exchange time) when entries become active", "Schedule");

		_rangeLength = Param(nameof(RangeLength), 3)
			.SetGreaterThanZero()
			.SetDisplay("Range Length", "Number of candles composing the range", "Setup");
	}

	public override IEnumerable<(Security sec, DataType dt)> GetWorkingSecurities()
	{
		return [(Security, CandleType)];
	}

	protected override void OnReseted()
	{
		base.OnReseted();

		_highest = null!;
		_lowest = null!;
		_pipSize = 0m;
		_entryPrice = 0m;
		_stopPrice = null;
		_takeProfitPrice = null;
	}

	protected override void OnStarted2(DateTime time)
	{
		base.OnStarted2(time);

		_highest = new Highest { Length = RangeLength };
		_lowest = new Lowest { Length = RangeLength };

		var priceStep = Security?.PriceStep ?? 0m;
		var decimals = Security?.Decimals;

		if (priceStep <= 0m)
			priceStep = 0.0001m;

		_pipSize = priceStep;

		if (decimals.HasValue && (decimals.Value == 3 || decimals.Value == 5))
			_pipSize *= 10m;

		var subscription = SubscribeCandles(CandleType);
		subscription
			.Bind(_highest, _lowest, 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 highestValue, decimal lowestValue)
	{
		if (candle.State != CandleStates.Finished)
			return;

		// Manage active trades before scanning for new setups.
		HandleExistingPosition(candle);

		if (Position != 0m)
			return;

		if (_highest == null || _lowest == null)
			return;

		if (!_highest.IsFormed || !_lowest.IsFormed)
			return;

		if (!IsFormedAndOnlineAndAllowTrading())
			return;

		var diff = highestValue - lowestValue;
		if (diff <= 0m)
			return;

		var quarter = diff / 4m;
		var closePrice = candle.ClosePrice;

		if (closePrice > lowestValue && closePrice <= lowestValue + quarter)
		{
			BuyMarket();
			_entryPrice = closePrice;
			_stopPrice = lowestValue - diff / 3m;
			_takeProfitPrice = TakeProfitPips > 0m ? closePrice + ToPrice(TakeProfitPips) : null;
			return;
		}

		if (closePrice < highestValue && closePrice >= highestValue - quarter)
		{
			SellMarket();
			_entryPrice = closePrice;
			_stopPrice = highestValue + diff / 3m;
			_takeProfitPrice = TakeProfitPips > 0m ? closePrice - ToPrice(TakeProfitPips) : null;
		}
	}

	private void HandleExistingPosition(ICandleMessage candle)
	{
		if (Position > 0m)
		{
			UpdateTrailingForLong(candle);

			if (_stopPrice.HasValue && candle.LowPrice <= _stopPrice.Value)
			{
				SellMarket(Math.Abs(Position));
				ResetTradeState();
				return;
			}

			if (_takeProfitPrice.HasValue && candle.HighPrice >= _takeProfitPrice.Value)
			{
				SellMarket(Math.Abs(Position));
				ResetTradeState();
			}
		}
		else if (Position < 0m)
		{
			UpdateTrailingForShort(candle);

			if (_stopPrice.HasValue && candle.HighPrice >= _stopPrice.Value)
			{
				BuyMarket(Math.Abs(Position));
				ResetTradeState();
				return;
			}

			if (_takeProfitPrice.HasValue && candle.LowPrice <= _takeProfitPrice.Value)
			{
				BuyMarket(Math.Abs(Position));
				ResetTradeState();
			}
		}
	}

	private void UpdateTrailingForLong(ICandleMessage candle)
	{
		if (TrailingStopPips <= 0m || TrailingStepPips <= 0m || _stopPrice == null)
			return;

		var trailingDistance = ToPrice(TrailingStopPips);
		var stepDistance = ToPrice(TrailingStepPips);

		var advance = candle.HighPrice - _entryPrice;
		if (advance < trailingDistance + stepDistance)
			return;

		var newStop = candle.HighPrice - trailingDistance;

		if (newStop <= _stopPrice.Value || newStop - _stopPrice.Value < stepDistance)
			return;

		// Raise the stop only after price travels an additional step distance.
		_stopPrice = newStop;
	}

	private void UpdateTrailingForShort(ICandleMessage candle)
	{
		if (TrailingStopPips <= 0m || TrailingStepPips <= 0m || _stopPrice == null)
			return;

		var trailingDistance = ToPrice(TrailingStopPips);
		var stepDistance = ToPrice(TrailingStepPips);

		var advance = _entryPrice - candle.LowPrice;
		if (advance < trailingDistance + stepDistance)
			return;

		var newStop = candle.LowPrice + trailingDistance;

		if (newStop >= _stopPrice.Value || _stopPrice.Value - newStop < stepDistance)
			return;

		// Lower the stop only after price moves the additional step distance in favor of the trade.
		_stopPrice = newStop;
	}

	private decimal ToPrice(decimal pips)
	{
		if (pips <= 0m)
			return 0m;

		var pip = _pipSize > 0m ? _pipSize : 0.0001m;
		return pips * pip;
	}

	private void ResetTradeState()
	{
		_entryPrice = 0m;
		_stopPrice = null;
		_takeProfitPrice = null;
	}
}