Открыть на GitHub

Стратегия Crossing of Two iMA

Эта стратегия переносит популярного эксперта MetaTrader 5 «Crossing of two iMA» в высокоуровневый API StockSharp. Сделки открываются при пересечении двух настраиваемых скользящих средних, а дополнительная третья средняя может выступать фильтром направления. Реализация сохраняет гибкость оригинала: поддерживаются ручной объём или риск-менеджмент по проценту капитала, отложенный стиль входа с параметром PriceLevelPips и трейлинг-стоп с регулируемым шагом.

Сигналы рассчитываются только на закрытии завершённых свечей, что повторяет логику эксперта MQL5 «ждать новый бар». Поведение отложенных ордеров (PriceLevelPips) эмулируется внутри стратегии — фактические стоп/лимит заявки не отправляются. Для покупок стоп-вход срабатывает, когда максимум свечи достигает целевого уровня, а лимит-вход — когда минимум опускается до заданной цены. Для коротких сделок применяется зеркальная схема.

Торговые правила

  • Индикаторы
    • Первая скользящая средняя (период, сдвиг и метод сглаживания задаются параметрами).
    • Вторая скользящая средняя (аналогично настраивается).
    • Опциональная третья скользящая средняя-фильтр (UseThirdMovingAverage = true).
  • Условия входа
    • Основное пересечение (бары 0 и 1)
      • Лонг: первая средняя пересекает вторую снизу вверх на текущем баре, а на предыдущем баре находилась ниже. Если фильтр активен, третья средняя должна находиться ниже первой.
      • Шорт: первая средняя пересекает вторую сверху вниз; при включённом фильтре третья средняя должна быть выше первой.
    • Дополнительное пересечение (бары 0 и 2)
      • Позволяет отловить резкие переходы, произошедшие между двумя предыдущими барами. Сигнал игнорируется, если в пределах последних трёх баров уже открывалась сделка (аналог SearchPositions в оригинале).
  • Направление: торгуются лонги и шорты.
  • Стопы и цели
    • Стоп-лосс и тейк-профит задаются в пунктах и преобразуются в ценовые оффсеты с учётом шага цены и 3/5-значного котирования, как в исходном советнике.
    • Трейлинг-стоп активируется, когда TrailingStopPips > 0. Стоп переносится на расстояние трейлинга после того, как цена продвинулась минимум на TrailingStepPips пунктов от предыдущего уровня.
  • Режим PriceLevelPips
    • 0: вход по рынку.
    • < 0: имитация стоп-ордеров (buy stop выше цены, sell stop ниже). Стоп и тейк смещаются на тот же оффсет.
    • > 0: имитация лимит-ордеров (buy limit ниже цены, sell limit выше). Защитные уровни смещаются симметрично.

Управление капиталом

  • UseFixedVolume = true повторяет ручной режим лота: стратегия использует параметр Volume и перед открытием новой сделки закрывает противоположную позицию.
  • При UseFixedVolume = false риск рассчитывается как Portfolio.CurrentValue * RiskPercent / 100. Объём позиции равен riskAmount / stopDistance. Если стоп-лосс отключён (StopLossPips = 0), расстояние до стопа равно нулю, поэтому стратегия отказывается открывать позицию — это полностью соответствует поведению класса MoneyFixedRisk в MQL5.

Логика трейлинг-стопа

  • Для лонга стоп переносится на уровень Close - TrailingStopPips * pipValue, когда цена проходит в прибыльном направлении минимум TrailingStepPips пунктов. Стоп никогда не отступает назад.
  • Для шорта стоп зеркально переносится на Close + TrailingStopPips * pipValue при достаточном движении в сторону прибыли.
  • Проверка тейк-профита и исходного стопа выполняется до перерасчёта трейлинга, что совпадает с приоритетами оригинала.

Значения по умолчанию

  • Первая средняя: период 5, сдвиг 3, метод Smoothed.
  • Вторая средняя: период 8, сдвиг 5, метод Smoothed.
  • Фильтр: включён, период 13, сдвиг 8, метод Smoothed.
  • Риск-параметры: стоп 50 пунктов, тейк 50 пунктов, трейлинг 10 пунктов и шаг 4 пункта.
  • Управление капиталом: UseFixedVolume = true, RiskPercent = 5 для альтернативного режима.
  • Смещение входа: 0 пунктов (рыночный вход).
  • Тип свечей: таймфрейм 1 минута (можно сменить на нужный таймфрейм графика).

Особенности реализации

  • Параметры shift задерживают значения скользящих ровно на заданное число баров, поэтому отображение на графиках StockSharp совпадает с MT5.
  • Хранится лишь минимальный буфер значений (текущий бар и два предыдущих), достаточный для логики «бары [0], [1], [2]» из MQL5 — дополнительных коллекций не создаётся.
  • Любой новый сигнал очищает ожидающий вход, имитируя вызов DeleteAllOrders().
  • Поскольку в StockSharp заявки исполняются асинхронно, для расчёта трейлинга и целей используется целевая цена входа. При тестировании на свечных данных это воспроизводит оригинальную логику без необходимости тиковой точности.
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;
	}
}