Ver en GitHub

EMA Cruce de Concurso con Cobertura

Descripción general

  • Recrea la estrategia de MetaTrader "EMA Cross Contest Hedged" usando la API de alto nivel de StockSharp.
  • Opera con un par de medias móviles exponenciales (EMA) y opcionalmente confirma con la línea principal del MACD.
  • Construye una escalera de órdenes stop pendientes (niveles de "cobertura") después de cada entrada para escalar en tendencias fuertes.
  • Aplica niveles estáticos de stop-loss/take-profit expresados en pips y un trailing stop que se activa después de una ganancia mínima.
  • Permite elegir si las señales deben usar la vela completada actual o la vela cerrada anterior.

Indicadores y datos

  • EMA corta con longitud configurable (por defecto 4).
  • EMA larga con longitud configurable (por defecto 24); el período corto debe permanecer por debajo del período largo.
  • MACD (4, 24, 12) línea principal usada como filtro de confirmación opcional.
  • Funciona en cualquier marco temporal proporcionado por el parámetro CandleType (por defecto velas de 15 minutos).

Lógica de entrada

  1. Esperar una vela terminada del marco temporal configurado.
  2. Calcular los valores de EMA rápida y lenta. Dependiendo de TradeBar, determinar el cruce usando:
    • La última y la anterior vela terminada (Current).
    • La anterior y la que le precede (Previous, por defecto).
  3. Generar una señal larga cuando la EMA rápida cruza por encima de la EMA lenta. Si UseMacdFilter está habilitado, el valor MACD para la misma barra debe ser no negativo.
  4. Generar una señal corta cuando la EMA rápida cruza por debajo de la EMA lenta. Con el filtro MACD habilitado, el valor MACD debe ser no positivo.
  5. Solo abrir una nueva posición cuando no hay exposición presente (todas las operaciones anteriores están planas).
  6. Ejecutar órdenes de mercado con tamaño OrderVolume. Después de una entrada, la estrategia:
    • Almacena los niveles de stop-loss y take-profit desplazados por StopLossPips y TakeProfitPips desde el precio de ejecución.
    • Restablece el estado del trailing stop.
    • Crea cuatro órdenes stop de cobertura espaciadas por HedgeLevelPips en la dirección de la operación. Cada orden pendiente hereda la misma distancia de stop-loss/take-profit y expira después de PendingExpirationSeconds segundos a menos que el precio la alcance antes.

Gestión de salida

  • Stop-loss / take-profit: La estrategia monitorea máximos y mínimos intrabarra. Si el precio toca el stop o el objetivo almacenado, toda la posición se cierra.
  • Trailing stop: Cuando la ganancia supera TrailingStopPips + TrailingStepPips, el stop se sigue a TrailingStopPips detrás del último cierre. Las posiciones largas se siguen hacia arriba, las cortas hacia abajo.
  • Cruce opuesto: Cuando CloseOppositePositions está habilitado, la posición se cierra tan pronto como se detecta el cruce EMA opuesto.
  • Escalera pendiente: Cada orden de cobertura se convierte en una orden de mercado adicional cuando el precio cruza el nivel stop. Las nuevas ejecuciones ajustan el precio de entrada promedio y ajustan los niveles de protección en consecuencia.

Parámetros

Nombre Por defecto Descripción
OrderVolume 0.1 Tamaño de la orden para cada orden de mercado o stop.
StopLossPips 140 Distancia del stop en pips. Poner en 0 para deshabilitar.
TakeProfitPips 120 Distancia del take-profit en pips. Poner en 0 para deshabilitar.
TrailingStopPips 30 Distancia del trailing stop en pips. Poner en 0 para deshabilitar.
TrailingStepPips 1 Ganancia adicional mínima (en pips) antes de que el trailing stop se ajuste de nuevo.
HedgeLevelPips 6 Distancia entre las órdenes stop de cobertura escalonadas.
CloseOppositePositions false Cerrar la posición activa cuando aparezca un cruce opuesto.
UseMacdFilter false Requerir confirmación MACD (>= 0 para largos, <= 0 para cortos).
PendingExpirationSeconds 65535 Vida útil de cada orden stop de cobertura en segundos.
ShortMaPeriod 4 Longitud de la EMA corta. Debe ser menor que LongMaPeriod.
LongMaPeriod 24 Longitud de la EMA larga.
TradeBar Previous Determina qué par de barras se usa para detectar el cruce.
CandleType 15 minutos Marco temporal solicitado al proveedor de datos.

Notas adicionales

  • Los pips se convierten multiplicando Security.PriceStep y aplicando automáticamente un factor de 10 para instrumentos de 3 y 5 decimales para coincidir con las convenciones de pip de MetaTrader.
  • Las órdenes de cobertura pendientes se simulan dentro de la estrategia y se ejecutan tan pronto como el rango de la vela toca su nivel.
  • Se invoca StartProtection() para activar los servicios integrados de protección de posición de StockSharp.
  • La estrategia mantiene lógica de trailing stop separada para posiciones largas y cortas para reflejar la implementación con cobertura original.
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>
/// EMA crossover strategy with hedged stop orders and trailing management.
/// Converted from the MQL version of "EMA Cross Contest Hedged".
/// </summary>
public class EmaCrossContestHedgedStrategy : Strategy
{
	public enum TradeBarOptions
	{
		Current,
		Previous
	}

	private readonly StrategyParam<int> _pendingOrderCount;
	private readonly StrategyParam<int> _macdFastLength;
	private readonly StrategyParam<int> _macdSlowLength;
	private readonly StrategyParam<int> _macdSignalLength;

	private readonly StrategyParam<decimal> _orderVolume;
	private readonly StrategyParam<int> _stopLossPips;
	private readonly StrategyParam<int> _takeProfitPips;
	private readonly StrategyParam<int> _trailingStopPips;
	private readonly StrategyParam<int> _trailingStepPips;
	private readonly StrategyParam<int> _hedgeLevelPips;
	private readonly StrategyParam<bool> _closeOppositePositions;
	private readonly StrategyParam<bool> _useMacdFilter;
	private readonly StrategyParam<int> _pendingExpirationSeconds;
	private readonly StrategyParam<int> _shortMaPeriod;
	private readonly StrategyParam<int> _longMaPeriod;
	private readonly StrategyParam<TradeBarOptions> _tradeBar;
	private readonly StrategyParam<DataType> _candleType;

	private decimal? _emaShortLast;
	private decimal? _emaShortPrevLast;
	private decimal? _emaLongLast;
	private decimal? _emaLongPrevLast;
	private decimal? _macdLast;

	private decimal _currentVolume;
	private decimal _entryPrice;
	private decimal? _longStop;
	private decimal? _longTakeProfit;
	private decimal? _shortStop;
	private decimal? _shortTakeProfit;
	private decimal? _longTrailingStop;
	private decimal? _shortTrailingStop;

	private readonly List<PendingOrder> _pendingOrders = new();

	private sealed class PendingOrder
	{
		public Sides Side { get; init; }
		public decimal Price { get; init; }
		public decimal? StopLoss { get; init; }
		public decimal? TakeProfit { get; init; }
		public DateTimeOffset ExpireTime { get; init; }
	}

	public decimal OrderVolume
	{
		get => _orderVolume.Value;
		set => _orderVolume.Value = value;
	}

	public int StopLossPips
	{
		get => _stopLossPips.Value;
		set => _stopLossPips.Value = value;
	}

	public int TakeProfitPips
	{
		get => _takeProfitPips.Value;
		set => _takeProfitPips.Value = value;
	}

	public int TrailingStopPips
	{
		get => _trailingStopPips.Value;
		set => _trailingStopPips.Value = value;
	}

	public int TrailingStepPips
	{
		get => _trailingStepPips.Value;
		set => _trailingStepPips.Value = value;
	}

	public int HedgeLevelPips
	{
		get => _hedgeLevelPips.Value;
		set => _hedgeLevelPips.Value = value;
	}

	public bool CloseOppositePositions
	{
		get => _closeOppositePositions.Value;
		set => _closeOppositePositions.Value = value;
	}

	public bool UseMacdFilter
	{
		get => _useMacdFilter.Value;
		set => _useMacdFilter.Value = value;
	}

	/// <summary>
	/// Number of pending stop orders per direction.
	/// </summary>
	public int PendingOrderCount
	{
		get => _pendingOrderCount.Value;
		set => _pendingOrderCount.Value = value;
	}

	public int PendingExpirationSeconds
	{
		get => _pendingExpirationSeconds.Value;
		set => _pendingExpirationSeconds.Value = value;
	}

	/// <summary>
	/// Fast moving average length for the MACD filter.
	/// </summary>
	public int MacdFastLength
	{
		get => _macdFastLength.Value;
		set => _macdFastLength.Value = value;
	}

	/// <summary>
	/// Slow moving average length for the MACD filter.
	/// </summary>
	public int MacdSlowLength
	{
		get => _macdSlowLength.Value;
		set => _macdSlowLength.Value = value;
	}

	/// <summary>
	/// Signal moving average length for the MACD filter.
	/// </summary>
	public int MacdSignalLength
	{
		get => _macdSignalLength.Value;
		set => _macdSignalLength.Value = value;
	}

	public int ShortMaPeriod
	{
		get => _shortMaPeriod.Value;
		set => _shortMaPeriod.Value = value;
	}

	public int LongMaPeriod
	{
		get => _longMaPeriod.Value;
		set => _longMaPeriod.Value = value;
	}

	public TradeBarOptions TradeBar
	{
		get => _tradeBar.Value;
		set => _tradeBar.Value = value;
	}

	public DataType CandleType
	{
		get => _candleType.Value;
		set => _candleType.Value = value;
	}

	public EmaCrossContestHedgedStrategy()
	{
		_orderVolume = Param(nameof(OrderVolume), 0.1m)
			.SetDisplay("Order Volume", "Order size", "General")
			.SetGreaterThanZero();

		_stopLossPips = Param(nameof(StopLossPips), 140)
			.SetDisplay("Stop Loss (pips)", "Stop loss distance in pips", "Risk");

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

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

		_trailingStepPips = Param(nameof(TrailingStepPips), 1)
			.SetDisplay("Trailing Step (pips)", "Minimum profit before trailing adjusts", "Risk");

		_hedgeLevelPips = Param(nameof(HedgeLevelPips), 6)
			.SetDisplay("Hedge Level (pips)", "Distance between hedging stop orders", "Orders");

		_closeOppositePositions = Param(nameof(CloseOppositePositions), false)
			.SetDisplay("Close Opposite", "Close positions on opposite crossover", "Risk");

		_useMacdFilter = Param(nameof(UseMacdFilter), false)
			.SetDisplay("Use MACD", "Require MACD confirmation", "Filters");

		_pendingOrderCount = Param(nameof(PendingOrderCount), 1)
			.SetGreaterThanZero()
			.SetDisplay("Pending Orders", "Pending stop orders per side", "Orders");

		_pendingExpirationSeconds = Param(nameof(PendingExpirationSeconds), 65535)
			.SetDisplay("Pending Expiration (s)", "Lifetime of hedging stop orders in seconds", "Orders");

		_macdFastLength = Param(nameof(MacdFastLength), 4)
			.SetGreaterThanZero()
			.SetDisplay("MACD Fast Length", "Fast EMA length for MACD", "Indicators");

		_macdSlowLength = Param(nameof(MacdSlowLength), 24)
			.SetGreaterThanZero()
			.SetDisplay("MACD Slow Length", "Slow EMA length for MACD", "Indicators");

		_macdSignalLength = Param(nameof(MacdSignalLength), 12)
			.SetGreaterThanZero()
			.SetDisplay("MACD Signal Length", "Signal EMA length for MACD", "Indicators");

		_shortMaPeriod = Param(nameof(ShortMaPeriod), 4)
			.SetGreaterThanZero()
			.SetDisplay("Short EMA Period", "Fast EMA length", "Indicators");

		_longMaPeriod = Param(nameof(LongMaPeriod), 24)
			.SetGreaterThanZero()
			.SetDisplay("Long EMA Period", "Slow EMA length", "Indicators");

		_tradeBar = Param(nameof(TradeBar), TradeBarOptions.Previous)
			.SetDisplay("Trade Bar", "Use current or previous bar for signals", "General");

		_candleType = Param(nameof(CandleType), TimeSpan.FromHours(1).TimeFrame())
			.SetDisplay("Candle Type", "Timeframe for calculations", "General");
	}

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

	protected override void OnReseted()
	{
		base.OnReseted();

		_emaShortLast = null;
		_emaShortPrevLast = null;
		_emaLongLast = null;
		_emaLongPrevLast = null;
		_macdLast = null;

		_currentVolume = 0m;
		_entryPrice = 0m;
		_longStop = null;
		_longTakeProfit = null;
		_shortStop = null;
		_shortTakeProfit = null;
		_longTrailingStop = null;
		_shortTrailingStop = null;
		_pendingOrders.Clear();
	}

	protected override void OnStarted2(DateTime time)
	{
		base.OnStarted2(time);

		if (ShortMaPeriod >= LongMaPeriod)
			throw new InvalidOperationException("Short EMA period must be less than long EMA period.");

		Volume = OrderVolume;

		var shortEma = new ExponentialMovingAverage { Length = ShortMaPeriod };
		var longEma = new ExponentialMovingAverage { Length = LongMaPeriod };
		var macd = new MovingAverageConvergenceDivergenceSignal
		{
			Macd =
			{
				ShortMa = { Length = MacdFastLength },
				LongMa = { Length = MacdSlowLength }
			},
			SignalMa = { Length = MacdSignalLength }
		};

		var subscription = SubscribeCandles(CandleType);
		subscription
			.BindEx(shortEma, longEma, macd, ProcessCandle)
			.Start();

		var area = CreateChartArea();
		if (area != null)
		{
			DrawCandles(area, subscription);
			DrawIndicator(area, shortEma);
			DrawIndicator(area, longEma);
			DrawOwnTrades(area);
		}
	}

	private void ProcessCandle(ICandleMessage candle, IIndicatorValue shortValue, IIndicatorValue longValue, IIndicatorValue macdValue)
	{
		if (candle.State != CandleStates.Finished)
			return;

		if (!shortValue.IsFinal || !longValue.IsFinal)
			return;

		var emaShort = shortValue.ToDecimal();
		var emaLong = longValue.ToDecimal();

		decimal? macdCurrent = null;
		if (macdValue.IsFinal)
		{
			var macdTyped = (MovingAverageConvergenceDivergenceSignalValue)macdValue;
			if (macdTyped.Macd is decimal macdLine)
				macdCurrent = macdLine;
		}

		ProcessPendingOrders(candle);

		var cross = DetectCross(emaShort, emaLong);

		decimal? macdFilterValue = null;
		if (UseMacdFilter)
		{
			macdFilterValue = TradeBar == TradeBarOptions.Current ? macdCurrent : _macdLast;
			if (!macdFilterValue.HasValue)
			{
				UpdateHistory(emaShort, emaLong, macdCurrent);
				return;
			}
		}

		if (!IsFormedAndOnlineAndAllowTrading())
		{
			UpdateHistory(emaShort, emaLong, macdCurrent);
			return;
		}

		if (_currentVolume > 0m)
		{
			if (CloseOppositePositions && cross == 2)
			{
				ExitLong();
				UpdateHistory(emaShort, emaLong, macdCurrent);
				return;
			}

			if (CheckLongStops(candle))
			{
				UpdateHistory(emaShort, emaLong, macdCurrent);
				return;
			}
		}
		else if (_currentVolume < 0m)
		{
			if (CloseOppositePositions && cross == 1)
			{
				ExitShort();
				UpdateHistory(emaShort, emaLong, macdCurrent);
				return;
			}

			if (CheckShortStops(candle))
			{
				UpdateHistory(emaShort, emaLong, macdCurrent);
				return;
			}
		}

		if (_currentVolume == 0m)
		{
			if (cross == 1 && (!UseMacdFilter || macdFilterValue >= 0m))
			{
				EnterLong(candle.ClosePrice, candle.CloseTime);
				UpdateHistory(emaShort, emaLong, macdCurrent);
				return;
			}

			if (cross == 2 && (!UseMacdFilter || macdFilterValue <= 0m))
			{
				EnterShort(candle.ClosePrice, candle.CloseTime);
				UpdateHistory(emaShort, emaLong, macdCurrent);
				return;
			}
		}

		UpdateHistory(emaShort, emaLong, macdCurrent);
	}

	private void ProcessPendingOrders(ICandleMessage candle)
	{
		if (_pendingOrders.Count == 0)
			return;

		var now = candle.CloseTime;
		var orders = _pendingOrders.ToArray();

		foreach (var order in orders)
		{
			if (order == null)
				continue;

			if (order.ExpireTime <= now)
			{
				_pendingOrders.Remove(order);
				continue;
			}

			var triggered = order.Side == Sides.Buy
				? candle.HighPrice >= order.Price
				: candle.LowPrice <= order.Price;

			if (!triggered)
				continue;

			if (!_pendingOrders.Remove(order))
				continue;

			if (order.Side == Sides.Buy)
			{
				BuyMarket(OrderVolume);
				RegisterLongEntry(order.Price, OrderVolume, order.StopLoss, order.TakeProfit);
			}
			else
			{
				SellMarket(OrderVolume);
				RegisterShortEntry(order.Price, OrderVolume, order.StopLoss, order.TakeProfit);
			}
		}
	}

	private void EnterLong(decimal price, DateTimeOffset time)
	{
		BuyMarket(OrderVolume);
		RegisterLongEntry(price, OrderVolume,
			StopLossPips > 0 ? price - PipToPrice(StopLossPips) : null,
			TakeProfitPips > 0 ? price + PipToPrice(TakeProfitPips) : null);

		_shortStop = null;
		_shortTakeProfit = null;
		_shortTrailingStop = null;

		CreatePendingOrders(time, price, Sides.Buy);
	}

	private void EnterShort(decimal price, DateTimeOffset time)
	{
		SellMarket(OrderVolume);
		RegisterShortEntry(price, OrderVolume,
			StopLossPips > 0 ? price + PipToPrice(StopLossPips) : null,
			TakeProfitPips > 0 ? price - PipToPrice(TakeProfitPips) : null);

		_longStop = null;
		_longTakeProfit = null;
		_longTrailingStop = null;

		CreatePendingOrders(time, price, Sides.Sell);
	}

	private void RegisterLongEntry(decimal price, decimal volume, decimal? stop, decimal? take)
	{
		var previousVolume = _currentVolume;
		_currentVolume += volume;

		if (previousVolume <= 0m)
			_entryPrice = price;
		else
			_entryPrice = ((previousVolume * _entryPrice) + (volume * price)) / _currentVolume;

		if (stop.HasValue)
			_longStop = _longStop.HasValue ? Math.Max(_longStop.Value, stop.Value) : stop;

		if (take.HasValue)
			_longTakeProfit = _longTakeProfit.HasValue ? Math.Max(_longTakeProfit.Value, take.Value) : take;

		_longTrailingStop = null;
	}

	private void RegisterShortEntry(decimal price, decimal volume, decimal? stop, decimal? take)
	{
		var previousVolume = _currentVolume;
		_currentVolume -= volume;

		if (previousVolume >= 0m)
			_entryPrice = price;
		else
			_entryPrice = ((Math.Abs(previousVolume) * _entryPrice) + (volume * price)) / Math.Abs(_currentVolume);

		if (stop.HasValue)
			_shortStop = _shortStop.HasValue ? Math.Min(_shortStop.Value, stop.Value) : stop;

		if (take.HasValue)
			_shortTakeProfit = _shortTakeProfit.HasValue ? Math.Min(_shortTakeProfit.Value, take.Value) : take;

		_shortTrailingStop = null;
	}

	private bool CheckLongStops(ICandleMessage candle)
	{
		var trailingDistance = PipToPrice(TrailingStopPips);
		var trailingStep = PipToPrice(TrailingStepPips);

		if (TrailingStopPips > 0 && _currentVolume > 0m)
		{
			var profit = candle.ClosePrice - _entryPrice;
			if (profit > trailingDistance + trailingStep)
			{
				var minAdvance = candle.ClosePrice - (trailingDistance + trailingStep);
				var newStop = candle.ClosePrice - trailingDistance;
				if (!_longTrailingStop.HasValue || _longTrailingStop.Value < minAdvance)
					_longTrailingStop = newStop;
			}
		}

		var effectiveStop = _longStop;
		if (_longTrailingStop.HasValue)
			effectiveStop = effectiveStop.HasValue ? Math.Max(effectiveStop.Value, _longTrailingStop.Value) : _longTrailingStop;

		if (effectiveStop.HasValue && candle.LowPrice <= effectiveStop.Value)
		{
			ExitLong();
			return true;
		}

		if (_longTakeProfit.HasValue && candle.HighPrice >= _longTakeProfit.Value)
		{
			ExitLong();
			return true;
		}

		return false;
	}

	private bool CheckShortStops(ICandleMessage candle)
	{
		var trailingDistance = PipToPrice(TrailingStopPips);
		var trailingStep = PipToPrice(TrailingStepPips);

		if (TrailingStopPips > 0 && _currentVolume < 0m)
		{
			var profit = _entryPrice - candle.ClosePrice;
			if (profit > trailingDistance + trailingStep)
			{
				var maxAdvance = candle.ClosePrice + trailingDistance + trailingStep;
				var newStop = candle.ClosePrice + trailingDistance;
				if (!_shortTrailingStop.HasValue || _shortTrailingStop.Value > maxAdvance)
					_shortTrailingStop = newStop;
			}
		}

		var effectiveStop = _shortStop;
		if (_shortTrailingStop.HasValue)
			effectiveStop = effectiveStop.HasValue ? Math.Min(effectiveStop.Value, _shortTrailingStop.Value) : _shortTrailingStop;

		if (effectiveStop.HasValue && candle.HighPrice >= effectiveStop.Value)
		{
			ExitShort();
			return true;
		}

		if (_shortTakeProfit.HasValue && candle.LowPrice <= _shortTakeProfit.Value)
		{
			ExitShort();
			return true;
		}

		return false;
	}

	private void ExitLong()
	{
		if (_currentVolume <= 0m)
			return;

		SellMarket(_currentVolume);
		_currentVolume = 0m;
		_entryPrice = 0m;
		_longStop = null;
		_longTakeProfit = null;
		_longTrailingStop = null;
	}

	private void ExitShort()
	{
		if (_currentVolume >= 0m)
			return;

		BuyMarket(Math.Abs(_currentVolume));
		_currentVolume = 0m;
		_entryPrice = 0m;
		_shortStop = null;
		_shortTakeProfit = null;
		_shortTrailingStop = null;
	}

	private int DetectCross(decimal emaShort, decimal emaLong)
	{
		decimal prevShort;
		decimal prevLong;
		decimal currentShort;
		decimal currentLong;

		if (TradeBar == TradeBarOptions.Current)
		{
			if (!_emaShortLast.HasValue || !_emaLongLast.HasValue)
				return 0;

			prevShort = _emaShortLast.Value;
			prevLong = _emaLongLast.Value;
			currentShort = emaShort;
			currentLong = emaLong;
		}
		else
		{
			if (!_emaShortLast.HasValue || !_emaLongLast.HasValue || !_emaShortPrevLast.HasValue || !_emaLongPrevLast.HasValue)
				return 0;

			prevShort = _emaShortPrevLast.Value;
			prevLong = _emaLongPrevLast.Value;
			currentShort = _emaShortLast.Value;
			currentLong = _emaLongLast.Value;
		}

		if (prevShort < prevLong && currentShort > currentLong)
			return 1;

		if (prevShort > prevLong && currentShort < currentLong)
			return 2;

		return 0;
	}

	private void UpdateHistory(decimal emaShort, decimal emaLong, decimal? macdCurrent)
	{
		_emaShortPrevLast = _emaShortLast;
		_emaLongPrevLast = _emaLongLast;
		_emaShortLast = emaShort;
		_emaLongLast = emaLong;

		if (macdCurrent.HasValue)
			_macdLast = macdCurrent;
	}

	private decimal PipToPrice(int pips)
	{
		if (pips <= 0)
			return 0m;

		var step = Security?.PriceStep ?? 1m;
		var decimals = Security?.Decimals ?? 0;
		var multiplier = (decimals == 3 || decimals == 5) ? 10m : 1m;

		return pips * step * multiplier;
	}

	private void CreatePendingOrders(DateTimeOffset time, decimal price, Sides side)
	{
		_pendingOrders.Clear();

		if (HedgeLevelPips <= 0)
			return;

		var distance = PipToPrice(HedgeLevelPips);
		if (distance <= 0m)
			return;

		var expiration = PendingExpirationSeconds > 0
			? time + TimeSpan.FromSeconds(PendingExpirationSeconds)
			: DateTimeOffset.MaxValue;

		var stopOffset = StopLossPips > 0 ? PipToPrice(StopLossPips) : 0m;
		var takeOffset = TakeProfitPips > 0 ? PipToPrice(TakeProfitPips) : 0m;

		for (var i = 1; i <= PendingOrderCount; i++)
		{
			var levelPrice = side == Sides.Buy
				? price + distance * i
				: price - distance * i;

			decimal? stop = null;
			decimal? take = null;

			if (StopLossPips > 0)
				stop = side == Sides.Buy
					? levelPrice - stopOffset
					: levelPrice + stopOffset;

			if (TakeProfitPips > 0)
				take = side == Sides.Buy
					? levelPrice + takeOffset
					: levelPrice - takeOffset;

			_pendingOrders.Add(new PendingOrder
			{
				Side = side,
				Price = levelPrice,
				StopLoss = stop,
				TakeProfit = take,
				ExpireTime = expiration
			});
		}
	}
}