Auf GitHub ansehen

Strategie Kreuzung Zweier iMA

Diese Strategie portiert den klassischen MetaTrader 5 Expert Advisor "Crossing of two iMA" in die High-Level-API von StockSharp. Sie handelt, wenn sich zwei konfigurierbare gleitende Durchschnitte kreuzen, und kann optional Bestätigung von einem dritten gleitenden Durchschnitt erfordern, der als Richtungsfilter fungiert. Die Implementierung behält die ursprüngliche Flexibilität, indem sie manuelle oder risikobasierte Positionsgrößenbestimmung, Offsets im Stil ausstehender Einstiege und einen Trailing Stop mit benutzerdefiniertem Schritt unterstützt.

Die Konvertierung verarbeitet Signale beim Schließen jeder abgeschlossenen Kerze und repliziert, wie der MQL5-Experte auf einen neuen Balken wartet. Das Verhalten von ausstehenden Aufträgen (PriceLevelPips) wird intern durch Überwachung von Kerzenhochs und -tiefs simuliert, sodass keine tatsächlichen Stop-/Limit-Aufträge gesendet werden. Ein ausstehender Long-Trigger wird aktiviert, wenn der Balken den gewählten Preis für Buy-Stop-Einstiege erreicht oder auf den Preis für Buy-Limit-Einstiege fällt, und die gleiche symmetrische Logik wird für Short-Setups angewendet.

Handelsregeln

  • Indikatoren
    • Erster gleitender Durchschnitt First (Periode, Versatz und Methode sind konfigurierbar).
    • Zweiter gleitender Durchschnitt Second (ebenfalls vollständig konfigurierbar).
    • Optionaler dritter gleitender Durchschnitt Third als Filter (UseThirdMovingAverage = true).
  • Einstiegskriterien
    • Primärer Kreuzung (Balken 0 und 1)
      • Long: der erste MA kreuzt auf dem aktuellen Balken von unten nach oben über den zweiten MA, während er auf dem vorherigen Balken darunter war. Wenn der Filter aktiv ist, muss der dritte MA unter dem ersten MA bleiben, um den Long-Ausbruch zu validieren.
      • Short: der erste MA kreuzt unter den zweiten MA und, wenn der Filter aktiviert ist, muss der dritte MA über dem ersten MA bleiben.
    • Zusatzkreuzung (Balken 0 und 2)
      • Führt eine zusätzliche Rückwärtssuche durch, um schnelle Kreuzungen zu erfassen, die zwischen den beiden vorherigen Balken aufgetreten sind. Die Strategie ignoriert dieses Signal, wenn innerhalb der letzten drei Balken bereits ein Trade eröffnet wurde (identisch mit der MQL5-Verlaufssuche).
  • Richtung: sowohl Long als auch Short.
  • Stops und Ziele
    • Stop-Loss und Take-Profit werden in Pips ausgedrückt. Sie werden basierend auf der Tick-Größe des Instruments in Preisoffsets umgerechnet und für 3/5-stellige Preisangaben wie der ursprüngliche EA angepasst.
    • Trailing Stop aktiviert sich nur, wenn TrailingStopPips > 0. Er bewegt den Stop um die Trailing-Distanz, sobald der Preis sich um mindestens TrailingStepPips über das vorherige Stop-Niveau hinaus bewegt.
  • Pending-Order-Modus (PriceLevelPips)
    • 0: sofort zum Marktpreis einsteigen.
    • < 0: Stop-Orders simulieren (Buy Stop über Preis, Sell Stop unter Preis). Stop-Loss und Take-Profit werden um denselben Offset verschoben.
    • > 0: Limit-Orders simulieren (Buy Limit unter Preis, Sell Limit über Preis). Schutzniveaus werden entsprechend verschoben.

Geldmanagement

  • UseFixedVolume = true repliziert den manuellen Lot-Modus des EA. Die Strategie verwendet einfach Volume (und schließt Gegenpositionen, bevor eine neue geöffnet wird).
  • Wenn UseFixedVolume = false, weist die Strategie Risiko als Portfolio.CurrentValue * RiskPercent / 100 zu. Die Auftragsgröße wird zu riskAmount / stopDistance. Wenn kein Stop-Loss angegeben wird (StopLossPips = 0), ist die berechnete Risikoentfernung null, sodass die Strategie die Positionseröffnung verweigert — identisch mit dem ursprünglichen MoneyFixedRisk-Verhalten, das null Lots zurückgibt.

Trailing-Logik

  • Long-Positionen verfolgen den Stop auf Close - TrailingStopPips * pipValue, sobald sich der Preis mindestens TrailingStepPips über den vorherigen Stop hinaus bewegt hat. Der Trailing-Wert bewegt sich immer nach oben und lockert den Stop nie.
  • Short-Positionen spiegeln dieses Verhalten, indem der Stop auf Close + TrailingStopPips * pipValue bewegt wird, wenn der Preis ausreichend zu seinen Gunsten voranschreitet.
  • Take-Profit und initialer Stop werden vor Trailing-Anpassungen überprüft, um sicherzustellen, dass Ausstiege mit den ursprünglichen EA-Prioritäten übereinstimmen.

Standardparameter

  • Erster MA: Länge 5, Versatz 3, Methode Smoothed.
  • Zweiter MA: Länge 8, Versatz 5, Methode Smoothed.
  • Dritter MA-Filter: aktiviert, Länge 13, Versatz 8, Methode Smoothed.
  • Risikokontrollen: Stop-Loss 50 Pips, Take-Profit 50 Pips, Trailing 10 Pips mit 4 Pip-Schritt.
  • Geldmanagement: UseFixedVolume = true, RiskPercent = 5 für den alternativen Dimensionierungsmodus.
  • Ausstehender Offset: 0 Pips (Marktausführung).
  • Kerzentyp: 1-Minuten-Zeitrahmen (kann geändert werden, um dem ursprünglichen Chartperiode zu entsprechen).

Implementierungshinweise

  • Die shift-Parameter des gleitenden Durchschnitts verzögern Signalwerte genau um die konfigurierte Anzahl von Balken, sodass das Plotting auf StockSharp-Charts mit dem MT5-visuellen Versatz übereinstimmt.
  • Die Strategie speichert nur den minimal erforderlichen Zustand (aktuell, vorherig und zwei Balken zurück), um die "Balken [0], [1], [2]"-Logik aus MQL5 zu erfüllen. Keine historischen Sammlungen werden über diesen Puffer hinaus neu erstellt.
  • Ausstehende Einstiege werden gelöscht, wenn ein neues Signal erscheint, was den DeleteAllOrders()-Aufruf des EA repliziert.
  • Da StockSharp Aufträge asynchron ausführt, verwendet der für Trailing- und Zielberechnungen erfasste Einstandspreis den beabsichtigten Trigger-Preis. Backtests reproduzieren daher die ursprüngliche EA-Logik auf Kerzendaten ohne Abhängigkeit von Tick-Level-Fills.
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>
/// Strategy that emulates the "Crossing of two iMA" MQL5 expert advisor.
/// It trades crossovers between two configurable moving averages with an optional third filter average.
/// Supports manual volume or percentage risk based sizing, simulated pending orders and trailing stop management.
/// </summary>
public class CrossingOfTwoIMAStrategy : Strategy
{
	/// <summary>
	/// Moving average calculation methods supported by the strategy.
	/// </summary>
	public enum MovingAverageMethods
	{
		/// <summary>
		/// Simple moving average.
		/// </summary>
		Simple,

		/// <summary>
		/// Exponential moving average.
		/// </summary>
		Exponential,

		/// <summary>
		/// Smoothed (RMA) moving average.
		/// </summary>
		Smoothed,

		/// <summary>
		/// Linear weighted moving average.
		/// </summary>
		Weighted,
	}

	private readonly StrategyParam<int> _firstPeriod;
	private readonly StrategyParam<int> _firstShift;
	private readonly StrategyParam<MovingAverageMethods> _firstMethod;

	private readonly StrategyParam<int> _secondPeriod;
	private readonly StrategyParam<int> _secondShift;
	private readonly StrategyParam<MovingAverageMethods> _secondMethod;

	private readonly StrategyParam<bool> _useThirdAverage;
	private readonly StrategyParam<int> _thirdPeriod;
	private readonly StrategyParam<int> _thirdShift;
	private readonly StrategyParam<MovingAverageMethods> _thirdMethod;

	private readonly StrategyParam<bool> _useFixedVolume;
	private readonly StrategyParam<decimal> _riskPercent;

	private readonly StrategyParam<int> _priceLevelPips;
	private readonly StrategyParam<int> _stopLossPips;
	private readonly StrategyParam<int> _takeProfitPips;
	private readonly StrategyParam<int> _trailingStopPips;
	private readonly StrategyParam<int> _trailingStepPips;
	private readonly StrategyParam<DataType> _candleType;

	private DecimalLengthIndicator _firstMa;
	private DecimalLengthIndicator _secondMa;
	private DecimalLengthIndicator _thirdMa;

	private readonly List<decimal> _firstValues = new();
	private readonly List<decimal> _secondValues = new();
	private readonly List<decimal> _thirdValues = new();
	private readonly List<DateTimeOffset> _openTimes = new();

	private decimal _pipSize;
	private decimal? _entryPrice;
	private decimal? _activeStopLoss;
	private decimal? _activeTakeProfit;
	private bool _isLongPosition;
	private PendingOrder _pendingOrder;
	private DateTimeOffset? _lastEntryTime;

	private enum PendingOrderTypes
	{
		None,
		BuyStop,
		BuyLimit,
		SellStop,
		SellLimit,
	}

	private sealed class PendingOrder
	{
		public PendingOrderTypes Type { get; init; }
		public decimal EntryPrice { get; init; }
		public decimal? StopLoss { get; init; }
		public decimal? TakeProfit { get; init; }
		public decimal Volume { get; init; }
	}

	/// <summary>
	/// Initializes a new instance of the <see cref="CrossingOfTwoIMAStrategy"/> class.
	/// </summary>
	public CrossingOfTwoIMAStrategy()
	{
		_firstPeriod = Param(nameof(FirstMaPeriod), 5)
			.SetGreaterThanZero()
			.SetDisplay("First MA Period", "Period of the first moving average", "First Moving Average")
			
			.SetOptimize(2, 30, 1);

		_firstShift = Param(nameof(FirstMaShift), 3)
			.SetNotNegative()
			.SetDisplay("First MA Shift", "Shift applied to the first moving average", "First Moving Average");

		_firstMethod = Param(nameof(FirstMaMethod), MovingAverageMethods.Simple)
			.SetDisplay("First MA Method", "Calculation method of the first moving average", "First Moving Average");

		_secondPeriod = Param(nameof(SecondMaPeriod), 8)
			.SetGreaterThanZero()
			.SetDisplay("Second MA Period", "Period of the second moving average", "Second Moving Average")
			
			.SetOptimize(3, 60, 1);

		_secondShift = Param(nameof(SecondMaShift), 5)
			.SetNotNegative()
			.SetDisplay("Second MA Shift", "Shift applied to the second moving average", "Second Moving Average");

		_secondMethod = Param(nameof(SecondMaMethod), MovingAverageMethods.Simple)
			.SetDisplay("Second MA Method", "Calculation method of the second moving average", "Second Moving Average");

		_useThirdAverage = Param(nameof(UseThirdMovingAverage), true)
			.SetDisplay("Use Third MA", "Enable the third moving average as a directional filter", "Third Moving Average");

		_thirdPeriod = Param(nameof(ThirdMaPeriod), 13)
			.SetGreaterThanZero()
			.SetDisplay("Third MA Period", "Period of the third moving average", "Third Moving Average");

		_thirdShift = Param(nameof(ThirdMaShift), 8)
			.SetNotNegative()
			.SetDisplay("Third MA Shift", "Shift applied to the third moving average", "Third Moving Average");

		_thirdMethod = Param(nameof(ThirdMaMethod), MovingAverageMethods.Simple)
			.SetDisplay("Third MA Method", "Calculation method of the third moving average", "Third Moving Average");

		_useFixedVolume = Param(nameof(UseFixedVolume), true)
			.SetDisplay("Use Fixed Volume", "Use the strategy volume directly instead of risk based sizing", "Money Management");

		_riskPercent = Param(nameof(RiskPercent), 5m)
			.SetNotNegative()
			.SetDisplay("Risk %", "Risk percentage of portfolio value per trade when position sizing is dynamic", "Money Management");

		_priceLevelPips = Param(nameof(PriceLevelPips), 0)
			.SetDisplay("Price Level (pips)", "Offset in pips for simulated pending orders (negative for stop, positive for limit)", "Orders");

		_stopLossPips = Param(nameof(StopLossPips), 50)
			.SetNotNegative()
			.SetDisplay("Stop Loss (pips)", "Initial stop loss distance in pips", "Risk");

		_takeProfitPips = Param(nameof(TakeProfitPips), 50)
			.SetNotNegative()
			.SetDisplay("Take Profit (pips)", "Take profit distance in pips", "Risk");

		_trailingStopPips = Param(nameof(TrailingStopPips), 10)
			.SetNotNegative()
			.SetDisplay("Trailing Stop (pips)", "Trailing stop distance in pips", "Risk");

		_trailingStepPips = Param(nameof(TrailingStepPips), 4)
			.SetNotNegative()
			.SetDisplay("Trailing Step (pips)", "Additional progress in pips required before the trailing stop is advanced", "Risk");

		_candleType = Param(nameof(CandleType), TimeSpan.FromHours(1).TimeFrame())
			.SetDisplay("Candle Type", "Primary candle series used for signals", "General");
	}

	/// <summary>
	/// Period of the first moving average.
	/// </summary>
	public int FirstMaPeriod
	{
		get => _firstPeriod.Value;
		set => _firstPeriod.Value = value;
	}

	/// <summary>
	/// Shift (in bars) of the first moving average.
	/// </summary>
	public int FirstMaShift
	{
		get => _firstShift.Value;
		set => _firstShift.Value = value;
	}

	/// <summary>
	/// Method used for the first moving average.
	/// </summary>
	public MovingAverageMethods FirstMaMethod
	{
		get => _firstMethod.Value;
		set => _firstMethod.Value = value;
	}

	/// <summary>
	/// Period of the second moving average.
	/// </summary>
	public int SecondMaPeriod
	{
		get => _secondPeriod.Value;
		set => _secondPeriod.Value = value;
	}

	/// <summary>
	/// Shift (in bars) of the second moving average.
	/// </summary>
	public int SecondMaShift
	{
		get => _secondShift.Value;
		set => _secondShift.Value = value;
	}

	/// <summary>
	/// Method used for the second moving average.
	/// </summary>
	public MovingAverageMethods SecondMaMethod
	{
		get => _secondMethod.Value;
		set => _secondMethod.Value = value;
	}

	/// <summary>
	/// Enables the third moving average filter.
	/// </summary>
	public bool UseThirdMovingAverage
	{
		get => _useThirdAverage.Value;
		set => _useThirdAverage.Value = value;
	}

	/// <summary>
	/// Period of the third moving average.
	/// </summary>
	public int ThirdMaPeriod
	{
		get => _thirdPeriod.Value;
		set => _thirdPeriod.Value = value;
	}

	/// <summary>
	/// Shift (in bars) of the third moving average.
	/// </summary>
	public int ThirdMaShift
	{
		get => _thirdShift.Value;
		set => _thirdShift.Value = value;
	}

	/// <summary>
	/// Method used for the third moving average.
	/// </summary>
	public MovingAverageMethods ThirdMaMethod
	{
		get => _thirdMethod.Value;
		set => _thirdMethod.Value = value;
	}

	/// <summary>
	/// Use fixed volume or percentage based sizing.
	/// </summary>
	public bool UseFixedVolume
	{
		get => _useFixedVolume.Value;
		set => _useFixedVolume.Value = value;
	}

	/// <summary>
	/// Risk percentage per trade when dynamic sizing is active.
	/// </summary>
	public decimal RiskPercent
	{
		get => _riskPercent.Value;
		set => _riskPercent.Value = value;
	}

	/// <summary>
	/// Offset in pips that defines simulated pending order behavior.
	/// </summary>
	public int PriceLevelPips
	{
		get => _priceLevelPips.Value;
		set => _priceLevelPips.Value = value;
	}

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

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

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

	/// <summary>
	/// Required additional progress (in pips) before advancing the trailing stop.
	/// </summary>
	public int TrailingStepPips
	{
		get => _trailingStepPips.Value;
		set => _trailingStepPips.Value = value;
	}

	/// <summary>
	/// Primary candle type used for signal generation.
	/// </summary>
	public DataType CandleType
	{
		get => _candleType.Value;
		set => _candleType.Value = value;
	}

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

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

		_firstValues.Clear();
		_secondValues.Clear();
		_thirdValues.Clear();
		_openTimes.Clear();

		_entryPrice = null;
		_activeStopLoss = null;
		_activeTakeProfit = null;
		_isLongPosition = false;
		_pendingOrder = null;
		_lastEntryTime = null;
		_pipSize = 0m;
		_firstMa = null;
		_secondMa = null;
		_thirdMa = null;
	}

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

		_firstMa = CreateMovingAverage(FirstMaMethod, FirstMaPeriod);
		_secondMa = CreateMovingAverage(SecondMaMethod, SecondMaPeriod);
		_thirdMa = UseThirdMovingAverage ? CreateMovingAverage(ThirdMaMethod, ThirdMaPeriod) : null;

		_firstValues.Clear();
		_secondValues.Clear();
		_thirdValues.Clear();
		_openTimes.Clear();

		_pipSize = Security?.PriceStep ?? 1m;
		var decimals = Security?.Decimals;
		if (decimals == 3 || decimals == 5)
			_pipSize *= 10m;

		var subscription = SubscribeCandles(CandleType);

		if (UseThirdMovingAverage && _thirdMa != null)
		{
			subscription
				.Bind(_firstMa, _secondMa, _thirdMa, ProcessCandle)
				.Start();
		}
		else
		{
			subscription
				.Bind(_firstMa, _secondMa, ProcessCandle)
				.Start();
		}
	}

	private void ProcessCandle(ICandleMessage candle, decimal firstValue, decimal secondValue)
	{
		ProcessCandleInternal(candle, firstValue, secondValue, null);
	}

	private void ProcessCandle(ICandleMessage candle, decimal firstValue, decimal secondValue, decimal thirdValue)
	{
		ProcessCandleInternal(candle, firstValue, secondValue, thirdValue);
	}

	private void ProcessCandleInternal(ICandleMessage candle, decimal firstValue, decimal secondValue, decimal? thirdValue)
	{
		if (candle.State != CandleStates.Finished)
			return;

		UpdateOpenTimes(candle.OpenTime);

		HandlePendingOrders(candle);

		var positionChanged = false;
		ManageActivePosition(candle, ref positionChanged);

		UpdateSeries(_firstValues, FirstMaShift, firstValue);
		UpdateSeries(_secondValues, SecondMaShift, secondValue);

		if (UseThirdMovingAverage && thirdValue.HasValue)
			UpdateSeries(_thirdValues, ThirdMaShift, thirdValue.Value);

		if (!_firstMa.IsFormed || !_secondMa.IsFormed)
			return;

		// already checked above

		decimal? thirdCurrent = null;
		if (UseThirdMovingAverage)
		{
			if (_thirdMa?.IsFormed != true)
				return;

			thirdCurrent = GetSeriesValue(_thirdValues, ThirdMaShift, 0);
		}

		var first0 = GetSeriesValue(_firstValues, FirstMaShift, 0);
		var first1 = GetSeriesValue(_firstValues, FirstMaShift, 1);
		var first2 = GetSeriesValue(_firstValues, FirstMaShift, 2);

		var second0 = GetSeriesValue(_secondValues, SecondMaShift, 0);
		var second1 = GetSeriesValue(_secondValues, SecondMaShift, 1);
		var second2 = GetSeriesValue(_secondValues, SecondMaShift, 2);

		if (first0 is null || first1 is null || second0 is null || second1 is null)
			return;

		var priceLevelOffset = Math.Abs(PriceLevelPips) * _pipSize;

		var stopLoss = StopLossPips > 0 ? StopLossPips * _pipSize : 0m;
		var takeProfit = TakeProfitPips > 0 ? TakeProfitPips * _pipSize : 0m;

		var currentOpenTime = candle.OpenTime;
		var startTime = GetOpenTime(3) ?? DateTimeOffset.MinValue;
		var recentEntry = _lastEntryTime.HasValue && _lastEntryTime.Value >= startTime && _lastEntryTime.Value < currentOpenTime;

		if (first0 > second0 && first1 < second1)
		{
			if (!UseThirdMovingAverage || thirdCurrent is null || thirdCurrent < first0)
			{
				EnterLong(candle, stopLoss, takeProfit, priceLevelOffset);
				return;
			}
		}
		else if (first0 < second0 && first1 > second1)
		{
			if (!UseThirdMovingAverage || thirdCurrent is null || thirdCurrent > first0)
			{
				EnterShort(candle, stopLoss, takeProfit, priceLevelOffset);
				return;
			}
		}
		else if (first0 > second0 && first2 is not null && second2 is not null && first2 < second2)
		{
			if (!recentEntry && (!UseThirdMovingAverage || thirdCurrent is null || thirdCurrent < first0))
			{
				EnterLong(candle, stopLoss, takeProfit, priceLevelOffset);
				return;
			}
		}
		else if (first0 < second2 && first1 > second2 && second2 is not null)
		{
			if (!recentEntry && (!UseThirdMovingAverage || thirdCurrent is null || thirdCurrent > first0))
			{
				EnterShort(candle, stopLoss, takeProfit, priceLevelOffset);
			}
		}
	}

	private void EnterLong(ICandleMessage candle, decimal stopLossOffset, decimal takeProfitOffset, decimal priceLevelOffset)
	{
		if (Position > 0)
			return;

		var entryPrice = candle.ClosePrice;
		var stopPrice = stopLossOffset > 0m ? entryPrice - stopLossOffset : (decimal?)null;
		var takePrice = takeProfitOffset > 0m ? entryPrice + takeProfitOffset : (decimal?)null;

		var volume = CalculateOrderVolume(entryPrice, stopPrice);
		if (volume <= 0m)
			return;

		CancelPendingOrders();

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

			BuyMarket();
			_entryPrice = entryPrice;
			_activeStopLoss = stopPrice;
			_activeTakeProfit = takePrice;
			_isLongPosition = true;
			_lastEntryTime = candle.OpenTime;
		}
		else if (PriceLevelPips < 0)
		{
			var targetPrice = entryPrice + priceLevelOffset;
			var stop = stopPrice.HasValue ? stopPrice.Value + priceLevelOffset : (decimal?)null;
			var take = takePrice.HasValue ? takePrice.Value + priceLevelOffset : (decimal?)null;
			_pendingOrder = new PendingOrder
			{
				Type = PendingOrderTypes.BuyStop,
				EntryPrice = targetPrice,
				StopLoss = stop,
				TakeProfit = take,
				Volume = volume,
			};
		}
		else
		{
			var targetPrice = entryPrice - priceLevelOffset;
			var stop = stopPrice.HasValue ? stopPrice.Value - priceLevelOffset : (decimal?)null;
			var take = takePrice.HasValue ? takePrice.Value - priceLevelOffset : (decimal?)null;
			_pendingOrder = new PendingOrder
			{
				Type = PendingOrderTypes.BuyLimit,
				EntryPrice = targetPrice,
				StopLoss = stop,
				TakeProfit = take,
				Volume = volume,
			};
		}
	}

	private void EnterShort(ICandleMessage candle, decimal stopLossOffset, decimal takeProfitOffset, decimal priceLevelOffset)
	{
		if (Position < 0)
			return;

		var entryPrice = candle.ClosePrice;
		var stopPrice = stopLossOffset > 0m ? entryPrice + stopLossOffset : (decimal?)null;
		var takePrice = takeProfitOffset > 0m ? entryPrice - takeProfitOffset : (decimal?)null;

		var volume = CalculateOrderVolume(entryPrice, stopPrice);
		if (volume <= 0m)
			return;

		CancelPendingOrders();

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

			SellMarket();
			_entryPrice = entryPrice;
			_activeStopLoss = stopPrice;
			_activeTakeProfit = takePrice;
			_isLongPosition = false;
			_lastEntryTime = candle.OpenTime;
		}
		else if (PriceLevelPips < 0)
		{
			var targetPrice = entryPrice - priceLevelOffset;
			var stop = stopPrice.HasValue ? stopPrice.Value - priceLevelOffset : (decimal?)null;
			var take = takePrice.HasValue ? takePrice.Value - priceLevelOffset : (decimal?)null;
			_pendingOrder = new PendingOrder
			{
				Type = PendingOrderTypes.SellStop,
				EntryPrice = targetPrice,
				StopLoss = stop,
				TakeProfit = take,
				Volume = volume,
			};
		}
		else
		{
			var targetPrice = entryPrice + priceLevelOffset;
			var stop = stopPrice.HasValue ? stopPrice.Value + priceLevelOffset : (decimal?)null;
			var take = takePrice.HasValue ? takePrice.Value + priceLevelOffset : (decimal?)null;
			_pendingOrder = new PendingOrder
			{
				Type = PendingOrderTypes.SellLimit,
				EntryPrice = targetPrice,
				StopLoss = stop,
				TakeProfit = take,
				Volume = volume,
			};
		}
	}

	private void HandlePendingOrders(ICandleMessage candle)
	{
		if (_pendingOrder is null)
			return;

		var triggered = _pendingOrder.Type switch
		{
			PendingOrderTypes.BuyStop => candle.HighPrice >= _pendingOrder.EntryPrice,
			PendingOrderTypes.BuyLimit => candle.LowPrice <= _pendingOrder.EntryPrice,
			PendingOrderTypes.SellStop => candle.LowPrice <= _pendingOrder.EntryPrice,
			PendingOrderTypes.SellLimit => candle.HighPrice >= _pendingOrder.EntryPrice,
			_ => false,
		};

		if (!triggered)
			return;

		var volume = _pendingOrder.Volume;
		if (volume <= 0m)
		{
			_pendingOrder = null;
			return;
		}

		if (_pendingOrder.Type == PendingOrderTypes.BuyStop || _pendingOrder.Type == PendingOrderTypes.BuyLimit)
		{
			var totalVolume = volume + (Position < 0 ? Math.Abs(Position) : 0m);
			if (totalVolume > 0m)
			{
				BuyMarket();
				_entryPrice = _pendingOrder.EntryPrice;
				_activeStopLoss = _pendingOrder.StopLoss;
				_activeTakeProfit = _pendingOrder.TakeProfit;
				_isLongPosition = true;
				_lastEntryTime = candle.OpenTime;
			}
		}
		else
		{
			var totalVolume = volume + (Position > 0 ? Math.Abs(Position) : 0m);
			if (totalVolume > 0m)
			{
				SellMarket();
				_entryPrice = _pendingOrder.EntryPrice;
				_activeStopLoss = _pendingOrder.StopLoss;
				_activeTakeProfit = _pendingOrder.TakeProfit;
				_isLongPosition = false;
				_lastEntryTime = candle.OpenTime;
			}
		}

		_pendingOrder = null;
	}

	private void ManageActivePosition(ICandleMessage candle, ref bool positionChanged)
	{
		if (Position == 0)
			return;

		var positionVolume = Math.Abs(Position);
		if (positionVolume <= 0m)
			return;

		if (_isLongPosition)
		{
			if (_activeTakeProfit.HasValue && candle.HighPrice >= _activeTakeProfit.Value)
			{
				SellMarket();
				ResetPositionState();
				positionChanged = true;
				return;
			}

			if (_activeStopLoss.HasValue && candle.LowPrice <= _activeStopLoss.Value)
			{
				SellMarket();
				ResetPositionState();
				positionChanged = true;
				return;
			}

			UpdateTrailingForLong(candle);
		}
		else
		{
			if (_activeTakeProfit.HasValue && candle.LowPrice <= _activeTakeProfit.Value)
			{
				BuyMarket();
				ResetPositionState();
				positionChanged = true;
				return;
			}

			if (_activeStopLoss.HasValue && candle.HighPrice >= _activeStopLoss.Value)
			{
				BuyMarket();
				ResetPositionState();
				positionChanged = true;
				return;
			}

			UpdateTrailingForShort(candle);
		}
	}

	private void UpdateTrailingForLong(ICandleMessage candle)
	{
		if (TrailingStopPips <= 0)
			return;

		var trailingDistance = TrailingStopPips * _pipSize;
		var trailingStep = TrailingStepPips * _pipSize;

		var targetStop = candle.ClosePrice - trailingDistance;
		if (!_activeStopLoss.HasValue || targetStop <= _activeStopLoss.Value)
			return;

		if (trailingStep <= 0m || _activeStopLoss.Value < targetStop - trailingStep)
			_activeStopLoss = targetStop;
	}

	private void UpdateTrailingForShort(ICandleMessage candle)
	{
		if (TrailingStopPips <= 0)
			return;

		var trailingDistance = TrailingStopPips * _pipSize;
		var trailingStep = TrailingStepPips * _pipSize;

		var targetStop = candle.ClosePrice + trailingDistance;
		if (!_activeStopLoss.HasValue || targetStop >= _activeStopLoss.Value)
			return;

		if (trailingStep <= 0m || _activeStopLoss.Value > targetStop + trailingStep)
			_activeStopLoss = targetStop;
	}

	private decimal CalculateOrderVolume(decimal entryPrice, decimal? stopPrice)
	{
		if (UseFixedVolume || !stopPrice.HasValue)
			return Volume;

		var riskDistance = Math.Abs(entryPrice - stopPrice.Value);
		if (riskDistance <= 0m)
			return 0m;

		var equity = Portfolio?.CurrentValue ?? 0m;
		var riskAmount = equity * RiskPercent / 100m;
		return riskAmount > 0m ? riskAmount / riskDistance : 0m;
	}

	private void CancelPendingOrders()
	{
		_pendingOrder = null;
	}

	private void ResetPositionState()
	{
		_entryPrice = null;
		_activeStopLoss = null;
		_activeTakeProfit = null;
		_isLongPosition = false;
	}

	private void UpdateSeries(List<decimal> values, int shift, decimal value)
	{
		values.Add(value);
		var maxSize = Math.Max(shift + 3, 3);
		while (values.Count > maxSize)
			values.RemoveAt(0);
	}

	private static decimal? GetSeriesValue(List<decimal> values, int shift, int index)
	{
		var targetIndex = values.Count - 1 - shift - index;
		if (targetIndex < 0 || targetIndex >= values.Count)
			return null;

		return values[targetIndex];
	}

	private void UpdateOpenTimes(DateTimeOffset openTime)
	{
		_openTimes.Add(openTime);
		while (_openTimes.Count > 4)
			_openTimes.RemoveAt(0);
	}

	private DateTimeOffset? GetOpenTime(int index)
	{
		var targetIndex = _openTimes.Count - 1 - index;
		if (targetIndex < 0 || targetIndex >= _openTimes.Count)
			return null;

		return _openTimes[targetIndex];
	}

	private static DecimalLengthIndicator CreateMovingAverage(MovingAverageMethods method, int length)
	{
		DecimalLengthIndicator ma = method switch
		{
			MovingAverageMethods.Simple => new SMA(),
			MovingAverageMethods.Exponential => new EMA(),
			MovingAverageMethods.Smoothed => new SmoothedMovingAverage(),
			MovingAverageMethods.Weighted => new WeightedMovingAverage(),
			_ => new SMA(),
		};

		ma.Length = Math.Max(1, length);
		return ma;
	}
}