在 GitHub 上查看

EMA Cross Contest Hedged 策略

概览

  • 将 MetaTrader 平台上的 “EMA Cross Contest Hedged” 策略迁移到 StockSharp 的高级 API。
  • 使用一组快慢 EMA 判断趋势,并可选用 MACD 主线作为信号过滤器。
  • 每次入场后会建立四个分层的止损挂单(对冲阶梯),在行情延续时逐步加仓。
  • 通过以点(pip)表示的固定止损/止盈以及可调节的跟踪止损控制风险。
  • TradeBar 参数允许选择基于当前已完成 K 线还是上一根 K 线来判定交叉信号。

指标与数据

  • 可配置长度的快 EMA(默认 4)。
  • 可配置长度的慢 EMA(默认 24),要求快 EMA 周期小于慢 EMA 周期。
  • MACD(4, 24, 12) 主线,用于可选的方向确认。
  • 支持任何由 CandleType 参数提供的时间周期,默认使用 15 分钟 K 线。

入场逻辑

  1. 等待所选时间周期的 K 线收盘。
  2. 计算快、慢 EMA 的最新值。根据 TradeBar 选项使用以下其中一种组合来判断交叉:
    • Current:使用最新收盘与上一根收盘。
    • Previous(默认):使用上一根收盘与更早一根收盘。
  3. 当快 EMA 向上穿越慢 EMA 时生成多头信号;若启用 UseMacdFilter,对应 K 线的 MACD 值需大于等于零。
  4. 当快 EMA 向下穿越慢 EMA 时生成空头信号;若启用过滤器,MACD 值需小于等于零。
  5. 仅在当前无持仓时开立新仓位。
  6. OrderVolume 指定的手数市价入场,随后:
    • 按照 StopLossPipsTakeProfitPips 计算并保存止损/止盈价格。
    • 重置跟踪止损状态。
    • HedgeLevelPips 的步长在趋势方向挂出四个止损单,带有相同的止损与止盈设置,每个挂单的有效期由 PendingExpirationSeconds 控制。

出场与持仓管理

  • 止损 / 止盈: 监控 K 线内的最高价和最低价,触及任一保护价位即平掉全部仓位。
  • 跟踪止损: 当浮动盈利超过 TrailingStopPips + TrailingStepPips 时,将止损上移/下移至距离最新收盘价 TrailingStopPips 的位置,随后继续随行情移动。
  • 反向交叉: 如果开启 CloseOppositePositions,在出现反向 EMA 交叉时立即平仓。
  • 对冲阶梯: 每个挂单被触发后会执行等量市价单,加仓的同时根据成交价调整平均持仓价及保护水平。

参数说明

参数 默认值 说明
OrderVolume 0.1 每次市价单和挂单的交易量。
StopLossPips 140 止损距离(点)。设置为 0 表示不使用固定止损。
TakeProfitPips 120 止盈距离(点)。设置为 0 表示不使用固定止盈。
TrailingStopPips 30 跟踪止损距离(点)。0 表示关闭跟踪止损。
TrailingStepPips 1 每次更新跟踪止损前所需的额外盈利(点)。
HedgeLevelPips 6 分层挂单之间的间隔(点)。
CloseOppositePositions false 出现反向交叉时是否立即平仓。
UseMacdFilter false 是否要求 MACD 主线确认(多头 >= 0,空头 <= 0)。
PendingExpirationSeconds 65535 每个对冲挂单的有效时间(秒)。
ShortMaPeriod 4 快 EMA 周期,必须小于 LongMaPeriod
LongMaPeriod 24 慢 EMA 周期。
TradeBar Previous 交叉检测所使用的 K 线组合。
CandleType 15 分钟 策略请求的数据时间周期。

额外说明

  • 点值换算方式为 PriceStep × 点数,对于 3 或 5 位小数的品种会自动乘以 10,以符合 MetaTrader 的点值定义。
  • 对冲挂单在策略内部模拟,当 K 线的最高价或最低价触及某一级别时立即执行。
  • 策略在启动时调用 StartProtection(),启用 StockSharp 自带的保护机制。
  • 多头与空头拥有独立的跟踪止损状态,以贴近原始 MQL 实现的对冲逻辑。
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
			});
		}
	}
}