GitHub で見る

EMA クロス・コンテスト・ヘッジ戦略

概要

  • StockSharp 高レベル API を使用して MetaTrader 戦略「EMA Cross Contest Hedged」を再現します。
  • 一対の指数移動平均(EMA)でトレードし、オプションで MACD メインラインで確認します。
  • 各エントリー後に保留中のストップオーダーの「ヘッジ」レベルの階段を構築し、強いトレンドにスケールインします。
  • pips で表された静的なストップロス/テイクプロフィットレベルと、最小利益後に活性化するトレーリングストップを適用します。
  • シグナルに現在の完成ローソク足または前のクローズしたローソク足のどちらを使用するかを選択できます。

インジケーターとデータ

  • 設定可能な長さの短い EMA(デフォルト 4)。
  • 設定可能な長さの長い EMA(デフォルト 24);短い期間は長い期間より小さくなければなりません。
  • MACD (4, 24, 12) メインライン はオプションの確認フィルターとして使用されます。
  • CandleType パラメーターが提供する任意の時間軸で動作します(デフォルトは 15 分足)。

エントリー条件

  1. 設定された時間軸の完成したローソク足を待ちます。
  2. 速い EMA と遅い EMA の値を計算します。TradeBar に応じて、以下のどちらかを使用してクロスオーバーを判定します:
    • 最新の完成したローソク足と前のローソク足(Current)。
    • 前のローソク足とさらに前のローソク足(Previous、デフォルト)。
  3. 速い EMA が遅い EMA を上抜けたときにロングシグナルを生成します。UseMacdFilter が有効な場合、同じバーの MACD 値が非負でなければなりません。
  4. 速い EMA が遅い EMA を下抜けたときにショートシグナルを生成します。MACD フィルターが有効な場合、MACD 値は非正でなければなりません。
  5. 露出がない場合のみ(以前のすべてのトレードがフラット)新規ポジションを開きます。
  6. サイズ OrderVolume の成行注文を実行します。エントリー後、戦略は:
    • 約定価格から StopLossPipsTakeProfitPips だけオフセットされたストップロスとテイクプロフィットのレベルを保存します。
    • トレーリングストップ状態をリセットします。
    • トレード方向に HedgeLevelPips の間隔で 4 つのヘッジングストップオーダーを作成します。各保留注文は同じストップロス/テイクプロフィット距離を継承し、価格が早くそこに到達しない限り PendingExpirationSeconds 秒後に期限切れになります。

エグジット管理

  • ストップロス / テイクプロフィット: 戦略はバー内の高値と安値を監視します。価格が保存されたストップまたはターゲットに触れた場合、全ポジションがクローズされます。
  • トレーリングストップ: 利益が TrailingStopPips + TrailingStepPips を超えると、ストップは最後のクローズから TrailingStopPips 後ろに追従します。ロングポジションは上方に、ショートポジションは下方に追従します。
  • 逆クロスオーバー: CloseOppositePositions が有効な場合、逆の EMA クロスオーバーが検出されるとすぐにポジションがクローズされます。
  • 保留中の階段: 各ヘッジング注文は、価格がストップレベルを越えると追加の成行注文に変換されます。新しい約定は平均エントリー価格を調整し、保護レベルを適切に調整します。

パラメーター

名前 デフォルト 説明
OrderVolume 0.1 すべての成行注文またはストップ注文に使用される注文サイズ。
StopLossPips 140 pips 単位のストップ距離。0 に設定すると無効になります。
TakeProfitPips 120 pips 単位のテイクプロフィット距離。0 に設定すると無効になります。
TrailingStopPips 30 pips 単位のトレーリングストップ距離。0 に設定するとトレーリングを無効にします。
TrailingStepPips 1 トレーリングストップが再び締まる前の最小追加利益(pips)。
HedgeLevelPips 6 段階的なヘッジングストップ注文間の距離。
CloseOppositePositions false 逆のクロスオーバーが現れたときにアクティブなポジションをクローズします。
UseMacdFilter false MACD 確認を要求します(ロングは >= 0、ショートは <= 0)。
PendingExpirationSeconds 65535 各ヘッジングストップ注文の有効期間(秒)。
ShortMaPeriod 4 短い EMA の長さ。LongMaPeriod より小さくなければなりません。
LongMaPeriod 24 長い EMA の長さ。
TradeBar Previous クロスオーバーを検出するために使用するバーのペアを決定します。
CandleType 15 分 データプロバイダーからリクエストされる時間軸。

補足事項

  • pips は Security.PriceStep を乗算して変換され、MetaTrader の pip 規約に合わせるために 3 桁および 5 桁の小数点を持つ商品には自動的に 10 の係数が適用されます。
  • 保留中のヘッジング注文は戦略内部でシミュレートされ、ローソク足の範囲がそのレベルに触れるとすぐに実行されます。
  • StartProtection() は 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>
/// 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
			});
		}
	}
}