在 GitHub 上查看

Crossing of Two iMA 策略

该策略将 MetaTrader 5 上广受欢迎的 “Crossing of two iMA” 专家顾问完整迁移到 StockSharp 的高级 API。系统在两条可配置均线发生交叉时交易,并可选地通过第三条均线确认趋势方向。与原始 EA 一样,策略支持手动下单手数或基于账户风险百分比的动态仓位、PriceLevelPips 参数控制的类挂单入场方式,以及带有步进的追踪止损。

所有信号都在收盘完成的 K 线处计算,实现了 EA “等待新柱子” 的逻辑。PriceLevelPips 对应的挂单行为在内部模拟:策略跟踪蜡烛的最高价和最低价,当价格触发预设水平时立即入场,因此不会向交易所发送真实的 stop/limit 订单。多单触发条件分别对应买入止损和买入限价,空头触发条件与之对称。

交易规则

  • 指标
    • 第一条均线(周期、移位与算法均可配置)。
    • 第二条均线(同样可配置)。
    • 可选的第三条均线过滤器(UseThirdMovingAverage = true)。
  • 入场条件
    • 主交叉(柱 0 与柱 1)
      • 多头:当前柱第一均线从下向上穿过第二均线,上一柱仍位于下方。如果启用了过滤器,则第三均线必须低于第一均线。
      • 空头:第一均线从上向下穿过第二均线,若启用过滤器,则第三均线需高于第一均线。
    • 补充交叉(柱 0 与柱 2)
      • 捕捉在前两根柱之间发生的快速交叉。如果最近三根柱内已开仓,则忽略该信号,等价于原始 EA 的历史检查逻辑。
  • 方向:可做多亦可做空。
  • 止损与目标
    • 止损和止盈以点(pip)为单位输入,随后根据品种最小报价步长转换成价格距离,并对 3/5 位小数的货币对做与 EA 相同的调整。
    • TrailingStopPips > 0 时启用追踪止损;价格每向有利方向推进至少 TrailingStepPips 点后,止损就向前移动固定距离。
  • PriceLevelPips 行为
    • 0:立即市价进场。
    • < 0:模拟止损挂单(多单高于当前价,空单低于当前价),止损和止盈同时按同样幅度平移。
    • > 0:模拟限价挂单(多单低于当前价,空单高于当前价),保护价位相应平移。

资金管理

  • UseFixedVolume = true 时完全复现 EA 的“手动手数”模式:使用策略参数 Volume,并在开新仓前平掉反向仓位。
  • UseFixedVolume = false 时,仓位大小按照 Portfolio.CurrentValue * RiskPercent / 100 计算风险金额,再除以止损距离。如果未设置止损(StopLossPips = 0),风险距离为零,策略将拒绝开仓,这与 MQL5 中 MoneyFixedRisk 返回零手数的结果一致。

追踪止损

  • 多单:当价格至少向有利方向移动 TrailingStepPips 点后,止损更新为 Close - TrailingStopPips * pipValue,且不会下移。
  • 空单:对称地更新为 Close + TrailingStopPips * pipValue,确保止损始终向盈利方向收紧。
  • 在调整追踪止损前,先检测是否触发初始止损或止盈,以保持与原策略相同的优先级。

默认参数

  • 第一均线:周期 5、移位 3、方法 Smoothed
  • 第二均线:周期 8、移位 5、方法 Smoothed
  • 第三均线过滤器:开启,周期 13、移位 8、方法 Smoothed
  • 风险控制:止损 50 点、止盈 50 点、追踪 10 点,步进 4 点。
  • 资金管理:默认 UseFixedVolume = trueRiskPercent = 5 供动态手数使用。
  • 入场偏移:0 点(市价)。
  • K 线类型:1 分钟,可根据需要替换成任意周期。

实现说明

  • 均线的 shift 参数会将指标值延迟指定柱数,因此在 StockSharp 图表上与 MT5 的视觉移位一致。
  • 策略只保留最小的状态(当前柱以及前两柱的数值),即可实现原 EA 的「柱 [0], [1], [2]」逻辑,不会创建额外的大型历史集合。
  • 每当出现新的信号都会清空待触发的入场,模拟 EA 中的 DeleteAllOrders() 调用。
  • 由于 StockSharp 的订单执行是异步的,策略记录的入场价使用目标触发价来计算追踪止损和止盈位置。在以蜡烛数据回测时,这样即可重现原 EA 的行为,无需依赖逐笔成交。
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;
	}
}