Открыть на GitHub

Стратегия Daily STP Entry Frame (StockSharp)

Общее описание

Стратегия воспроизводит логику экспертного советника MetaTrader «Daily STP Entry Frame» средствами высокоуровневого API StockSharp. На каждом новом торговом дне формируются отложенные стоп-заявки у экстремумов предыдущей сессии. Дополнительные фильтры гарантируют, что текущая цена уже находится рядом с уровнем пробоя. Алгоритм ориентирован на валютные инструменты, где под «базовым пунктом» понимается 0.0001 для пятизначных котировок.

Последовательность работы

  1. Анализ дневного диапазона. Подписка на дневные свечи обеспечивает хранение максимума и минимума предыдущей завершённой свечи.
  2. Контроль в реальном времени. Данные Level1 дают оперативные значения bid/ask и последней сделки для управления позициями и времени размещения ордеров.
  3. Подготовка заявок. В начале нового дня, если выполняются условия дистанции ThresholdPoints и расположения дневного открытия относительно вчерашнего экстремума, регистрируются:
    • стоп-покупка по цене High + SpreadPoints / 2;
    • стоп-продажа по цене Low - SpreadPoints / 2.
  4. Фильтрация рисков. Новые заявки не выставляются при превышении допустимой просадки MaximumDrawdownPercent, при наступлении выходных, вне допустимых часов или при несоответствии фильтру по дням недели.
  5. Ведение позиции. После активации ордера стратегия:
    • удерживает фиксированные стоп-лосс и тейк-профит в базовых пунктах;
    • может закрыть сделку по таймеру CloseAfterSeconds;
    • выполняет виртуальное трейлинг-сопровождение при TrailingSlope < 1, повторяя формулу оригинального советника.
  6. Вечернее обслуживание. Ненужные отложенные заявки снимаются после NoNewOrdersHour (в пятницу — NoNewOrdersHourFriday) и при смене календарного дня.

Правила входа и защиты

  • Покупка
    • Разрешена при SideFilter = 0 (обе стороны) или 1 (только покупки).
    • Разница между вчерашним максимумом и текущей ценой ≥ ThresholdPoints.
    • Сегодняшнее открытие находится ниже вчерашнего максимума.
    • Цена заявки должна быть достаточно удалена от текущего ask.
  • Продажа
    • Разрешена при SideFilter = 0 или -1.
    • Текущая цена выше вчерашнего минимума не менее чем на ThresholdPoints.
    • Сегодняшнее открытие выше вчерашнего минимума.
    • Цена заявки должна быть достаточно удалена от текущего bid.
  • Управление капиталом
    • Размер заявки рассчитывается из доли накопленной прибыли (PercentOfProfit).
    • Объём ограничен интервалом [MinVolume; MaxVolume] и приводится к шагу лота инструмента.
    • При просадке свыше MaximumDrawdownPercent новые ордера не формируются.
  • Защитные механизмы
    • Стоп-лосс и тейк-профит хранятся в базовых пунктах и переводятся в абсолютную цену с учётом PriceStep.
    • Трейлинг использует формулу оригинального советника: Bid - StopLoss - Slope * (Bid - Entry) для длинных позиций (и зеркально для коротких).
    • Таймер CloseAfterSeconds принудительно закрывает позицию по истечении заданного времени.

Параметры

Параметр Назначение
CandleType Таймфрейм свечей для расчёта дневного диапазона (по умолчанию 1 день).
StopLossPoints / TakeProfitPoints Дистанция до стоп-лосса и тейк-профита в базовых пунктах.
TrailingSlope Коэффициент трейлинга; значения ≥ 1 отключают сопровождение.
SideFilter -1 только продажи, 0 обе стороны, 1 только покупки.
ThresholdPoints Минимальная дистанция от текущей цены до экстремума для активации сигналов.
SpreadPoints Добавочный сдвиг для компенсации спреда.
SlippagePoints Дополнительный буфер при проверке минимально допустимого расстояния.
NoNewOrdersHour / NoNewOrdersHourFriday Время вечернего удаления отложенных заявок.
EarliestOrderHour Самый ранний час, когда разрешено ставить новые ордера.
DayFilter 6 — все дни; значения 0–5 соответствуют воскресенью–пятнице.
CloseAfterSeconds Время жизни позиции в секундах (0 отключает).
PercentOfProfit Доля прибыли портфеля, используемая для расчёта объёма.
MinVolume / MaxVolume Нижняя и верхняя границы объёма.
MaximumDrawdownPercent Порог просадки, при котором торговля приостанавливается.

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

  • Если у инструмента Decimals равен 3 или 5, размер базового пункта вычисляется как PriceStep * 10, что повторяет подход MetaTrader.
  • Автоматическое снятие ордеров при смене суток предотвращает накопление старых заявок.
  • Учтён отдельный пятничный дедлайн для снятия заявок.
  • Почтовые уведомления об изменении Equity заменены сообщениями в журнал стратегии.
  • Даже при наличии открытой позиции противоположные стоп-заявки остаются активными, как и в оригинальном советнике.

Рекомендации по применению

  • Перед боевым запуском протестируйте параметры в StockSharp Designer или Runner.
  • Убедитесь, что инструмент имеет корректно настроенные PriceStep, StepPrice и VolumeStep — это критично для верного пересчёта базовых пунктов и объёмов.
  • Используйте встроенные средства управления рисками StockSharp (лимиты портфеля, проверки заявок) для дополнительной защиты.
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>
/// Daily breakout strategy that enters on price crossing previous session extremes.
/// Converted from "Daily STP Entry Frame" MetaTrader expert.
/// Uses market orders when price breaks above previous high or below previous low.
/// </summary>
public class DailyStpEntryFrameStrategy : Strategy
{
	private readonly StrategyParam<DataType> _candleType;
	private readonly StrategyParam<decimal> _stopLossPoints;
	private readonly StrategyParam<decimal> _takeProfitPoints;

	private decimal _pipSize;
	private decimal _stopLossOffset;
	private decimal _takeProfitOffset;

	private decimal? _previousDayHigh;
	private decimal? _previousDayLow;
	private decimal _currentDayHigh;
	private decimal _currentDayLow;
	private DateTime? _currentTradingDay;
	private bool _tradedToday;

	private decimal _entryPrice;
	private decimal? _longStop;
	private decimal? _longTake;
	private decimal? _shortStop;
	private decimal? _shortTake;

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

	public decimal StopLossPoints
	{
		get => _stopLossPoints.Value;
		set => _stopLossPoints.Value = value;
	}

	public decimal TakeProfitPoints
	{
		get => _takeProfitPoints.Value;
		set => _takeProfitPoints.Value = value;
	}

	public DailyStpEntryFrameStrategy()
	{
		_candleType = Param(nameof(CandleType), TimeSpan.FromMinutes(5).TimeFrame())
			.SetDisplay("Candle Type", "Time-frame for monitoring", "General");

		_stopLossPoints = Param(nameof(StopLossPoints), 80m)
			.SetNotNegative()
			.SetDisplay("Stop-Loss (points)", "Stop-loss distance", "Risk");

		_takeProfitPoints = Param(nameof(TakeProfitPoints), 200m)
			.SetNotNegative()
			.SetDisplay("Take-Profit (points)", "Take-profit distance", "Risk");
	}

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

	protected override void OnReseted()
	{
		base.OnReseted();
		_pipSize = 0m;
		_stopLossOffset = 0m;
		_takeProfitOffset = 0m;
		_previousDayHigh = null;
		_previousDayLow = null;
		_currentDayHigh = 0m;
		_currentDayLow = 0m;
		_currentTradingDay = null;
		_tradedToday = false;
		_entryPrice = 0m;
		_longStop = null;
		_longTake = null;
		_shortStop = null;
		_shortTake = null;
	}

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

		_pipSize = Security?.PriceStep ?? 0.01m;
		if (_pipSize <= 0) _pipSize = 0.01m;

		_stopLossOffset = StopLossPoints * _pipSize;
		_takeProfitOffset = TakeProfitPoints * _pipSize;

		_previousDayHigh = null;
		_previousDayLow = null;
		_currentTradingDay = null;
		_tradedToday = false;

		var subscription = SubscribeCandles(CandleType);
		subscription
			.Bind(ProcessCandle)
			.Start();
	}

	private void ProcessCandle(ICandleMessage candle)
	{
		if (candle.State != CandleStates.Finished)
			return;

		var date = candle.OpenTime.Date;

		// Track daily high/low
		if (_currentTradingDay != date)
		{
			// Save previous day's range
			if (_currentTradingDay != null)
			{
				_previousDayHigh = _currentDayHigh;
				_previousDayLow = _currentDayLow;
			}

			_currentTradingDay = date;
			_currentDayHigh = candle.HighPrice;
			_currentDayLow = candle.LowPrice;
			_tradedToday = false;
		}
		else
		{
			_currentDayHigh = Math.Max(_currentDayHigh, candle.HighPrice);
			_currentDayLow = Math.Min(_currentDayLow, candle.LowPrice);
		}

		// Manage existing position
		ManagePosition(candle);

		// Check for breakout entries
		if (_previousDayHigh is null || _previousDayLow is null)
			return;

		if (_tradedToday || Position != 0)
			return;

		var close = candle.ClosePrice;

		// Breakout above previous day high => buy
		if (close > _previousDayHigh.Value)
		{
			_entryPrice = close;
			_longStop = _stopLossOffset > 0 ? close - _stopLossOffset : null;
			_longTake = _takeProfitOffset > 0 ? close + _takeProfitOffset : null;
			_shortStop = null;
			_shortTake = null;
			BuyMarket();
			_tradedToday = true;
		}
		// Breakout below previous day low => sell
		else if (close < _previousDayLow.Value)
		{
			_entryPrice = close;
			_shortStop = _stopLossOffset > 0 ? close + _stopLossOffset : null;
			_shortTake = _takeProfitOffset > 0 ? close - _takeProfitOffset : null;
			_longStop = null;
			_longTake = null;
			SellMarket();
			_tradedToday = true;
		}
	}

	private void ManagePosition(ICandleMessage candle)
	{
		if (Position > 0)
		{
			if (_longStop is decimal stop && candle.LowPrice <= stop)
			{
				SellMarket();
				_longStop = null;
				_longTake = null;
				return;
			}
			if (_longTake is decimal take && candle.HighPrice >= take)
			{
				SellMarket();
				_longStop = null;
				_longTake = null;
			}
		}
		else if (Position < 0)
		{
			if (_shortStop is decimal stop && candle.HighPrice >= stop)
			{
				BuyMarket();
				_shortStop = null;
				_shortTake = null;
				return;
			}
			if (_shortTake is decimal take && candle.LowPrice <= take)
			{
				BuyMarket();
				_shortStop = null;
				_shortTake = null;
			}
		}
	}
}