Открыть на GitHub

Стратегия Open Time Two

Обзор

Open Time Two Strategy автоматизирует торговлю по расписанию и позволяет вести два независимых торговых интервала в течение дня. Для каждого интервала задаются направление, параметры риск-менеджмента и необязательное окно принудительного закрытия. Реализация повторяет исходный советник MetaTrader, но использует высокоуровневые API StockSharp, свечные данные и систему StrategyParam.

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

  1. Окна закрытия. Для каждого интервала можно активировать собственное окно закрытия. Когда время свечи попадает в окно (старт + глобальная продолжительность), стратегия закрывает позиции этого интервала и сбрасывает состояние.
  2. Сопровождение трейлинг-стопа. Если заданы положительные величины трейлинг-стопа и шага, стратегия на каждой закрытой свече проверяет, прошла ли цена минимум TrailingStop + TrailingStep в прибыльную сторону. Тогда стоп переносится вперёд на расстояние TrailingStop, при этом шаг гарантирует отсутствие лишних обновлений.
  3. Контроль стоп-лосса и тейк-профита. Для каждого интервала определены собственные расстояния в пунктах. На каждой свече максимум/минимум сравниваются с уровнями; при пробитии позиция интервала закрывается.
  4. Фильтр по дням недели. Торговля ведётся только в разрешённые дни. Если текущая свеча относится к запрещённому дню, новые сделки не открываются.
  5. Окна открытия. У каждого интервала есть своё начало и конец открытия. Глобальный параметр продолжительности расширяет окно за пределы времени окончания. Пока окно активно и интервал свободен, стратегия открывает рыночный ордер в заданном направлении.
  6. Синхронизация позиции. Активные интервалы формируют целевую суммарную позицию. Стратегия вызывает BuyMarket или SellMarket, чтобы реальная позиция совпадала с суммой позиций интервалов. Каждый интервал хранит цену входа, уровни стоп/тейк и состояние трейлинг-стопа.

Описание параметров

  • Close Window #1 / Close Window #2 – включение окон принудительного закрытия для каждого интервала.
  • Close Start #1 / Close Start #2 – локальное время начала соответствующих окон закрытия.
  • Trailing Stop / Trailing Step – расстояние трейлинг-стопа и шаг в пунктах. Оба параметра должны быть больше нуля для активации сопровождения.
  • Trade Monday … Trade Friday – фильтры по дням недели, требуется оставить хотя бы один включённый день.
  • Open Start #1 / Open End #1 / Open Start #2 / Open End #2 – границы окон открытия. Конец окна продлевается на время, указанное в Duration.
  • Window Duration – дополнительная длительность, прибавляемая к окончаниям окон открытия и закрытия.
  • Direction #1 / Direction #2 – направление торговли (true – покупка, false – продажа) по каждому интервалу.
  • Trade Volume – объём ордеров для каждого интервала, совпадает для обеих сессий как в оригинальном советнике.
  • Stop Loss #1 / Take Profit #1 / Stop Loss #2 / Take Profit #2 – расстояния до стоп-лосса и тейк-профита в пунктах. Ноль отключает уровень.
  • Candle Type – тип свечей, по которым принимаются решения. Все проверки выполняются после закрытия свечи.

Особенности риск-менеджмента

  • Пункты переводятся в цену через минимальный шаг цены. Для инструментов с тремя или пятью знаками шаг умножается на десять, что повторяет определение пункта в MetaTrader.
  • Логика трейлинг-стопа общая для двух интервалов, а уровни стоп/тейк работают независимо.
  • При срабатывании стопа или трейлинга интервал сбрасывается, что позволяет снова открыться внутри того же окна.

Ограничения и примечания

  • StockSharp использует неттинговую модель. Если направления интервалов противоположны, итоговая позиция будет обнуляться, и удерживать хедж одновременно не получится. Для полноценного хеджирования нужен портфель с поддержкой хедж-режима.
  • Решения принимаются на основе выбранного таймфрейма свечей, поэтому чем больше период, тем медленнее реакция по сравнению с тиковым исполнением MetaTrader.
  • Сравнения времени выполняются в локальной временной зоне. Необходимо следить за синхронизацией времени терминала и биржи.

Рекомендации по использованию

  • Подбирайте тип свечей в соответствии с требуемой точностью расписания (например, минутные свечи для минутных окон).
  • Сочетайте фильтр по дням недели и окна закрытия, чтобы избегать переноса позиций через нежелательные сессии.
  • Используйте встроенные параметры StrategyParam для оптимизации – ключевые поля уже помечены SetCanOptimize.
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 strategy that opens up to two independent sessions with individual direction and risk management.
/// Supports configurable opening windows, optional forced closing windows, pip based stops and trailing logic.
/// </summary>
public class OpenTimeTwoStrategy : Strategy
{
	private readonly StrategyParam<int> _secondsInDay;

	private readonly StrategyParam<bool> _useClosingWindowOne;
	private readonly StrategyParam<TimeSpan> _closeWindowOneStart;
	private readonly StrategyParam<bool> _useClosingWindowTwo;
	private readonly StrategyParam<TimeSpan> _closeWindowTwoStart;
	private readonly StrategyParam<decimal> _trailingStopPips;
	private readonly StrategyParam<decimal> _trailingStepPips;
	private readonly StrategyParam<bool> _tradeOnMonday;
	private readonly StrategyParam<bool> _tradeOnTuesday;
	private readonly StrategyParam<bool> _tradeOnWednesday;
	private readonly StrategyParam<bool> _tradeOnThursday;
	private readonly StrategyParam<bool> _tradeOnFriday;
	private readonly StrategyParam<TimeSpan> _intervalOneOpenStart;
	private readonly StrategyParam<TimeSpan> _intervalOneOpenEnd;
	private readonly StrategyParam<TimeSpan> _intervalTwoOpenStart;
	private readonly StrategyParam<TimeSpan> _intervalTwoOpenEnd;
	private readonly StrategyParam<TimeSpan> _duration;
	private readonly StrategyParam<bool> _intervalOneBuy;
	private readonly StrategyParam<bool> _intervalTwoBuy;
	private readonly StrategyParam<decimal> _tradeVolume;
	private readonly StrategyParam<decimal> _stopLossOnePips;
	private readonly StrategyParam<decimal> _takeProfitOnePips;
	private readonly StrategyParam<decimal> _stopLossTwoPips;
	private readonly StrategyParam<decimal> _takeProfitTwoPips;
	private readonly StrategyParam<DataType> _candleType;

	private readonly IntervalState _intervalOne = new();
	private readonly IntervalState _intervalTwo = new();

	private decimal _pipSize;

	/// <summary>
	/// Use closing window for the first interval.
	/// </summary>
	public bool UseClosingWindowOne
	{
		get => _useClosingWindowOne.Value;
		set => _useClosingWindowOne.Value = value;
	}

	/// <summary>
	/// Closing window start time for the first interval.
	/// </summary>
	public TimeSpan CloseWindowOneStart
	{
		get => _closeWindowOneStart.Value;
		set => _closeWindowOneStart.Value = value;
	}

	/// <summary>
	/// Use closing window for the second interval.
	/// </summary>
	public bool UseClosingWindowTwo
	{
		get => _useClosingWindowTwo.Value;
		set => _useClosingWindowTwo.Value = value;
	}

	/// <summary>
	/// Closing window start time for the second interval.
	/// </summary>
	public TimeSpan CloseWindowTwoStart
	{
		get => _closeWindowTwoStart.Value;
		set => _closeWindowTwoStart.Value = value;
	}

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

	/// <summary>
	/// Trailing step distance in pips.
	/// </summary>
	public decimal TrailingStepPips
	{
		get => _trailingStepPips.Value;
		set => _trailingStepPips.Value = value;
	}

	/// <summary>
	/// Enable trading on Monday.
	/// </summary>
	public bool TradeOnMonday
	{
		get => _tradeOnMonday.Value;
		set => _tradeOnMonday.Value = value;
	}

	/// <summary>
	/// Enable trading on Tuesday.
	/// </summary>
	public bool TradeOnTuesday
	{
		get => _tradeOnTuesday.Value;
		set => _tradeOnTuesday.Value = value;
	}

	/// <summary>
	/// Enable trading on Wednesday.
	/// </summary>
	public bool TradeOnWednesday
	{
		get => _tradeOnWednesday.Value;
		set => _tradeOnWednesday.Value = value;
	}

	/// <summary>
	/// Enable trading on Thursday.
	/// </summary>
	public bool TradeOnThursday
	{
		get => _tradeOnThursday.Value;
		set => _tradeOnThursday.Value = value;
	}

	/// <summary>
	/// Enable trading on Friday.
	/// </summary>
	public bool TradeOnFriday
	{
		get => _tradeOnFriday.Value;
		set => _tradeOnFriday.Value = value;
	}

	/// <summary>
	/// Opening window start for the first interval.
	/// </summary>
	public TimeSpan IntervalOneOpenStart
	{
		get => _intervalOneOpenStart.Value;
		set => _intervalOneOpenStart.Value = value;
	}

	/// <summary>
	/// Opening window end for the first interval.
	/// </summary>
	public TimeSpan IntervalOneOpenEnd
	{
		get => _intervalOneOpenEnd.Value;
		set => _intervalOneOpenEnd.Value = value;
	}

	/// <summary>
	/// Opening window start for the second interval.
	/// </summary>
	public TimeSpan IntervalTwoOpenStart
	{
		get => _intervalTwoOpenStart.Value;
		set => _intervalTwoOpenStart.Value = value;
	}

	/// <summary>
	/// Opening window end for the second interval.
	/// </summary>
	public TimeSpan IntervalTwoOpenEnd
	{
		get => _intervalTwoOpenEnd.Value;
		set => _intervalTwoOpenEnd.Value = value;
	}

	/// <summary>
	/// Extra duration added to each opening and closing window.
	/// </summary>
	public TimeSpan Duration
	{
		get => _duration.Value;
		set => _duration.Value = value;
	}

	/// <summary>
	/// Total number of seconds considered a full trading day.
	/// </summary>
	public int SecondsInDay
	{
		get => _secondsInDay.Value;
		set => _secondsInDay.Value = value;
	}

	/// <summary>
	/// Trade direction for interval one (true for buy, false for sell).
	/// </summary>
	public bool IntervalOneBuy
	{
		get => _intervalOneBuy.Value;
		set => _intervalOneBuy.Value = value;
	}

	/// <summary>
	/// Trade direction for interval two (true for buy, false for sell).
	/// </summary>
	public bool IntervalTwoBuy
	{
		get => _intervalTwoBuy.Value;
		set => _intervalTwoBuy.Value = value;
	}

	/// <summary>
	/// Trade volume for each interval.
	/// </summary>
	public decimal TradeVolume
	{
		get => _tradeVolume.Value;
		set => _tradeVolume.Value = value;
	}

	/// <summary>
	/// Stop loss distance for interval one in pips.
	/// </summary>
	public decimal StopLossOnePips
	{
		get => _stopLossOnePips.Value;
		set => _stopLossOnePips.Value = value;
	}

	/// <summary>
	/// Take profit distance for interval one in pips.
	/// </summary>
	public decimal TakeProfitOnePips
	{
		get => _takeProfitOnePips.Value;
		set => _takeProfitOnePips.Value = value;
	}

	/// <summary>
	/// Stop loss distance for interval two in pips.
	/// </summary>
	public decimal StopLossTwoPips
	{
		get => _stopLossTwoPips.Value;
		set => _stopLossTwoPips.Value = value;
	}

	/// <summary>
	/// Take profit distance for interval two in pips.
	/// </summary>
	public decimal TakeProfitTwoPips
	{
		get => _takeProfitTwoPips.Value;
		set => _takeProfitTwoPips.Value = value;
	}

	/// <summary>
	/// Candle type used as a driver for the strategy.
	/// </summary>
	public DataType CandleType
	{
		get => _candleType.Value;
		set => _candleType.Value = value;
	}

	/// <summary>
	/// Initializes a new instance of the strategy.
	/// </summary>
	public OpenTimeTwoStrategy()
	{
		_useClosingWindowOne = Param(nameof(UseClosingWindowOne), true)
			.SetDisplay("Close Window #1", "Enable closing window for interval #1", "Closing")
			;

		_closeWindowOneStart = Param(nameof(CloseWindowOneStart), new TimeSpan(19, 50, 0))
			.SetDisplay("Close Start #1", "Start time for closing window #1", "Closing");

		_useClosingWindowTwo = Param(nameof(UseClosingWindowTwo), true)
			.SetDisplay("Close Window #2", "Enable closing window for interval #2", "Closing")
			;

		_closeWindowTwoStart = Param(nameof(CloseWindowTwoStart), new TimeSpan(23, 20, 0))
			.SetDisplay("Close Start #2", "Start time for closing window #2", "Closing");

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

		_trailingStepPips = Param(nameof(TrailingStepPips), 3m)
			.SetDisplay("Trailing Step", "Trailing step distance in pips", "Risk")
			.SetRange(0m, 200m)
			;

		_tradeOnMonday = Param(nameof(TradeOnMonday), true)
			.SetDisplay("Trade Monday", "Allow trading on Monday", "Schedule")
			;

		_tradeOnTuesday = Param(nameof(TradeOnTuesday), true)
			.SetDisplay("Trade Tuesday", "Allow trading on Tuesday", "Schedule")
			;

		_tradeOnWednesday = Param(nameof(TradeOnWednesday), true)
			.SetDisplay("Trade Wednesday", "Allow trading on Wednesday", "Schedule")
			;

		_tradeOnThursday = Param(nameof(TradeOnThursday), true)
			.SetDisplay("Trade Thursday", "Allow trading on Thursday", "Schedule")
			;

		_tradeOnFriday = Param(nameof(TradeOnFriday), true)
			.SetDisplay("Trade Friday", "Allow trading on Friday", "Schedule")
			;

		_intervalOneOpenStart = Param(nameof(IntervalOneOpenStart), new TimeSpan(9, 30, 0))
			.SetDisplay("Open Start #1", "Opening window start for interval #1", "Opening");

		_intervalOneOpenEnd = Param(nameof(IntervalOneOpenEnd), new TimeSpan(14, 0, 0))
			.SetDisplay("Open End #1", "Opening window end for interval #1", "Opening");

		_intervalTwoOpenStart = Param(nameof(IntervalTwoOpenStart), TimeSpan.Zero)
			.SetDisplay("Open Start #2", "Opening window start for interval #2", "Opening");

		_intervalTwoOpenEnd = Param(nameof(IntervalTwoOpenEnd), TimeSpan.Zero)
			.SetDisplay("Open End #2", "Opening window end for interval #2", "Opening");

		_duration = Param(nameof(Duration), TimeSpan.FromSeconds(30))
			.SetDisplay("Window Duration", "Extra duration added to opening/closing windows", "Opening")
			.SetRange(TimeSpan.Zero, TimeSpan.FromHours(1));

		_secondsInDay = Param(nameof(SecondsInDay), 24 * 60 * 60)
			.SetGreaterThanZero()
			.SetDisplay("Seconds In Day", "Total number of seconds in a trading day", "Opening");

		_intervalOneBuy = Param(nameof(IntervalOneBuy), true)
			.SetDisplay("Direction #1", "Trade direction for interval #1 (Buy=true)", "Opening")
			;

		_intervalTwoBuy = Param(nameof(IntervalTwoBuy), true)
			.SetDisplay("Direction #2", "Trade direction for interval #2 (Buy=true)", "Opening")
			;

		_tradeVolume = Param(nameof(TradeVolume), 0.1m)
			.SetDisplay("Trade Volume", "Volume for each interval", "Risk")
			.SetRange(0.01m, 100m)
			;

		_stopLossOnePips = Param(nameof(StopLossOnePips), 30m)
			.SetDisplay("Stop Loss #1", "Stop loss for interval #1 (pips)", "Risk")
			.SetRange(0m, 1000m)
			;

		_takeProfitOnePips = Param(nameof(TakeProfitOnePips), 90m)
			.SetDisplay("Take Profit #1", "Take profit for interval #1 (pips)", "Risk")
			.SetRange(0m, 2000m)
			;

		_stopLossTwoPips = Param(nameof(StopLossTwoPips), 10m)
			.SetDisplay("Stop Loss #2", "Stop loss for interval #2 (pips)", "Risk")
			.SetRange(0m, 1000m)
			;

		_takeProfitTwoPips = Param(nameof(TakeProfitTwoPips), 35m)
			.SetDisplay("Take Profit #2", "Take profit for interval #2 (pips)", "Risk")
			.SetRange(0m, 2000m)
			;

		_candleType = Param(nameof(CandleType), TimeSpan.FromHours(2).TimeFrame())
			.SetDisplay("Candle Type", "Base candle type driving decisions", "General");
	}

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

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

		_pipSize = 0m;
		ResetInterval(_intervalOne);
		ResetInterval(_intervalTwo);
	}

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

		var decimals = Security?.Decimals ?? 0;
		var adjust = decimals is 3 or 5 ? 10m : 1m;
		var step = Security?.PriceStep ?? 1m;
		_pipSize = step * adjust;

		if (_pipSize <= 0m)
		{
			_pipSize = 1m;
		}

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

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

		var localTime = candle.OpenTime.ToLocalTime();
		var timeOfDay = localTime.TimeOfDay;

		if (UseClosingWindowOne && IsWithinSimpleWindow(timeOfDay, CloseWindowOneStart, Duration))
		{
			ExitInterval(_intervalOne);
		}

		if (UseClosingWindowTwo && IsWithinSimpleWindow(timeOfDay, CloseWindowTwoStart, Duration))
		{
			ExitInterval(_intervalTwo);
		}

		if (TrailingStopPips > 0m && TrailingStepPips > 0m)
		{
			UpdateTrailingStops(candle);
		}

		CheckRiskControls(_intervalOne, candle);
		CheckRiskControls(_intervalTwo, candle);

		if (!IsTradingDay(localTime.DayOfWeek))
		{
			return;
		}

		var inFirstWindow = IsWithinOpeningWindow(timeOfDay, IntervalOneOpenStart, IntervalOneOpenEnd);
		if (inFirstWindow)
		{
			TryOpenInterval(_intervalOne, IntervalOneBuy, StopLossOnePips, TakeProfitOnePips, candle.ClosePrice);
		}

		var inSecondWindow = IsWithinOpeningWindow(timeOfDay, IntervalTwoOpenStart, IntervalTwoOpenEnd);
		if (inSecondWindow)
		{
			TryOpenInterval(_intervalTwo, IntervalTwoBuy, StopLossTwoPips, TakeProfitTwoPips, candle.ClosePrice);
		}
	}

	private void TryOpenInterval(IntervalState state, bool isBuy, decimal stopLossPips, decimal takeProfitPips, decimal referencePrice)
	{
		if (state.IsActive)
		{
			return;
		}

		if (TradeVolume <= 0m)
		{
			return;
		}

		var direction = isBuy ? 1 : -1;
		var stopDistance = stopLossPips > 0m ? stopLossPips * _pipSize : 0m;
		var takeDistance = takeProfitPips > 0m ? takeProfitPips * _pipSize : 0m;

		decimal? stopPrice = null;
		decimal? takePrice = null;

		if (direction > 0)
		{
			if (stopDistance > 0m)
			{
				stopPrice = referencePrice - stopDistance;
			}

			if (takeDistance > 0m)
			{
				takePrice = referencePrice + takeDistance;
			}
		}
		else
		{
			if (stopDistance > 0m)
			{
				stopPrice = referencePrice + stopDistance;
			}

			if (takeDistance > 0m)
			{
				takePrice = referencePrice - takeDistance;
			}
		}

		state.IsActive = true;
		state.Direction = direction;
		state.EntryPrice = referencePrice;
		state.StopLossPrice = stopPrice;
		state.TakeProfitPrice = takePrice;
		state.TrailingStopPrice = null;

		SyncPosition();
	}

	private void UpdateTrailingStops(ICandleMessage candle)
	{
		var trailingDistance = TrailingStopPips * _pipSize;
		var stepDistance = TrailingStepPips * _pipSize;

		if (trailingDistance <= 0m || stepDistance <= 0m)
		{
			return;
		}

		UpdateTrailingForInterval(_intervalOne, candle, trailingDistance, stepDistance);
		UpdateTrailingForInterval(_intervalTwo, candle, trailingDistance, stepDistance);
	}

	private static void ResetInterval(IntervalState state)
	{
		state.IsActive = false;
		state.Direction = 0;
		state.EntryPrice = 0m;
		state.StopLossPrice = null;
		state.TakeProfitPrice = null;
		state.TrailingStopPrice = null;
	}

	private void UpdateTrailingForInterval(IntervalState state, ICandleMessage candle, decimal trailingDistance, decimal stepDistance)
	{
		if (!state.IsActive)
		{
			return;
		}

		if (state.Direction > 0)
		{
			var profit = candle.ClosePrice - state.EntryPrice;
			if (profit <= trailingDistance + stepDistance)
			{
				return;
			}

			var proposed = candle.ClosePrice - trailingDistance;

			if (state.TrailingStopPrice is null || proposed - state.TrailingStopPrice.Value >= stepDistance)
			{
				state.TrailingStopPrice = state.TrailingStopPrice is null
					? proposed
					: Math.Max(state.TrailingStopPrice.Value, proposed);
			}
		}
		else
		{
			var profit = state.EntryPrice - candle.ClosePrice;
			if (profit <= trailingDistance + stepDistance)
			{
				return;
			}

			var proposed = candle.ClosePrice + trailingDistance;

			if (state.TrailingStopPrice is null || state.TrailingStopPrice.Value - proposed >= stepDistance)
			{
				state.TrailingStopPrice = state.TrailingStopPrice is null
					? proposed
					: Math.Min(state.TrailingStopPrice.Value, proposed);
			}
		}
	}

	private void CheckRiskControls(IntervalState state, ICandleMessage candle)
	{
		if (!state.IsActive)
		{
			return;
		}

		if (state.Direction > 0)
		{
			if (state.StopLossPrice is decimal sl && candle.LowPrice <= sl)
			{
				ExitInterval(state);
				return;
			}

			if (state.TrailingStopPrice is decimal trail && candle.LowPrice <= trail)
			{
				ExitInterval(state);
				return;
			}

			if (state.TakeProfitPrice is decimal tp && candle.HighPrice >= tp)
			{
				ExitInterval(state);
			}
		}
		else
		{
			if (state.StopLossPrice is decimal sl && candle.HighPrice >= sl)
			{
				ExitInterval(state);
				return;
			}

			if (state.TrailingStopPrice is decimal trail && candle.HighPrice >= trail)
			{
				ExitInterval(state);
				return;
			}

			if (state.TakeProfitPrice is decimal tp && candle.LowPrice <= tp)
			{
				ExitInterval(state);
			}
		}
	}

	private void ExitInterval(IntervalState state)
	{
		if (!state.IsActive)
		{
			return;
		}

		ResetInterval(state);
		SyncPosition();
	}

	private void SyncPosition()
	{
		var target = GetTargetPosition();
		var diff = target - Position;

		if (diff == 0m)
		{
			return;
		}

		if (diff > 0m)
		{
			BuyMarket(diff);
		}
		else
		{
			SellMarket(-diff);
		}
	}

	private new decimal GetTargetPosition()
	{
		var target = 0m;

		if (_intervalOne.IsActive)
		{
			target += _intervalOne.Direction * TradeVolume;
		}

		if (_intervalTwo.IsActive)
		{
			target += _intervalTwo.Direction * TradeVolume;
		}

		return target;
	}

	private bool IsTradingDay(DayOfWeek day)
	{
		return day switch
		{
			DayOfWeek.Monday => TradeOnMonday,
			DayOfWeek.Tuesday => TradeOnTuesday,
			DayOfWeek.Wednesday => TradeOnWednesday,
			DayOfWeek.Thursday => TradeOnThursday,
			DayOfWeek.Friday => TradeOnFriday,
			_ => true,
		};
	}

	private bool IsWithinOpeningWindow(TimeSpan current, TimeSpan start, TimeSpan end)
	{
		var startSec = ToSeconds(start);
		var endSec = ToSeconds(end);
		var durationSec = ToSeconds(Duration);
		var currentSec = ToSeconds(current);

		if (endSec <= startSec)
		{
			return false;
		}

		var finalEnd = Math.Min(SecondsInDay, endSec + durationSec);
		return currentSec >= startSec && currentSec < finalEnd;
	}

	private bool IsWithinSimpleWindow(TimeSpan current, TimeSpan start, TimeSpan length)
	{
		var startSec = ToSeconds(start);
		var currentSec = ToSeconds(current);
		var lengthSec = Math.Max(0, ToSeconds(length));
		var endSec = startSec + lengthSec;

		if (lengthSec == 0)
		{
			return currentSec == startSec;
		}

		if (endSec <= SecondsInDay)
		{
			return currentSec >= startSec && currentSec < endSec;
		}

		endSec -= SecondsInDay;
		return currentSec >= startSec || currentSec < endSec;
	}

	private int ToSeconds(TimeSpan time)
	{
		var value = time.TotalSeconds;

		if (value < 0)
		{
			return 0;
		}

		if (value > SecondsInDay)
		{
			return SecondsInDay;
		}

		return (int)Math.Floor(value);
	}

	private sealed class IntervalState : IEquatable<IntervalState>
	{
		public bool IsActive;
		public int Direction;
		public decimal EntryPrice;
		public decimal? StopLossPrice;
		public decimal? TakeProfitPrice;
		public decimal? TrailingStopPrice;

		public bool Equals(IntervalState other)
		{
			return other != null
				&& IsActive == other.IsActive
				&& Direction == other.Direction
				&& EntryPrice == other.EntryPrice
				&& StopLossPrice == other.StopLossPrice
				&& TakeProfitPrice == other.TakeProfitPrice
				&& TrailingStopPrice == other.TrailingStopPrice;
		}

		public override bool Equals(object obj)
		{
			return obj is IntervalState other && Equals(other);
		}

		public override int GetHashCode()
		{
			return HashCode.Combine(IsActive, Direction, EntryPrice, StopLossPrice, TakeProfitPrice, TrailingStopPrice);
		}
	}
}