Открыть на GitHub

Стратегия Open Time

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

Стратегия Open Time — это порт советника MetaTrader 5 OpenTime, построенного на временном расписании. Она отслеживает биржевое время по завершённым свечам и открывает сделки только внутри заданного окна. Дополнительно предусмотрено отдельное окно принудительного закрытия, опциональный трейлинг-стоп и базовые уровни стоп-лосса/тейк-профита, задаваемые в пунктах.

В StockSharp стратегия работает с нетто-позицией. Поэтому если новый сигнал противоречит текущему направлению, сначала закрывается противоположная позиция, а затем открывается новая сделка с указанным объёмом.

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

  1. Окно закрытия — при включённом параметре Use Close Window любая активная позиция закрывается, если текущее время попадает внутрь окна закрытия. До окончания окна новые сделки не инициируются.
  2. Обновление трейлинг-стопа — когда движение цены в сторону позиции превышает TrailingStop + TrailingStep пунктов, стоп подтягивается на расстояние TrailingStop. Это повторяет оригинальный алгоритм MT5, где стоп переносится только после минимального дополнительного смещения.
  3. Проверка рисков — на каждой закрытой свече проверяются уровни стоп-лосса и тейк-профита. При срабатывании любого из уровней позиция закрывается, внутренние переменные очищаются.
  4. Окно открытия — если текущий момент попадает в окно открытия, стратегия анализирует доступные направления:
    • При разрешённых покупках и отсутствии длинной позиции (или наличии короткой) отправляется рыночная покупка на заданный объём. Если ранее была открыта короткая позиция, дополнительно покупается объём для её покрытия.
    • При разрешённых продажах и отсутствии короткой позиции (или наличии длинной) отправляется рыночная продажа на заданный объём с учётом закрытия текущей длинной позиции.

В момент входа запоминаются цена сделки, а также уровни стопа и цели (если заданы). Эти значения используются трейлингом и блоком управления рисками.

Параметры

Параметр Значение по умолчанию Описание
Candle Type Свечи 1 минута Тип данных, по которым определяется время; логика выполняется только на закрытых свечах.
Use Close Window true Включает окно принудительного закрытия.
Close Hour / Close Minute 20:50 Начало окна закрытия. Час может принимать значения 0–24 (24 соответствует полуночи следующего дня).
Enable Trailing false Активирует логику трейлинг-стопа.
Trailing Stop 30 пунктов Расстояние между ценой и трейлинг-стопом. Пересчитывается в цену исходя из минимального шага котировки.
Trailing Step 3 пункта Минимальное дополнительное движение, после которого трейлинг переносится.
Trade Hour / Trade Minute 18:50 Начало окна для открытия позиций.
Duration 300 секунд Длительность окна открытия и закрытия.
Enable Sell / Enable Buy Sell = true, Buy = false Разрешённые направления торговли.
Volume 0.1 Объём заявки. При развороте добавляется объём для закрытия противоположной позиции.
Stop Loss 0 пунктов Начальный стоп-лосс. Нуль отключает фиксированный стоп и оставляет управление выходом трейлингу или окну закрытия.
Take Profit 0 пунктов Начальный тейк-профит. Нуль отключает фиксированную цель.

Реализация

  • Пересчёт пунктов выполняется через Security.PriceStep. Для инструментов с тремя или пятью знаками после запятой шаг умножается на десять, что соответствует логике MT5.
  • Для проверки стопов и целей используются экстремумы свечи (HighPrice и LowPrice), что позволяет приблизить реакцию к тиковому режиму, оставаясь на высокоуровневом API.
  • После закрытия позиции внутреннее состояние очищается, чтобы последующие сделки получали актуальные параметры.
  • Из-за неттингового режима StockSharp одновременные длинные и короткие позиции недоступны. При смене направления стратегия сначала закрывает текущую позицию и лишь затем открывает новую.

Рекомендации

  • Выбирайте таймфрейм свечей, соответствующий требуемой точности по времени. Для большинства сценариев достаточно 1-минутных свечей.
  • Окна открытия и закрытия используют общую длительность. Чтобы отключить одно из окон, установите длительность в ноль или выключите параметр Use Close Window.
  • Трейлинг-стоп активируется только после того, как цена пройдёт минимум Trailing Stop + Trailing Step пунктов от цены входа, полностью повторяя алгоритм оригинальной версии.
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>
/// Time based opening strategy with optional trailing stop logic.
/// </summary>
public class OpenTimeStrategy : Strategy
{
	private readonly StrategyParam<DataType> _candleType;
	private readonly StrategyParam<bool> _useCloseTime;
	private readonly StrategyParam<int> _closeHour;
	private readonly StrategyParam<int> _closeMinute;
	private readonly StrategyParam<bool> _enableTrailing;
	private readonly StrategyParam<int> _trailingStopPips;
	private readonly StrategyParam<int> _trailingStepPips;
	private readonly StrategyParam<int> _tradeHour;
	private readonly StrategyParam<int> _tradeMinute;
	private readonly StrategyParam<int> _durationSeconds;
	private readonly StrategyParam<bool> _enableSell;
	private readonly StrategyParam<bool> _enableBuy;
	private readonly StrategyParam<int> _stopLossPips;
	private readonly StrategyParam<int> _takeProfitPips;

	private decimal _pipSize;
	private decimal _stopOffset;
	private decimal _takeOffset;
	private decimal _trailOffset;
	private decimal _trailStep;

	private decimal? _longEntry;
	private decimal? _shortEntry;
	private decimal? _longStop;
	private decimal? _longTake;
	private decimal? _shortStop;
	private decimal? _shortTake;

	/// <summary>
	/// Initializes a new instance of the <see cref="OpenTimeStrategy"/> class.
	/// </summary>
	public OpenTimeStrategy()
	{
		_candleType = Param(nameof(CandleType), TimeSpan.FromHours(4).TimeFrame())
			.SetDisplay("Candle Type", "Candle subscription type", "General");
		_useCloseTime = Param(nameof(UseCloseTime), true)
			.SetDisplay("Use Close Window", "Enable automatic closing window", "Trading");
		_closeHour = Param(nameof(CloseHour), 20)
			.SetDisplay("Close Hour", "Hour for the closing window", "Trading");
		_closeMinute = Param(nameof(CloseMinute), 50)
			.SetDisplay("Close Minute", "Minute for the closing window", "Trading");
		_enableTrailing = Param(nameof(EnableTrailing), false)
			.SetDisplay("Enable Trailing", "Use trailing stop logic", "Risk");
		_trailingStopPips = Param(nameof(TrailingStopPips), 30)
			.SetDisplay("Trailing Stop", "Trailing stop distance in pips", "Risk");
		_trailingStepPips = Param(nameof(TrailingStepPips), 3)
			.SetDisplay("Trailing Step", "Additional move required to shift the trail", "Risk");
		_tradeHour = Param(nameof(TradeHour), 10)
			.SetDisplay("Trade Hour", "Hour to start opening positions", "Trading");
		_tradeMinute = Param(nameof(TradeMinute), 0)
			.SetDisplay("Trade Minute", "Minute to start opening positions", "Trading");
		_durationSeconds = Param(nameof(DurationSeconds), 18000)
			.SetDisplay("Duration", "Window length in seconds", "Trading");
		_enableSell = Param(nameof(EnableSell), true)
			.SetDisplay("Enable Sell", "Allow short entries", "Trading");
		_enableBuy = Param(nameof(EnableBuy), true)
			.SetDisplay("Enable Buy", "Allow long entries", "Trading");
		_stopLossPips = Param(nameof(StopLossPips), 500)
			.SetDisplay("Stop Loss", "Initial stop loss in pips", "Risk");
		_takeProfitPips = Param(nameof(TakeProfitPips), 1000)
			.SetDisplay("Take Profit", "Initial take profit in pips", "Risk");
	}

	/// <summary>
	/// Candle subscription type.
	/// </summary>
	public DataType CandleType
	{
		get => _candleType.Value;
		set => _candleType.Value = value;
	}

	/// <summary>
	/// Indicates whether automatic closing window is enabled.
	/// </summary>
	public bool UseCloseTime
	{
		get => _useCloseTime.Value;
		set => _useCloseTime.Value = value;
	}

	/// <summary>
	/// Hour of the closing window (0-24).
	/// </summary>
	public int CloseHour
	{
		get => _closeHour.Value;
		set => _closeHour.Value = value;
	}

	/// <summary>
	/// Minute of the closing window (0-59).
	/// </summary>
	public int CloseMinute
	{
		get => _closeMinute.Value;
		set => _closeMinute.Value = value;
	}

	/// <summary>
	/// Enables trailing stop logic.
	/// </summary>
	public bool EnableTrailing
	{
		get => _enableTrailing.Value;
		set => _enableTrailing.Value = value;
	}

	/// <summary>
	/// Trailing stop distance expressed in pips.
	/// </summary>
	public int TrailingStopPips
	{
		get => _trailingStopPips.Value;
		set => _trailingStopPips.Value = value;
	}

	/// <summary>
	/// Additional movement required to move the trailing stop.
	/// </summary>
	public int TrailingStepPips
	{
		get => _trailingStepPips.Value;
		set => _trailingStepPips.Value = value;
	}

	/// <summary>
	/// Hour when the strategy can open positions.
	/// </summary>
	public int TradeHour
	{
		get => _tradeHour.Value;
		set => _tradeHour.Value = value;
	}

	/// <summary>
	/// Minute when the strategy can open positions.
	/// </summary>
	public int TradeMinute
	{
		get => _tradeMinute.Value;
		set => _tradeMinute.Value = value;
	}

	/// <summary>
	/// Duration of the trading window in seconds.
	/// </summary>
	public int DurationSeconds
	{
		get => _durationSeconds.Value;
		set => _durationSeconds.Value = value;
	}

	/// <summary>
	/// Enables short entries.
	/// </summary>
	public bool EnableSell
	{
		get => _enableSell.Value;
		set => _enableSell.Value = value;
	}

	/// <summary>
	/// Enables long entries.
	/// </summary>
	public bool EnableBuy
	{
		get => _enableBuy.Value;
		set => _enableBuy.Value = value;
	}


	/// <summary>
	/// Initial stop loss distance in pips.
	/// </summary>
	public int StopLossPips
	{
		get => _stopLossPips.Value;
		set => _stopLossPips.Value = value;
	}

	/// <summary>
	/// Initial take profit distance in pips.
	/// </summary>
	public int TakeProfitPips
	{
		get => _takeProfitPips.Value;
		set => _takeProfitPips.Value = value;
	}

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

	/// <inheritdoc />
	protected override void OnReseted()
	{
		base.OnReseted();

		_pipSize = 0m;
		_stopOffset = 0m;
		_takeOffset = 0m;
		_trailOffset = 0m;
		_trailStep = 0m;

		ResetLongState();
		ResetShortState();
	}

	/// <inheritdoc />
	protected override void OnStarted2(DateTime time)
	{
		base.OnStarted2(time);

		// Convert pip-based inputs to absolute price offsets.
		_pipSize = CalculatePipSize();
		_stopOffset = StopLossPips * _pipSize;
		_takeOffset = TakeProfitPips * _pipSize;
		_trailOffset = TrailingStopPips * _pipSize;
		_trailStep = TrailingStepPips * _pipSize;

		// Subscribe to candle data used for time-based processing.
		var subscription = SubscribeCandles(CandleType);
		subscription.Bind(ProcessCandle).Start();

		var area = CreateChartArea();
		if (area != null)
		{
			DrawCandles(area, subscription);
			DrawOwnTrades(area);
		}

		// no protection
	}

	private void ProcessCandle(ICandleMessage candle)
	{
		// Work only with finished candles to avoid premature actions.
		if (candle.State != CandleStates.Finished)
			return;

		var now = candle.CloseTime;

		// Force-close any position during the configured closing window.
		if (UseCloseTime && IsWithinWindow(now, CloseHour, CloseMinute, DurationSeconds))
		{
			CloseActivePositions();
			return;
		}

		// Update trailing stops and exit if risk limits were exceeded.
		UpdateTrailingStops(candle);
		CheckRiskManagement(candle);

		// no bound indicators to check

		// Skip entries outside the trading window.
		if (!IsWithinWindow(now, TradeHour, TradeMinute, DurationSeconds))
			return;

		// Open or reverse long positions when buying is enabled.
		if (EnableBuy && Position <= 0)
		{
			if (Position < 0)
			{
				BuyMarket();
				ResetShortState();
			}

			BuyMarket();
			InitializeLongState(candle.ClosePrice);
		}
		else if (EnableSell && Position >= 0)
		{
			if (Position > 0)
			{
				SellMarket();
				ResetLongState();
			}

			SellMarket();
			InitializeShortState(candle.ClosePrice);
		}
	}

	private void UpdateTrailingStops(ICandleMessage candle)
	{
		if (!EnableTrailing || _trailOffset <= 0m)
			return;

		// Move the trailing stop for long positions once the minimal step is reached.
		if (Position > 0 && _longEntry.HasValue)
		{
			var distance = candle.ClosePrice - _longEntry.Value;
			if (distance > _trailOffset + _trailStep)
			{
				var triggerLevel = candle.ClosePrice - (_trailOffset + _trailStep);
				if (!_longStop.HasValue || _longStop.Value < triggerLevel)
					_longStop = candle.ClosePrice - _trailOffset;
			}
		}
		// Move the trailing stop for short positions in a symmetrical way.
		else if (Position < 0 && _shortEntry.HasValue)
		{
			var distance = _shortEntry.Value - candle.ClosePrice;
			if (distance > _trailOffset + _trailStep)
			{
				var triggerLevel = candle.ClosePrice + (_trailOffset + _trailStep);
				if (!_shortStop.HasValue || _shortStop.Value > triggerLevel)
					_shortStop = candle.ClosePrice + _trailOffset;
			}
		}
	}

	private void CheckRiskManagement(ICandleMessage candle)
	{
		if (Position > 0)
		{
			if (_longStop.HasValue && candle.LowPrice <= _longStop.Value)
			{
				SellMarket();
				ResetLongState();
				return;
			}

			if (_longTake.HasValue && candle.HighPrice >= _longTake.Value)
			{
				SellMarket();
				ResetLongState();
			}
		}
		else if (Position < 0)
		{
			if (_shortStop.HasValue && candle.HighPrice >= _shortStop.Value)
			{
				BuyMarket();
				ResetShortState();
				return;
			}

			if (_shortTake.HasValue && candle.LowPrice <= _shortTake.Value)
			{
				BuyMarket();
				ResetShortState();
			}
		}
	}

	private void CloseActivePositions()
	{
		// Flatten the portfolio and clear cached levels.
		if (Position > 0)
		{
			SellMarket();
			ResetLongState();
		}
		else if (Position < 0)
		{
			BuyMarket();
			ResetShortState();
		}
	}

	private void InitializeLongState(decimal price)
	{
		// Remember entry price and derived risk levels for long trades.
		_longEntry = price;
		_longStop = StopLossPips > 0 ? price - _stopOffset : null;
		_longTake = TakeProfitPips > 0 ? price + _takeOffset : null;
	}

	private void InitializeShortState(decimal price)
	{
		// Remember entry price and derived risk levels for short trades.
		_shortEntry = price;
		_shortStop = StopLossPips > 0 ? price + _stopOffset : null;
		_shortTake = TakeProfitPips > 0 ? price - _takeOffset : null;
	}

	private void ResetLongState()
	{
		_longEntry = null;
		_longStop = null;
		_longTake = null;
	}

	private void ResetShortState()
	{
		_shortEntry = null;
		_shortStop = null;
		_shortTake = null;
	}

	private decimal CalculatePipSize()
	{
		// Convert MT5-style pip values into absolute price units.
		var step = Security?.PriceStep ?? 1m;
		if (step <= 0m)
			return 1m;

		var decimals = CountDecimals(step);
		var multiplier = decimals == 3 || decimals == 5
			? 10m
			: 1m;
		return step * multiplier;
	}

	private static int CountDecimals(decimal value)
	{
		// Count decimal places by repeatedly shifting the decimal point.
		value = Math.Abs(value);
		var decimals = 0;
		while (value != Math.Truncate(value) && decimals < 10)
		{
			value *= 10m;
			decimals++;
		}
		return decimals;
	}

	private static bool IsWithinWindow(DateTimeOffset time, int hour, int minute, int durationSeconds)
	{
		if (durationSeconds <= 0)
			return false;

		var start = BuildReferenceTime(time, hour, minute);
		var end = start.AddSeconds(durationSeconds);
		return time >= start && time < end;
	}

	private static DateTimeOffset BuildReferenceTime(DateTimeOffset reference, int hour, int minute)
	{
		// Align the target time with the current trading day, allowing hour values above 23.
		var normalizedHour = hour;
		var day = new DateTimeOffset(reference.Year, reference.Month, reference.Day, 0, 0, 0, reference.Offset);

		while (normalizedHour >= 24)
		{
			normalizedHour -= 24;
			day = day.AddDays(1);
		}

		if (minute < 0)
			minute = 0;
		else if (minute > 59)
			minute = 59;

		return day.AddHours(normalizedHour).AddMinutes(minute);
	}
}