Ver no GitHub

Dual Lot Step Hedge Strategy

Overview

The Dual Lot Step Hedge Strategy is a C# port of the MetaTrader 5 expert advisors "x1 lot from high to low" and "x1 lot from low to high" (folder MQL/19543). The original robots immediately open a hedged basket of buy and sell positions, cycle the order volume after every new entry, and close the entire basket once a fixed profit target is reached. This implementation reproduces that behaviour on top of the StockSharp high-level API while exposing clean parameters and detailed state management.

Two operating modes are available:

  • HighToLow – starts with the maximum lot multiplier, opens the first hedged basket with the largest volume, and then decreases to the next lot step after the first entries.
  • LowToHigh – begins with the minimal lot step, increases the lot size after every new entry until the configured multiplier is reached, and then keeps trading at that size.

The strategy keeps both buy and sell legs alive simultaneously, manages stop-loss and take-profit levels per leg, and monitors the portfolio equity to enforce a basket-wide profit target.

Trading Logic

  1. When no positions exist the strategy opens both a long and a short market order using the current lot size.
  2. If exactly one leg is active (for example, the opposite side was stopped out), the missing leg is re-opened at market with the current lot size.
  3. After every successful entry the lot size is updated depending on the selected mode (HighToLow or LowToHigh).
  4. Per-leg protective exits are evaluated on every incoming trade tick:
    • A long leg is closed if price reaches its stop-loss (StopLossPips below the average long entry) or its take-profit (TakeProfitPips above the average entry).
    • A short leg is closed if price reaches its stop-loss (StopLossPips above the average short entry) or its take-profit (TakeProfitPips below the average entry).
  5. Once the portfolio equity gain exceeds MinProfit, the strategy closes all remaining positions and resets the lot state to the mode’s starting size.
  6. Safety logic closes the basket and resets everything if more than one buy or sell position is unexpectedly detected.

All orders are submitted via the high-level BuyMarket and SellMarket helpers. The strategy tracks fills with OnOwnTradeReceived, maintains aggregated exposure per leg, and prevents duplicate orders while entries or exits are still pending.

Parameters

Parameter Description
LotMultiplier Maximum lot multiplier expressed in minimal volume steps (default 10).
StopLossPips Stop-loss distance in pips for each leg (default 50). Set to 0 to disable.
TakeProfitPips Take-profit distance in pips for each leg (default 150). Set to 0 to disable.
MinProfit Basket profit target in account currency. Once the equity gain exceeds this value all positions are closed (default 27).
ScalingMode Lot stepping behaviour. HighToLow mirrors the “x1 lot from high to low” EA, LowToHigh mirrors “x1 lot from low to high”.

The strategy automatically derives the minimal volume step from Security.VolumeStep and computes pip value using the security price step (with the traditional 4/5-digit forex adjustment).

Reset and Volume Cycling

  • HighToLow – opens the first basket with the highest volume (VolumeStep * LotMultiplier). After any entry the internal volume is reduced by one step. When the basket profit target is reached, the volume is reset to 0 so the next cycle starts from the maximum again.
  • LowToHigh – starts from the minimal lot step. After each entry the lot is increased by one step until the multiplier ceiling is reached. When the basket profit target is hit the volume is reset to the minimal step.

Usage Notes

  • The strategy subscribes to tick trades (DataType.Ticks) because the original MetaTrader bots run on tick events. Configure the history provider or live connector accordingly.
  • Stop-loss and take-profit checks happen inside the algorithm, so no additional protective orders are registered on the exchange.
  • Because both legs are opened at market, the strategy performs best on brokers that support hedged positions and small spreads. On netting venues it will still function but legs effectively offset each other until one of them is closed by the internal logic.
  • The default parameters copy the original MQL settings. Adjust them carefully: hedging high volumes can generate significant drawdowns before the basket profit target is met.

Mapping to the Original MQL Logic

MetaTrader Variable C# Property / Behaviour
InpLots LotMultiplier with automatic volume-step handling.
InpStopLoss & InpTakeProfit StopLossPips and TakeProfitPips with pip conversion based on PriceStep.
InpMinProfit MinProfit and the portfolio equity check.
LotCheck LotCheck helper that enforces the minimum step and maximum volume.
CalculatePositions Internal long/short exposure tracking through OnOwnTradeReceived.
CloseAllPositions() CloseAllPositions method with pending-order coordination and state reset.

Risk Management Considerations

The strategy intentionally keeps both long and short positions open, which causes continuous exposure to spread costs and swap rates. Before running on real capital:

  • Validate the behaviour in the StockSharp emulator or in paper trading.
  • Ensure your broker supports hedging; otherwise the long/short legs will be netted immediately.
  • Tune the stop-loss, take-profit, and profit target values to the instrument’s volatility.
  • Monitor margin usage, because simultaneous long/short legs double the nominal exposure.

Files

  • CS/DualLotStepHedgeStrategy.cs – StockSharp strategy implementation with extensive inline comments.
  • README_ru.md – Russian translation with detailed instructions.
  • README_zh.md – Chinese translation with detailed instructions.
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>
/// Recreates the "x1 lot from high to low" and "x1 lot from low to high" MetaTrader robots.
/// Opens hedged long/short positions with adjustable lot cycling and closes the basket once
/// a profit target is achieved.
/// </summary>
public class DualLotStepHedgeStrategy : Strategy
{
	private readonly StrategyParam<int> _lotMultiplier;
	private readonly StrategyParam<decimal> _stopLossPips;
	private readonly StrategyParam<decimal> _takeProfitPips;
	private readonly StrategyParam<decimal> _minProfit;
	private readonly StrategyParam<LotScalingModes> _scalingMode;

	private decimal _volumeStep;
	private decimal _maxVolume;
	private decimal _currentVolume;
	private decimal _pipValue;
	private decimal _initialEquity;

	private decimal _longVolume;
	private decimal _shortVolume;
	private decimal _longAveragePrice;
	private decimal _shortAveragePrice;

	private bool _longEntryInProgress;
	private bool _shortEntryInProgress;
	private bool _longExitInProgress;
	private bool _shortExitInProgress;

	private decimal _pendingLongEntryVolume;
	private decimal _pendingShortEntryVolume;
	private decimal _pendingLongExitVolume;
	private decimal _pendingShortExitVolume;

	private bool _resetRequested;

	/// <summary>
	/// Defines the lot stepping mode that matches the original MetaTrader experts.
	/// </summary>
	public enum LotScalingModes
	{
		/// <summary>
		/// Start with the maximum lot multiplier and drop to the next step after the first cycle.
		/// </summary>
		HighToLow,

		/// <summary>
		/// Start with the minimum lot step and grow until the configured multiplier is reached.
		/// </summary>
		LowToHigh,
	}

	/// <summary>
	/// Maximum lot multiplier expressed in minimal volume steps.
	/// </summary>
	public int LotMultiplier
	{
		get => _lotMultiplier.Value;
		set => _lotMultiplier.Value = value;
	}

	/// <summary>
	/// Stop loss distance in pips from the average entry price of the leg.
	/// </summary>
	public decimal StopLossPips
	{
		get => _stopLossPips.Value;
		set => _stopLossPips.Value = value;
	}

	/// <summary>
	/// Take profit distance in pips from the average entry price of the leg.
	/// </summary>
	public decimal TakeProfitPips
	{
		get => _takeProfitPips.Value;
		set => _takeProfitPips.Value = value;
	}

	/// <summary>
	/// Basket profit target in account currency.
	/// </summary>
	public decimal MinProfit
	{
		get => _minProfit.Value;
		set => _minProfit.Value = value;
	}

	/// <summary>
	/// Selected lot stepping mode.
	/// </summary>
	public LotScalingModes ScalingMode
	{
		get => _scalingMode.Value;
		set => _scalingMode.Value = value;
	}

	/// <summary>
	/// Initializes a new instance of <see cref="DualLotStepHedgeStrategy"/>.
	/// </summary>
	public DualLotStepHedgeStrategy()
	{
		_lotMultiplier = Param(nameof(LotMultiplier), 10)
		.SetGreaterThanZero()
		.SetDisplay("Lot Multiplier", "Maximum lot multiplier over the minimal step", "Trading")
		
		.SetOptimize(1, 20, 1);

		_stopLossPips = Param(nameof(StopLossPips), 50m)
		.SetDisplay("Stop Loss (pips)", "Stop loss distance for each leg", "Risk")
		
		.SetOptimize(10m, 200m, 10m);

		_takeProfitPips = Param(nameof(TakeProfitPips), 150m)
		.SetDisplay("Take Profit (pips)", "Take profit distance for each leg", "Risk")
		
		.SetOptimize(20m, 400m, 20m);

		_minProfit = Param(nameof(MinProfit), 27m)
		.SetDisplay("Basket Profit", "Target profit in account currency", "Trading")
		
		.SetOptimize(5m, 200m, 5m);

		_scalingMode = Param(nameof(ScalingMode), LotScalingModes.HighToLow)
		.SetDisplay("Scaling Mode", "How the lot size evolves after entries", "Trading");
	}

	/// <inheritdoc />
	public override IEnumerable<(Security sec, DataType dt)> GetWorkingSecurities()
	{
		return [(Security, DataType.Ticks)];
	}

	/// <inheritdoc />
	protected override void OnReseted()
	{
		base.OnReseted();

		_volumeStep = 0m;
		_maxVolume = 0m;
		_currentVolume = 0m;
		_pipValue = 0m;
		_initialEquity = 0m;

		_longVolume = 0m;
		_shortVolume = 0m;
		_longAveragePrice = 0m;
		_shortAveragePrice = 0m;

		_longEntryInProgress = false;
		_shortEntryInProgress = false;
		_longExitInProgress = false;
		_shortExitInProgress = false;

		_pendingLongEntryVolume = 0m;
		_pendingShortEntryVolume = 0m;
		_pendingLongExitVolume = 0m;
		_pendingShortExitVolume = 0m;

		_resetRequested = false;
	}

	/// <inheritdoc />
	protected override void OnStarted2(DateTime time)
	{
		base.OnStarted2(time);

		_volumeStep = Security.VolumeStep ?? 0m;
			if (_volumeStep <= 0m)
		_volumeStep = 1m;

		_maxVolume = LotCheck(_volumeStep * LotMultiplier);
		if (_maxVolume <= 0m)
		_maxVolume = _volumeStep;

		_currentVolume = ScalingMode == LotScalingModes.HighToLow ? _maxVolume : _volumeStep;
		_pipValue = CalculatePipValue();

		var subscription = SubscribeCandles(TimeSpan.FromMinutes(5).TimeFrame());
		subscription.Bind(ProcessCandle).Start();
	}

		private void ProcessCandle(ICandleMessage candle)
		{
			if (candle.State != CandleStates.Finished)
				return;

			var price = candle.ClosePrice;

			if (_volumeStep <= 0m)
				return;

			if (_initialEquity <= 0m)
				_initialEquity = Portfolio.CurrentValue ?? 0m;

			CheckProtectiveLevels(price);

			if (_longExitInProgress || _shortExitInProgress)
				return;

			if (CheckProfitTarget())
				return;

			ResetCurrentVolumeIfNeeded();

			var buyCount = _longVolume > 0m ? 1 : 0;
			var sellCount = _shortVolume > 0m ? 1 : 0;

			if (buyCount > 1 || sellCount > 1)
			{
				CloseAllPositions();
				return;
			}

			if (_longEntryInProgress || _shortEntryInProgress)
				return;

			if (buyCount == 0 && sellCount == 0)
			{
				TryOpenHedge();
			}
			else if (buyCount == 1 && sellCount == 0)
			{
				OpenShortIfNeeded();
			}
			else if (buyCount == 0 && sellCount == 1)
			{
				OpenLongIfNeeded();
			}
		}

	private bool CheckProfitTarget()
	{
		if (_initialEquity <= 0m || MinProfit <= 0m)
		return false;

		var currentEquity = Portfolio.CurrentValue ?? 0m;
		if (currentEquity - _initialEquity >= MinProfit)
		{
			CloseAllPositions();
			return true;
		}

		return false;
	}

	private void TryOpenHedge()
	{
		if (_longEntryInProgress || _shortEntryInProgress)
		return;

		var volume = LotCheck(_currentVolume);
		if (volume <= 0m)
		return;

		var buyOk = ExecuteBuy(volume, true);
		var sellOk = ExecuteSell(volume, true);

		if (buyOk && sellOk)
		AdjustVolumeAfterEntry();
	}

	private void OpenLongIfNeeded()
	{
		if (_longEntryInProgress)
		return;

		var volume = LotCheck(_currentVolume);
		if (volume <= 0m)
		return;

		if (ExecuteBuy(volume, true))
		AdjustVolumeAfterEntry();
	}

	private void OpenShortIfNeeded()
	{
		if (_shortEntryInProgress)
		return;

		var volume = LotCheck(_currentVolume);
		if (volume <= 0m)
		return;

		if (ExecuteSell(volume, true))
		AdjustVolumeAfterEntry();
	}

	private void AdjustVolumeAfterEntry()
	{
		if (ScalingMode == LotScalingModes.HighToLow)
		{
			_currentVolume = LotCheck(_currentVolume - _volumeStep);
		}
		else
		{
			_currentVolume = LotCheck(_currentVolume + _volumeStep);
		}
	}

	private void CloseAllPositions()
	{
		if (_longVolume <= 0m && _shortVolume <= 0m && !_longExitInProgress && !_shortExitInProgress)
		{
			_resetRequested = true;
			ApplyResetIfFlat();
			return;
		}

		if (_longVolume > 0m && !_longExitInProgress)
		{
			if (ExecuteSell(_longVolume, false))
			_resetRequested = true;
		}

		if (_shortVolume > 0m && !_shortExitInProgress)
		{
			if (ExecuteBuy(_shortVolume, false))
			_resetRequested = true;
		}
	}

	private void CloseLong()
	{
		if (_longVolume <= 0m || _longExitInProgress)
		return;

		ExecuteSell(_longVolume, false);
	}

	private void CloseShort()
	{
		if (_shortVolume <= 0m || _shortExitInProgress)
		return;

		ExecuteBuy(_shortVolume, false);
	}

	private bool ExecuteBuy(decimal volume, bool openingLong)
	{
		if (volume <= 0m)
		return false;

		var order = BuyMarket(volume);
		if (order == null)
		return false;

		if (openingLong)
		{
			_longEntryInProgress = true;
			_pendingLongEntryVolume += volume;
		}
		else
		{
			_shortExitInProgress = true;
			_pendingShortExitVolume += volume;
		}

		return true;
	}

	private bool ExecuteSell(decimal volume, bool openingShort)
	{
		if (volume <= 0m)
		return false;

		var order = SellMarket(volume);
		if (order == null)
		return false;

		if (openingShort)
		{
			_shortEntryInProgress = true;
			_pendingShortEntryVolume += volume;
		}
		else
		{
			_longExitInProgress = true;
			_pendingLongExitVolume += volume;
		}

		return true;
	}

	private void CheckProtectiveLevels(decimal price)
	{
		if (_pipValue <= 0m)
		return;

		if (_longVolume > 0m && !_longExitInProgress)
		{
			var stop = StopLossPips > 0m ? _longAveragePrice - StopLossPips * _pipValue : decimal.MinValue;
			var take = TakeProfitPips > 0m ? _longAveragePrice + TakeProfitPips * _pipValue : decimal.MaxValue;

			if (StopLossPips > 0m && price <= stop)
			{
				CloseLong();
				return;
			}

			if (TakeProfitPips > 0m && price >= take)
			{
				CloseLong();
				return;
			}
		}

		if (_shortVolume > 0m && !_shortExitInProgress)
		{
			var stop = StopLossPips > 0m ? _shortAveragePrice + StopLossPips * _pipValue : decimal.MaxValue;
			var take = TakeProfitPips > 0m ? _shortAveragePrice - TakeProfitPips * _pipValue : decimal.MinValue;

			if (StopLossPips > 0m && price >= stop)
			{
				CloseShort();
				return;
			}

			if (TakeProfitPips > 0m && price <= take)
			{
				CloseShort();
			}
		}
	}

	private void ResetCurrentVolumeIfNeeded()
	{
		if (ScalingMode == LotScalingModes.HighToLow)
		{
			if (_currentVolume < _volumeStep)
			_currentVolume = _maxVolume;
		}
		else
		{
			if (_currentVolume < _volumeStep)
			_currentVolume = _volumeStep;
			else if (_currentVolume > _maxVolume)
			_currentVolume = _volumeStep;
		}
	}

	private decimal LotCheck(decimal volume)
	{
		if (volume <= 0m)
		return 0m;

		var step = _volumeStep;
		if (step <= 0m)
		return 0m;

		var ratio = Math.Floor(volume / step);
		var normalized = ratio * step;

		if (normalized < step)
		normalized = 0m;

		if (normalized > _maxVolume)
		normalized = _maxVolume;

		return normalized;
	}

	private decimal CalculatePipValue()
	{
		var step = Security.PriceStep ?? 0m;
		if (step <= 0m)
		return 1m;

		double stepDouble;
		try
		{
			stepDouble = Convert.ToDouble(step);
		}
		catch
		{
			return step;
		}

		if (stepDouble <= 0d)
		return step;

		var decimals = (int)Math.Round(-Math.Log10(stepDouble));
		if (decimals == 3 || decimals == 5)
		return step * 10m;

		return step;
	}

	private void ApplyResetIfFlat()
	{
		if (!_resetRequested)
		return;

		if (_longVolume > 0m || _shortVolume > 0m)
		return;

			if (_longExitInProgress || _shortExitInProgress)
		return;

		if (_pendingLongEntryVolume > 0m || _pendingShortEntryVolume > 0m)
		return;

		_resetRequested = false;
		_initialEquity = 0m;

		if (ScalingMode == LotScalingModes.HighToLow)
		{
			_currentVolume = 0m;
		}
		else
		{
			_currentVolume = _volumeStep;
		}
	}

	private void ApplyLongOpen(decimal volume, decimal price)
	{
		if (volume <= 0m)
		return;

		var total = _longVolume + volume;
		_longAveragePrice = _longVolume <= 0m
		? price
		: (_longAveragePrice * _longVolume + price * volume) / total;
		_longVolume = total;
	}

	private void ApplyShortOpen(decimal volume, decimal price)
	{
		if (volume <= 0m)
		return;

		var total = _shortVolume + volume;
		_shortAveragePrice = _shortVolume <= 0m
		? price
		: (_shortAveragePrice * _shortVolume + price * volume) / total;
		_shortVolume = total;
	}

	private void ApplyLongClose(decimal volume)
	{
		if (volume <= 0m || _longVolume <= 0m)
		return;

		var closed = Math.Min(_longVolume, volume);
		_longVolume -= closed;
		if (_longVolume <= 0m)
		{
			_longVolume = 0m;
			_longAveragePrice = 0m;
		}
	}

	private void ApplyShortClose(decimal volume)
	{
		if (volume <= 0m || _shortVolume <= 0m)
		return;

		var closed = Math.Min(_shortVolume, volume);
		_shortVolume -= closed;
		if (_shortVolume <= 0m)
		{
			_shortVolume = 0m;
			_shortAveragePrice = 0m;
		}
	}

	/// <inheritdoc />
	protected override void OnOwnTradeReceived(MyTrade trade)
	{
		base.OnOwnTradeReceived(trade);

		if (trade.Order.Security != Security)
		return;

		var volume = trade.Trade.Volume;
		if (volume <= 0m)
		return;

		var price = trade.Trade.Price;

		if (trade.Order.Side == Sides.Buy)
		{
			ProcessBuyTrade(volume, price);
		}
		else if (trade.Order.Side == Sides.Sell)
		{
			ProcessSellTrade(volume, price);
		}

		ApplyResetIfFlat();
	}

	private void ProcessBuyTrade(decimal volume, decimal price)
	{
		var remaining = volume;

		if (_pendingShortExitVolume > 0m)
		{
			var closing = Math.Min(_pendingShortExitVolume, remaining);
			ApplyShortClose(closing);
			_pendingShortExitVolume -= closing;
			remaining -= closing;

			if (_pendingShortExitVolume <= 0m)
			_shortExitInProgress = false;
		}

		if (remaining <= 0m)
		return;

		if (_pendingLongEntryVolume > 0m)
		{
			var opening = Math.Min(_pendingLongEntryVolume, remaining);
			ApplyLongOpen(opening, price);
			_pendingLongEntryVolume -= opening;
			remaining -= opening;

			if (_pendingLongEntryVolume <= 0m)
			_longEntryInProgress = false;
		}

		if (remaining > 0m)
		ApplyLongOpen(remaining, price);
	}

	private void ProcessSellTrade(decimal volume, decimal price)
	{
		var remaining = volume;

		if (_pendingLongExitVolume > 0m)
		{
			var closing = Math.Min(_pendingLongExitVolume, remaining);
			ApplyLongClose(closing);
			_pendingLongExitVolume -= closing;
			remaining -= closing;

			if (_pendingLongExitVolume <= 0m)
			_longExitInProgress = false;
		}

		if (remaining <= 0m)
		return;

		if (_pendingShortEntryVolume > 0m)
		{
			var opening = Math.Min(_pendingShortEntryVolume, remaining);
			ApplyShortOpen(opening, price);
			_pendingShortEntryVolume -= opening;
			remaining -= opening;

			if (_pendingShortEntryVolume <= 0m)
			_shortEntryInProgress = false;
		}

		if (remaining > 0m)
		ApplyShortOpen(remaining, price);
	}
}