Auf GitHub ansehen

Dual-Lot-Schritt-Hedge-Strategie

Übersicht

Die Dual-Lot-Schritt-Hedge-Strategie ist ein C#-Port der MetaTrader 5-Experten "x1 lot from high to low" und "x1 lot from low to high" (Ordner MQL/19543). Die ursprünglichen Roboter öffnen sofort einen gehedgten Korb aus Kauf- und Verkaufspositionen, zyklieren das Ordervolumen nach jedem neuen Einstieg und schließen den gesamten Korb, sobald ein festes Gewinnziel erreicht wird. Diese Implementierung reproduziert dieses Verhalten auf der StockSharp High-Level-API unter Bereitstellung sauberer Parameter und detailliertem Zustandsmanagement.

Zwei Betriebsmodi sind verfügbar:

  • HighToLow – startet mit dem maximalen Lot-Multiplikator, öffnet den ersten gehedgten Korb mit dem größten Volumen und verringert dann auf den nächsten Lot-Schritt nach den ersten Einstiegen.
  • LowToHigh – beginnt mit dem minimalen Lot-Schritt, erhöht die Lotgröße nach jedem neuen Einstieg, bis der konfigurierte Multiplikator erreicht ist, und handelt dann bei dieser Größe.

Die Strategie hält sowohl Kauf- als auch Verkaufsbeine gleichzeitig aktiv, verwaltet Stop-Loss- und Take-Profit-Niveaus pro Bein und überwacht das Portfolio-Eigenkapital zur Durchsetzung eines korbweiten Gewinnziels.

Handelslogik

  1. Wenn keine Positionen existieren, öffnet die Strategie sowohl eine Long- als auch eine Short-Marktorder mit der aktuellen Lotgröße.
  2. Wenn genau ein Bein aktiv ist (zum Beispiel wurde die entgegengesetzte Seite gestoppt), wird das fehlende Bein zum Markt mit der aktuellen Lotgröße wieder geöffnet.
  3. Nach jedem erfolgreichen Einstieg wird die Lotgröße abhängig vom gewählten Modus (HighToLow oder LowToHigh) aktualisiert.
  4. Pro-Bein-Schutzausstiege werden bei jedem eingehenden Trade-Tick ausgewertet:
    • Ein Long-Bein wird geschlossen, wenn der Preis seinen Stop-Loss (StopLossPips unter dem durchschnittlichen Long-Einstieg) oder seinen Take-Profit (TakeProfitPips über dem durchschnittlichen Einstieg) erreicht.
    • Ein Short-Bein wird geschlossen, wenn der Preis seinen Stop-Loss (StopLossPips über dem durchschnittlichen Short-Einstieg) oder seinen Take-Profit (TakeProfitPips unter dem durchschnittlichen Einstieg) erreicht.
  5. Sobald der Portfolio-Eigenkapitalgewinn MinProfit überschreitet, schließt die Strategie alle verbleibenden Positionen und setzt den Lot-Zustand auf die Startgröße des Modus zurück.
  6. Sicherheitslogik schließt den Korb und setzt alles zurück, wenn mehr als eine Kauf- oder Verkaufsposition unerwartet erkannt wird.

Alle Orders werden über die High-Level-Helfer BuyMarket und SellMarket eingereicht. Die Strategie verfolgt Fills mit OnOwnTradeReceived, pflegt aggregierte Exposure pro Bein und verhindert doppelte Orders, während Einstiege oder Ausstiege noch ausstehen.

Parameter

Parameter Beschreibung
LotMultiplier Maximaler Lot-Multiplikator ausgedrückt in minimalen Volumenschritten (Standard 10).
StopLossPips Stop-Loss-Distanz in Pips für jedes Bein (Standard 50). Auf 0 setzen, um zu deaktivieren.
TakeProfitPips Take-Profit-Distanz in Pips für jedes Bein (Standard 150). Auf 0 setzen, um zu deaktivieren.
MinProfit Korbgewinnziel in Kontowährung. Sobald der Eigenkapitalgewinn diesen Wert überschreitet, werden alle Positionen geschlossen (Standard 27).
ScalingMode Lot-Schrittverhalten. HighToLow spiegelt den "x1 lot from high to low"-EA, LowToHigh spiegelt "x1 lot from low to high".

Die Strategie leitet automatisch den minimalen Volumenschritt aus Security.VolumeStep ab und berechnet den Pip-Wert mit dem Sicherheitspreisschritt (mit der traditionellen 4/5-stelligen Forex-Anpassung).

Zurücksetzen und Volumen-Cycling

  • HighToLow – öffnet den ersten Korb mit dem höchsten Volumen (VolumeStep * LotMultiplier). Nach jedem Einstieg wird das interne Volumen um einen Schritt reduziert. Wenn das Korbgewinnziel erreicht wird, wird das Volumen auf 0 zurückgesetzt, damit der nächste Zyklus wieder vom Maximum beginnt.
  • LowToHigh – startet vom minimalen Lot-Schritt. Nach jedem Einstieg wird das Lot um einen Schritt erhöht, bis die Multiplikator-Obergrenze erreicht ist. Wenn das Korbgewinnziel erreicht wird, wird das Volumen auf den minimalen Schritt zurückgesetzt.

Verwendungshinweise

  • Die Strategie abonniert Tick-Trades (DataType.Ticks), weil die ursprünglichen MetaTrader-Bots auf Tick-Ereignissen laufen. Konfigurieren Sie den Historiendatenanbieter oder Live-Connector entsprechend.
  • Stop-Loss- und Take-Profit-Prüfungen erfolgen innerhalb des Algorithmus, daher werden keine zusätzlichen Schutzorders am Exchange registriert.
  • Da beide Beine zu Marktpreisen eröffnet werden, funktioniert die Strategie am besten bei Brokern, die gehedgte Positionen und kleine Spreads unterstützen. Auf Netting-Plätzen wird sie weiterhin funktionieren, aber Beine kompensieren sich effektiv, bis eines davon durch die interne Logik geschlossen wird.
  • Die Standardparameter kopieren die ursprünglichen MQL-Einstellungen. Passen Sie sie sorgfältig an: Das Hedgen hoher Volumen kann erhebliche Drawdowns erzeugen, bevor das Korbgewinnziel erreicht wird.

Zuordnung zur ursprünglichen MQL-Logik

MetaTrader Variable C#-Eigenschaft / Verhalten
InpLots LotMultiplier mit automatischer Volumenschritt-Behandlung.
InpStopLoss & InpTakeProfit StopLossPips und TakeProfitPips mit Pip-Konvertierung basierend auf PriceStep.
InpMinProfit MinProfit und die Portfolio-Eigenkapital-Prüfung.
LotCheck LotCheck-Helfer, der den Mindestschritt und das maximale Volumen durchsetzt.
CalculatePositions Interne Long/Short-Exposure-Verfolgung durch OnOwnTradeReceived.
CloseAllPositions() CloseAllPositions-Methode mit ausstehender Order-Koordination und Zustandsreset.

Risikomanagement-Überlegungen

Die Strategie hält absichtlich sowohl Long- als auch Short-Positionen offen, was zu kontinuierlicher Exposure gegenüber Spread-Kosten und Swap-Raten führt. Vor dem Betrieb mit echtem Kapital:

  • Das Verhalten im StockSharp-Emulator oder im Paper-Trading validieren.
  • Sicherstellen, dass Ihr Broker Hedging unterstützt; andernfalls werden Long/Short-Beine sofort genetzt.
  • Stop-Loss-, Take-Profit- und Gewinnzielwerte an die Volatilität des Instruments anpassen.
  • Margennutzung überwachen, da gleichzeitige Long/Short-Beine die nominale Exposure verdoppeln.

Dateien

  • CS/DualLotStepHedgeStrategy.cs – StockSharp-Strategieimplementierung mit umfangreichen Inline-Kommentaren.
  • README_ru.md – Russische Übersetzung mit detaillierten Anweisungen.
  • README_zh.md – Chinesische Übersetzung mit detaillierten Anweisungen.
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);
	}
}