Auf GitHub ansehen

Exp Hans Indicator Wolkensystem Tm Plus Strategie

Überblick

Exp Hans Indicator Wolkensystem Tm Plus ist eine sessionbasierte Ausbruchsstrategie, die das Verhalten des ursprünglichen MQL5 Expert Advisors reproduziert. Der Algorithmus überwacht die Farbzustände des Hans-Indikators auf einem konfigurierbaren Zeitrahmen. Er eröffnet eine neue Position, nachdem ein bullischer (Farben 0/1) oder bärischer (Farben 3/4) Ausbruch endet und der Preis in den Kanal zurückkehrt. Die Implementierung hält alle Handelsentscheidungen bei geschlossenen Kerzen, verwendet pip-basierte Risikolimits und spiegelt die zeitbasierte Liquidationsregel der MQL-Version wider.

Die Strategie operiert auf einem einzigen Instrument/Kerzen-Feed-Paar, das von GetWorkingSecurities() bezogen wird. Alle Ordergrößen werden aus der Volume-Eigenschaft der Strategie und dem von den Parametern bereitgestellten Geldmanagement-Anteil abgeleitet.

Indikatorlogik

  1. Kerzenzeitstempel werden von der Broker-Zeit (LocalTimeZone) in die Zielzeitzone (DestinationTimeZone) konvertiert. Standardmäßig arbeitet das Skript mit GMT+4, was der Referenzimplementierung entspricht.
  2. Täglich werden zwei Londoner-Session-Bereiche gesammelt:
    • Bereich 1: 04:00–08:00 Zielzeit. Das Hoch/Tief dieses Zeitraums wird zum initialen Ausbruchskanal.
    • Bereich 2: 08:00–12:00 Zielzeit. Einmal abgeschlossen, ersetzt er den ersten Bereich für den Rest des Tages.
  3. Jeder Bereich wird um PipsForEntry Pips auf beiden Seiten erweitert. Ein Pip entspricht dem Instrumenten-PriceStep, multipliziert mit 10, wenn das Wertpapier 3 oder 5 Dezimalstellen hat (MetaTrader-artige Dezimalpips).
  4. Kerzenfarben werden genau wie im Indikator abgeleitet:
    • Schluss über dem oberen Band → Farbe 0 (bullischer Schluss) oder 1 (bärischer Schluss).
    • Schluss unter dem unteren Band → Farbe 4 (bärischer Schluss) oder 3 (bullischer Schluss).
    • Schluss innerhalb des Kanals → neutrale Farbe 2.

Handelsregeln

  • Einstieg: Wenn die vorherige geschlossene Kerze eine bullische Farbe (0/1) hatte und die neueste nicht bullisch ist, eröffnet die Strategie eine Long-Position (wenn aktiviert). Symmetrisch dazu löst eine vorherige bärische Farbe (3/4), gefolgt von einer neutralen/gegenteiligen Farbe, einen Short-Einstieg aus.
  • Ausstieg:
    • Direktionaler Ausstieg wenn die vorherige Farbe sich gegen die aktuelle Position wendet (0/1 für Shorts, 3/4 für Longs).
    • Optionaler zeitbasierter Ausstieg sobald die Haltezeit HoldingMinutes überschreitet.
    • Optionale Stop-Loss/Take-Profit-Niveaus in Punkten (StopLossPoints, TakeProfitPoints). Niveaus werden übersprungen, wenn das Wertpapier kein positives PriceStep offenlegt.
  • Ausstiege werden vor neuen Einstiegen verarbeitet, sodass eine Position geflacht wird, bevor eine Umkehrorder gesendet wird.

Parameter

Parameter Beschreibung Standard
MoneyManagement Anteil des Strategie-Volume pro Trade. Werte ≤ 0 verwenden das volle Volumen. 0.1
MoneyMode Platzhalter für die originalen Geldmanagement-Modi. Derzeit wird nur Lot angewendet. Lot
StopLossPoints / TakeProfitPoints Schutzstop und Gewinnziel in Punkten (Pips). Auf 0 setzen zum Deaktivieren. 1000 / 2000
DeviationPoints Maximale akzeptable Ausführungsabweichung in Punkten. Zur Kompatibilität vorhanden; von der StockSharp-Orderschicht nicht durchgesetzt. 10
AllowBuyEntries / AllowSellEntries Aktiviert Long-/Short-Einstiege. true
AllowBuyExits / AllowSellExits Aktiviert automatische Ausstiege für Long-/Short-Positionen. true
UseTimeExit Schaltet den zeitbasierten Liquidationsfilter ein. true
HoldingMinutes Maximale Haltezeit für eine Position in Minuten. 1500
PipsForEntry Pip-Offset, der über/unter den Ausbruchsbereichen hinzugefügt wird. 100
SignalBar Offset für geschlossene Kerzen für Signale. Verwenden Sie Werte ≥ 1 zur Übereinstimmung mit MT5-Logik. 1
LocalTimeZone Broker/Server-Zeitzone (Stunden von UTC). 0
DestinationTimeZone Zielzeitzone für Session-Grenzen. 4
CandleType Zeitrahmen für Hans-Berechnungen. 30m-Kerzen

Geldmanagement und Ausführung

  • Ordergröße = Volume * MoneyManagement, normalisiert auf den Instrumenten-VolumeStep. Wenn der berechnete Wert nicht-positiv ist, fällt die Logik auf einen Volumenschritt zurück.
  • Wenn ein Umkehrsignal erscheint, sendet die Strategie eine einzige Marktorder gleich dem neuen Volumen plus jede offene entgegengesetzte Menge. Dies reproduziert das Verhalten von BuyPositionOpen/SellPositionOpen aus dem MQL-Helper.
  • Stop-Loss- und Take-Profit-Niveaus werden bei jedem Einstieg neu berechnet und gelöscht, wenn eine Position geschlossen oder umgekehrt wird.

Verwendungsrichtlinien

  1. Hängen Sie die Strategie an ein Wertpapier an, das gültige PriceStep-, Decimals- und VolumeStep-Metadaten veröffentlicht.
  2. Setzen Sie das gewünschte Volume auf der Strategie, bevor Sie sie starten. Der Geldmanagement-Anteil wird dann angewendet.
  3. Wählen Sie einen Kerzentyp gleich dem in MetaTrader verwendeten (M30 Standard). Alle Berechnungen basieren auf abgeschlossenen Kerzen.
  4. Passen Sie die Zeitzonen an, wenn Ihre Marktdatenquelle von der Standard-GMT+4-Zielzeit des Hans-Indikators abweicht.
  5. Überwachen Sie die Protokolle auf Meldungen über fehlende Pip-Größe; Risikoniveaus werden übersprungen, wenn kein PriceStep verfügbar ist.

Implementierungshinweise

  • Farberkennung wird ausschließlich bei abgeschlossenen Kerzen über die High-Level-SubscribeCandles-API durchgeführt, was manuelle Indikatorbuffer vermeidet.
  • Ausbruchsniveaus werden einmal pro Kerze neu berechnet und im Speicher gecacht; keine historischen Sammlungen werden erstellt.
  • DeviationPoints wird für Konfigurationsvollständigkeit beibehalten, kann aber nicht mit einfachen Marktorders in StockSharp durchgesetzt werden.
  • Die Strategie setzt ihren internen Zustand in OnReseted() zurück, um wiederholte Backtests ohne veraltete Session-Daten zu unterstützen.

Einschränkungen

  • Die aktuelle Implementierung unterstützt nur SignalBar ≥ 1, passend zum ursprünglichen EA-Verhalten bei neuen Balken-Ereignissen. Die Verwendung von 0 würde Tick-Level-Zugang erfordern, der im High-Level-Port nicht vorhanden ist.
  • Geldmanagement-Modi außer Lot sind nicht implementiert. Erweitern Sie GetOrderVolume(), wenn Ihr Workflow von bilanbasierter Größenbestimmung abhängt.
  • Ohne einen gültigen PriceStep-Wert können pip-basierte Abstände (Stop, Take-Profit, Hans-Offsets) nicht berechnet werden und werden ignoriert.
namespace StockSharp.Samples.Strategies;

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;

/// <summary>
/// Port of the Exp_Hans_Indicator_Cloud_System_Tm_Plus MQL5 expert advisor.
/// Replicates the Hans indicator breakout logic, including time-based exits and pip-based risk limits.
/// </summary>
public class ExpHansIndicatorCloudSystemTmPlusStrategy : Strategy
{
	private readonly StrategyParam<int> _maxHistory;

	private static readonly TimeSpan Session1Start = TimeSpan.FromHours(4);
	private static readonly TimeSpan Session1End = TimeSpan.FromHours(8);
	private static readonly TimeSpan Session2End = TimeSpan.FromHours(12);

	private readonly StrategyParam<decimal> _moneyManagement;
	private readonly StrategyParam<MoneyManagementModes> _moneyMode;
	private readonly StrategyParam<int> _stopLossPoints;
	private readonly StrategyParam<int> _takeProfitPoints;
	private readonly StrategyParam<int> _deviationPoints;
	private readonly StrategyParam<bool> _allowBuyEntries;
	private readonly StrategyParam<bool> _allowSellEntries;
	private readonly StrategyParam<bool> _allowBuyExits;
	private readonly StrategyParam<bool> _allowSellExits;
	private readonly StrategyParam<bool> _useTimeExit;
	private readonly StrategyParam<int> _holdingMinutes;
	private readonly StrategyParam<int> _pipsForEntry;
	private readonly StrategyParam<int> _signalBar;
	private readonly StrategyParam<int> _localTimeZone;
	private readonly StrategyParam<int> _destinationTimeZone;
	private readonly StrategyParam<int> _entryCooldownBars;
	private readonly StrategyParam<DataType> _candleType;

	private DailySessionState _dayState;
	private readonly List<int> _colorHistory = new();
	private decimal? _stopPrice;
	private decimal? _takePrice;
	private DateTimeOffset? _entryTime;
	private decimal _prevClosePrice;
	private int _cooldownRemaining;
	private bool _hasPrevClose;

	/// <summary>
	/// Enumeration matching the money management modes of the original expert.
	/// Currently only the Lot mode is applied; other options are reserved for future extensions.
	/// </summary>
	public enum MoneyManagementModes
	{
		FreeMargin,
		Balance,
		LossFreeMargin,
		LossBalance,
		Lot,
	}

	/// <summary>
	/// Portion of the base strategy volume used for each order.
	/// </summary>
	public decimal MoneyManagement
	{
		get => _moneyManagement.Value;
		set => _moneyManagement.Value = value;
	}

	/// <summary>
	/// Selected money management interpretation.
	/// </summary>
	public MoneyManagementModes MoneyMode
	{
		get => _moneyMode.Value;
		set => _moneyMode.Value = value;
	}

	/// <summary>
	/// Stop loss distance expressed in instrument points.
	/// </summary>
	public int StopLossPoints
	{
		get => _stopLossPoints.Value;
		set => _stopLossPoints.Value = value;
	}

	/// <summary>
	/// Take profit distance expressed in instrument points.
	/// </summary>
	public int TakeProfitPoints
	{
		get => _takeProfitPoints.Value;
		set => _takeProfitPoints.Value = value;
	}

	/// <summary>
	/// Allowed execution deviation in points (kept for compatibility).
	/// </summary>
	public int DeviationPoints
	{
		get => _deviationPoints.Value;
		set => _deviationPoints.Value = value;
	}

	/// <summary>
	/// Enables long entries when the bullish breakout sequence completes.
	/// </summary>
	public bool AllowBuyEntries
	{
		get => _allowBuyEntries.Value;
		set => _allowBuyEntries.Value = value;
	}

	/// <summary>
	/// Enables short entries when the bearish breakout sequence completes.
	/// </summary>
	public bool AllowSellEntries
	{
		get => _allowSellEntries.Value;
		set => _allowSellEntries.Value = value;
	}

	/// <summary>
	/// Allows closing long positions on bearish Hans colors.
	/// </summary>
	public bool AllowBuyExits
	{
		get => _allowBuyExits.Value;
		set => _allowBuyExits.Value = value;
	}

	/// <summary>
	/// Allows closing short positions on bullish Hans colors.
	/// </summary>
	public bool AllowSellExits
	{
		get => _allowSellExits.Value;
		set => _allowSellExits.Value = value;
	}

	/// <summary>
	/// Enables the time-based exit filter.
	/// </summary>
	public bool UseTimeExit
	{
		get => _useTimeExit.Value;
		set => _useTimeExit.Value = value;
	}

	/// <summary>
	/// Maximum holding time in minutes before the position is liquidated.
	/// </summary>
	public int HoldingMinutes
	{
		get => _holdingMinutes.Value;
		set => _holdingMinutes.Value = value;
	}

	/// <summary>
	/// Number of pips added to the breakout range.
	/// </summary>
	public int PipsForEntry
	{
		get => _pipsForEntry.Value;
		set => _pipsForEntry.Value = value;
	}

	/// <summary>
	/// Number of closed candles used as signal offset.
	/// </summary>
	public int SignalBar
	{
		get => _signalBar.Value;
		set => _signalBar.Value = value;
	}

	/// <summary>
	/// Broker/server time zone in hours.
	/// </summary>
	public int LocalTimeZone
	{
		get => _localTimeZone.Value;
		set => _localTimeZone.Value = value;
	}

	/// <summary>
	/// Destination time zone defining the Hans breakout sessions.
	/// </summary>
	public int DestinationTimeZone
	{
		get => _destinationTimeZone.Value;
		set => _destinationTimeZone.Value = value;
	}

	/// <summary>
	/// Bars to wait after each entry before accepting a new one.
	/// </summary>
	public int EntryCooldownBars
	{
		get => _entryCooldownBars.Value;
		set => _entryCooldownBars.Value = value;
	}

	/// <summary>
	/// Candle series used for indicator calculations.
	/// </summary>
	public DataType CandleType
	{
		get => _candleType.Value;
		set => _candleType.Value = value;
	}

	/// <summary>
	/// Maximum number of Hans color samples preserved for decision making.
	/// </summary>
	public int MaxHistory
	{
		get => _maxHistory.Value;
		set => _maxHistory.Value = value;
	}

	/// <summary>
	/// Initialize the strategy parameters with defaults matching the MQL5 inputs.
	/// </summary>
	public ExpHansIndicatorCloudSystemTmPlusStrategy()
	{
		_maxHistory = Param(nameof(MaxHistory), 1024)
			.SetGreaterThanZero()
			.SetDisplay("Max History", "Maximum number of Hans color entries stored", "Indicator");

		_moneyManagement = Param(nameof(MoneyManagement), 0.1m)
		.SetDisplay("Money Management", "Portion of the base volume traded per entry", "Risk");

		_moneyMode = Param(nameof(MoneyMode), MoneyManagementModes.Lot)
		.SetDisplay("Money Mode", "Interpretation of the money management value", "Risk");

		_stopLossPoints = Param(nameof(StopLossPoints), 1000)
		.SetDisplay("Stop Loss (points)", "Distance to the protective stop in points", "Risk")
		.SetNotNegative();

		_takeProfitPoints = Param(nameof(TakeProfitPoints), 2000)
		.SetDisplay("Take Profit (points)", "Distance to the profit target in points", "Risk")
		.SetNotNegative();

		_deviationPoints = Param(nameof(DeviationPoints), 10)
		.SetDisplay("Execution Deviation", "Maximum acceptable slippage in points", "Orders")
		.SetNotNegative();

		_allowBuyEntries = Param(nameof(AllowBuyEntries), true)
		.SetDisplay("Enable Long Entries", "Allow opening long positions", "Signals");

		_allowSellEntries = Param(nameof(AllowSellEntries), true)
		.SetDisplay("Enable Short Entries", "Allow opening short positions", "Signals");

		_allowBuyExits = Param(nameof(AllowBuyExits), true)
		.SetDisplay("Enable Long Exits", "Allow automated long exits", "Signals");

		_allowSellExits = Param(nameof(AllowSellExits), true)
		.SetDisplay("Enable Short Exits", "Allow automated short exits", "Signals");

		_useTimeExit = Param(nameof(UseTimeExit), true)
		.SetDisplay("Use Time Exit", "Close positions after the holding period", "Risk");

		_holdingMinutes = Param(nameof(HoldingMinutes), 1500)
		.SetDisplay("Holding Minutes", "Maximum position lifetime in minutes", "Risk")
		.SetNotNegative();

		_pipsForEntry = Param(nameof(PipsForEntry), 5)
		.SetDisplay("Pips For Entry", "Offset added above/below the breakout range", "Indicator")
		.SetNotNegative();

		_signalBar = Param(nameof(SignalBar), 1)
		.SetDisplay("Signal Bar", "Closed candle offset used for signals", "Indicator")
		.SetNotNegative();

		_localTimeZone = Param(nameof(LocalTimeZone), 0)
		.SetDisplay("Local Time Zone", "Broker/server time zone", "Indicator");

		_destinationTimeZone = Param(nameof(DestinationTimeZone), 0)
		.SetDisplay("Destination Time Zone", "Target time zone for sessions", "Indicator");

		_entryCooldownBars = Param(nameof(EntryCooldownBars), 10)
			.SetDisplay("Entry Cooldown", "Bars to wait after an entry signal", "Risk")
			.SetGreaterThanZero();

		_candleType = Param(nameof(CandleType), TimeSpan.FromMinutes(30).TimeFrame())
		.SetDisplay("Candle Type", "Time frame used for Hans calculations", "Data");
	}

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

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

		_colorHistory.Clear();
		_dayState = null;
		_prevClosePrice = 0m;
		_cooldownRemaining = 0;
		_hasPrevClose = false;
		ResetPositionState();
	}

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

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

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

		if (Position == 0 && (_entryTime.HasValue || _stopPrice.HasValue || _takePrice.HasValue))
		ResetPositionState();

		if (_cooldownRemaining > 0)
			_cooldownRemaining--;

		UpdateDailyState(candle);

		var color = CalculateColor(candle);
		_colorHistory.Add(color);
		TrimHistory();

		var offset = Math.Max(1, SignalBar);
		if (_colorHistory.Count <= offset)
		return;

		var currentIndex = _colorHistory.Count - offset;
		if (currentIndex >= _colorHistory.Count)
		return;

		var currentColor = _colorHistory[currentIndex];
		var hasBands = TryGetActiveBands(out var upper, out var lower);
		var buyEntrySignal = false;
		var sellEntrySignal = false;

		if (hasBands && _hasPrevClose)
		{
			buyEntrySignal = AllowBuyEntries && _prevClosePrice <= upper && candle.ClosePrice > upper;
			sellEntrySignal = AllowSellEntries && _prevClosePrice >= lower && candle.ClosePrice < lower;
		}

		var buyExitSignal = AllowBuyExits && (IsLowerBreakout(currentColor) || (hasBands && candle.ClosePrice < lower));
		var sellExitSignal = AllowSellExits && (IsUpperBreakout(currentColor) || (hasBands && candle.ClosePrice > upper));

		if (Position > 0)
		{
			var exitByTime = UseTimeExit && HoldingMinutes > 0 && _entryTime.HasValue && candle.CloseTime - _entryTime.Value >= TimeSpan.FromMinutes(HoldingMinutes);
			var exitByStop = _stopPrice.HasValue && candle.LowPrice <= _stopPrice.Value;
			var exitByTarget = _takePrice.HasValue && candle.HighPrice >= _takePrice.Value;
			if (exitByTime || buyExitSignal || exitByStop || exitByTarget)
			{
				CloseLong();
			}
		}
		else if (Position < 0)
		{
			var exitByTime = UseTimeExit && HoldingMinutes > 0 && _entryTime.HasValue && candle.CloseTime - _entryTime.Value >= TimeSpan.FromMinutes(HoldingMinutes);
			var exitByStop = _stopPrice.HasValue && candle.HighPrice >= _stopPrice.Value;
			var exitByTarget = _takePrice.HasValue && candle.LowPrice <= _takePrice.Value;
			if (exitByTime || sellExitSignal || exitByStop || exitByTarget)
			{
				CloseShort();
			}
		}

		_prevClosePrice = candle.ClosePrice;
		_hasPrevClose = true;

		if (_cooldownRemaining > 0)
			return;

		if (_dayState != null && _dayState.EntryTaken)
			return;

		if (buyEntrySignal && Position <= 0)
		{
			EnterLong(candle);
		}
		else if (sellEntrySignal && Position >= 0)
		{
			EnterShort(candle);
		}
	}

	private void EnterLong(ICandleMessage candle)
	{
		var volume = GetOrderVolume();
		if (volume <= 0)
		return;

		var existingShort = Position < 0 ? Math.Abs(Position) : 0m;
		var totalVolume = volume + existingShort;
		if (totalVolume <= 0)
		return;

		BuyMarket(totalVolume);
		_cooldownRemaining = EntryCooldownBars;
		_dayState!.EntryTaken = true;

		_entryTime = candle.CloseTime != default ? candle.CloseTime : candle.OpenTime;

		var pipSize = GetPipSize();
		if (pipSize <= 0)
		{
			_stopPrice = null;
			_takePrice = null;
			return;
		}

		_stopPrice = StopLossPoints > 0 ? candle.ClosePrice - pipSize * StopLossPoints : null;
		_takePrice = TakeProfitPoints > 0 ? candle.ClosePrice + pipSize * TakeProfitPoints : null;
	}

	private void EnterShort(ICandleMessage candle)
	{
		var volume = GetOrderVolume();
		if (volume <= 0)
		return;

		var existingLong = Position > 0 ? Math.Abs(Position) : 0m;
		var totalVolume = volume + existingLong;
		if (totalVolume <= 0)
		return;

		SellMarket(totalVolume);
		_cooldownRemaining = EntryCooldownBars;
		_dayState!.EntryTaken = true;

		_entryTime = candle.CloseTime != default ? candle.CloseTime : candle.OpenTime;

		var pipSize = GetPipSize();
		if (pipSize <= 0)
		{
			_stopPrice = null;
			_takePrice = null;
			return;
		}

		_stopPrice = StopLossPoints > 0 ? candle.ClosePrice + pipSize * StopLossPoints : null;
		_takePrice = TakeProfitPoints > 0 ? candle.ClosePrice - pipSize * TakeProfitPoints : null;
	}

	private void CloseLong()
	{
		var volume = Math.Abs(Position);
		if (volume <= 0)
		return;

		SellMarket(volume);
		ResetPositionState();
	}

	private void CloseShort()
	{
		var volume = Math.Abs(Position);
		if (volume <= 0)
		return;

		BuyMarket(volume);
		ResetPositionState();
	}

	private void UpdateDailyState(ICandleMessage candle)
	{
		var destOpen = ToDestinationTime(candle.OpenTime);
		var date = destOpen.Date;

		if (_dayState == null || _dayState.Date != date)
		{
			_dayState = new DailySessionState { Date = date };
		}

		var state = _dayState;
		var timeOfDay = destOpen.TimeOfDay;

		if (timeOfDay >= Session1Start && timeOfDay < Session1End)
		{
			UpdateSessionRange(state, candle.HighPrice, candle.LowPrice, true);
			state.Session1Completed = false;
		}
		else if (timeOfDay >= Session1End && timeOfDay < Session2End)
		{
			if (!state.Session1Completed && state.Session1High.HasValue && state.Session1Low.HasValue)
			state.Session1Completed = true;

			UpdateSessionRange(state, candle.HighPrice, candle.LowPrice, false);
			state.Session2Completed = false;
		}
		else
		{
			if (!state.Session1Completed && state.Session1High.HasValue && state.Session1Low.HasValue)
			state.Session1Completed = true;

			if (!state.Session2Completed && state.Session2High.HasValue && state.Session2Low.HasValue)
			state.Session2Completed = true;
		}
	}

	private int CalculateColor(ICandleMessage candle)
	{
		if (!TryGetActiveBands(out var upper, out var lower))
		return 2;

		if (candle.ClosePrice > upper)
		return candle.ClosePrice >= candle.OpenPrice ? 0 : 1;

		if (candle.ClosePrice < lower)
		return candle.ClosePrice <= candle.OpenPrice ? 4 : 3;

		return 2;
	}

	private bool TryGetActiveBands(out decimal upper, out decimal lower)
	{
		upper = 0m;
		lower = 0m;

		var pipSize = GetPipSize();
		if (pipSize <= 0)
		return false;

		if (_dayState == null)
		return false;

		if (_dayState.Session2Completed && _dayState.Session2High.HasValue && _dayState.Session2Low.HasValue)
		{
			upper = _dayState.Session2High.Value + pipSize * PipsForEntry;
			lower = _dayState.Session2Low.Value - pipSize * PipsForEntry;
			return true;
		}

		if (_dayState.Session1Completed && _dayState.Session1High.HasValue && _dayState.Session1Low.HasValue)
		{
			upper = _dayState.Session1High.Value + pipSize * PipsForEntry;
			lower = _dayState.Session1Low.Value - pipSize * PipsForEntry;
			return true;
		}

		return false;
	}

	private void UpdateSessionRange(DailySessionState state, decimal high, decimal low, bool isFirstSession)
	{
		if (isFirstSession)
		{
			state.Session1High = state.Session1High.HasValue ? Math.Max(state.Session1High.Value, high) : high;
			state.Session1Low = state.Session1Low.HasValue ? Math.Min(state.Session1Low.Value, low) : low;
		}
		else
		{
			state.Session2High = state.Session2High.HasValue ? Math.Max(state.Session2High.Value, high) : high;
			state.Session2Low = state.Session2Low.HasValue ? Math.Min(state.Session2Low.Value, low) : low;
		}
	}

	private decimal GetOrderVolume()
	{
		var step = Security?.VolumeStep ?? 1m;
		if (step <= 0)
		step = 1m;

		var baseVolume = Volume * MoneyManagement;
		if (baseVolume <= 0)
		baseVolume = Volume;

		var normalized = Math.Round(baseVolume / step) * step;
		if (normalized <= 0)
		normalized = step;

		return normalized;
	}

	private decimal GetPipSize()
	{
		if (Security?.PriceStep is decimal step && step > 0m)
		{
			var decimals = Security.Decimals;
			if (decimals == 3 || decimals == 5)
			return step * 10m;

			return step;
		}

		return 0.01m;
	}

	private void ResetPositionState()
	{
		_entryTime = null;
		_stopPrice = null;
		_takePrice = null;
	}

	private void TrimHistory()
	{
		if (_colorHistory.Count <= MaxHistory)
		return;

		var excess = _colorHistory.Count - MaxHistory;
		_colorHistory.RemoveRange(0, excess);
	}

	private DateTimeOffset ToDestinationTime(DateTimeOffset time)
	{
		var shift = TimeSpan.FromHours(LocalTimeZone - DestinationTimeZone);
		return time - shift;
	}

	private static bool IsUpperBreakout(int? color) => color is 0 or 1;

	private static bool IsLowerBreakout(int? color) => color is 3 or 4;

	private sealed class DailySessionState
	{
		public DateTime Date { get; set; }
		public decimal? Session1High { get; set; }
		public decimal? Session1Low { get; set; }
		public decimal? Session2High { get; set; }
		public decimal? Session2Low { get; set; }
		public bool Session1Completed { get; set; }
		public bool Session2Completed { get; set; }
		public bool EntryTaken { get; set; }
	}
}