Ver en GitHub

Estrategia de Apertura de Tiempo para Dos Sesiones

Descripción general

La Estrategia de Apertura de Tiempo para Dos Sesiones automatiza un plan de trading programado por tiempo que puede gestionar dos sesiones independientes durante el día de trading. Cada sesión puede configurarse con su propia dirección, parámetros de riesgo y ventana opcional de cierre forzado. La conversión sigue la lógica original de MetaTrader pero se apoya en las API de alto nivel de StockSharp, velas y objetos de parámetros para configuración y optimización.

Lógica de trading

  1. Ventanas de cierre de sesión. Para cada intervalo se puede definir una ventana de cierre opcional. Cuando el tiempo de la vela cae dentro de la ventana (tiempo de inicio más la duración global), la estrategia cierra forzosamente el intervalo correspondiente y limpia su estado.
  2. Mantenimiento del trailing stop. Si el trailing stop y el paso son positivos, la lógica de trailing monitorea las velas completadas. Una vez que el precio se mueve a favor de la posición al menos (TrailingStop + TrailingStep), el stop avanza en TrailingStop. Las actualizaciones requieren la distancia del paso para evitar recálculos ruidosos.
  3. Verificaciones de stop loss y take profit. Cada intervalo tiene distancias independientes de stop loss y take profit medidas en pips. En cada vela completada, los precios altos/bajos se comparan con estos niveles, cerrando el intervalo inmediatamente cuando se supera un nivel.
  4. Filtro por día de semana. El trading procede solo en los días de semana habilitados. Si la vela actual pertenece a un día deshabilitado, no se abren nuevas operaciones.
  5. Ventanas de apertura. Cada intervalo tiene una ventana de apertura con tiempos de inicio y fin. El valor de duración global extiende la ventana por el lado de finalización. Cuando una ventana está activa y el intervalo no tiene posición abierta, la estrategia abre una orden de mercado en la dirección configurada.
  6. Sincronización de posición. Los intervalos activos contribuyen a una posición neta objetivo. La estrategia llama a BuyMarket o SellMarket para que la posición neta coincida con la suma de exposiciones de intervalos. Cada intervalo mantiene su propio precio de entrada, niveles de stop/take y estado de trailing stop.

Referencia de parámetros

  • Close Window #1 / Close Window #2 – habilitar o deshabilitar las ventanas de cierre forzado dedicadas para cada intervalo.
  • Close Start #1 / Close Start #2 – hora local del día en que comienza la ventana de cierre para cada intervalo.
  • Trailing Stop / Trailing Step – distancias en pips usadas por la lógica de trailing. Ambas deben ser mayores que cero para activar el trailing.
  • Trade Monday … Trade Friday – filtros por día de semana. Al menos un día debe permanecer habilitado para permitir el trading.
  • Open Start #1 / Open End #1 / Open Start #2 / Open End #2 – límites de ventana de apertura para cada intervalo. La duración global extiende la ventana más allá del tiempo de fin.
  • Window Duration – intervalo de tiempo extra añadido a las ventanas de apertura y cierre.
  • Direction #1 / Direction #2 – indicadores de dirección de trade (true para largo, false para corto) para cada intervalo.
  • Trade Volume – volumen de orden de mercado para cada intervalo. La estrategia asume volumen idéntico para ambos intervalos como en el asesor experto original.
  • Stop Loss #1 / Take Profit #1 / Stop Loss #2 / Take Profit #2 – distancias en pips para los niveles de stop loss y take profit por intervalo. Un valor de cero deshabilita el nivel correspondiente.
  • Candle Type – serie de velas utilizada para impulsar la estrategia. Todos los cálculos, incluidas ventanas de tiempo y verificaciones de riesgo, se ejecutan cuando estas velas finalizan.

Detalles de gestión de riesgo

  • Las distancias en pips se convierten a unidades de precio usando el paso de precio del instrumento. Si el instrumento usa tres o cinco decimales, el paso se multiplica por diez para replicar la definición de pip de MetaTrader.
  • La lógica de trailing es compartida por ambos intervalos, mientras que los valores de stop loss y take profit permanecen independientes.
  • Cuando se activa el nivel de stop o trailing, el intervalo restablece su estado para poder reabrir dentro de la misma ventana si el tiempo lo permite.

Limitaciones y notas

  • StockSharp opera con un modelo de posición neta. Si el intervalo #1 y #2 están configurados con direcciones opuestas, la posición neta resultante se aplanará en lugar de mantener dos operaciones cubiertas abiertas simultáneamente. Use un portafolio con capacidad de cobertura si se requiere cobertura real.
  • Las decisiones se basan en la serie de velas seleccionada. Usar un marco temporal grande puede retrasar las reacciones comparado con la implementación basada en ticks de MetaTrader.
  • La estrategia espera que los relojes del exchange y del terminal estén sincronizados porque las comparaciones de hora del día se basan en hora local.

Consejos de uso

  • Configure el tipo de vela para que coincida con la granularidad temporal usada para el horario (p. ej., un minuto para control granular).
  • Combine el filtro de día y las ventanas de cierre para evitar llevar posiciones durante sesiones indeseables.
  • Optimice los parámetros a través de los objetos StrategyParam integrados; los campos clave ya tienen SetCanOptimize habilitado.
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);
		}
	}
}