Auf GitHub ansehen

Dealers Trade MACD-Strategie

Die Dealers Trade MACD-Strategie ist ein Pyramidisierungssystem, das vom ursprünglichen MQL5-Experten "Dealers Trade v7.74" portiert wurde. Es folgt der Steigung der MACD-Hauptlinie, um zu entscheiden, wann Positionen in Trendrichtung akkumuliert werden sollen. Die Logik ist für Swing-Trading auf H4- und D1-Charts ausgelegt, wo Momentum-Verschiebungen weniger verrauscht sind.

Funktionsweise der Strategie

  • Signalgenerierung – die Strategie abonniert Kerzen des gewählten Zeitrahmens und wertet den MACD-Hauptlinienwert auf jedem geschlossenen Balken aus. Ein steigender MACD impliziert Long-Bias und ein fallender MACD impliziert Short-Bias. Das Signal kann mit dem Parameter ReverseCondition invertiert werden, um Konten anzupassen, die historisch konträre Einstiege handelten.
  • Positions-Sizing – die erste Order verwendet entweder die feste FixedVolume-Größe oder, wenn sie auf 0 gesetzt ist, weist das System dynamisch Risiko aus dem Portfolio-Eigenkapital über den Parameter RiskPercent und den konfigurierten Stop-Loss-Abstand zu. Weitere Einstiege werden mit VolumeMultiplier potenziert mit der aktuellen Positionsanzahl multipliziert (z.B. 1.6, 1.6², 1.6³, …) und werden nur gesendet, wenn der Preis mindestens IntervalPoints * PriceStep vom letzten Fill entfernt ist. Orders werden übersprungen, sobald die Nettoexposure MaxVolume überschreiten würde oder die Anzahl der Einstiege MaxPositions erreicht.
  • Order-Management – jede Position behält ihre eigenen Stop-Loss- und Take-Profit-Ziele, die vom Einstiegspreis und den punktbasierten Offsets (StopLossPoints, TakeProfitPoints) berechnet werden. Wenn TrailingStopPoints größer als null ist, wird der Stop nach oben gezogen (oder nach unten für Shorts), sobald der Gewinn TrailingStopPoints + TrailingStepPoints überschreitet, was das ursprüngliche Trailing-Verhalten emuliert.
  • Kontoschutz – wenn die Anzahl offener Trades größer als PositionsForProtection ist und der aggregierte unrealisierte Gewinn SecureProfit übersteigt, schließt die Strategie das profitabelste Bein, um Gewinne zu sichern, bevor neue Exposure hinzugefügt wird. Dies spiegelt den "Kontoschutz"-Block aus der MQL-Version wider.

Parameter

Name Standard Beschreibung
CandleType H4 Zeitrahmen für MACD-Berechnungen und Handelsentscheidungen.
FixedVolume 0.1 Lotgröße für den ersten Einstieg. Auf 0 setzen, um risikobasiertes Sizing zu aktivieren.
RiskPercent 5 Prozentsatz des aktuellen Eigenkapitals, das riskiert wird, wenn FixedVolume null ist.
StopLossPoints 90 Stop-Loss-Distanz in Preisschritten. 0 verwenden, um harte Stops zu deaktivieren.
TakeProfitPoints 30 Take-Profit-Distanz in Preisschritten. 0 verwenden, um zu deaktivieren.
TrailingStopPoints 15 Trailing-Stop-Distanz in Preisschritten. Auf 0 setzen, um Trailing auszuschalten.
TrailingStepPoints 5 Zusätzliche Distanz, die gewonnen werden muss, bevor sich der Trailing Stop wieder bewegt.
MaxPositions 5 Maximale Anzahl gleichzeitig offener Einstiege.
IntervalPoints 15 Mindestdistanz in Preisschritten zwischen aufeinanderfolgenden Einstiegen.
SecureProfit 50 Gewinnschwelle (in Kurswährung), die den Kontoschutz auslöst.
AccountProtection true Aktiviert das Schließen des am besten performenden Trades, wenn das sichere Gewinnziel erreicht ist.
PositionsForProtection 3 Minimale Anzahl von Trades, die offen sein müssen, bevor der Schutz auslösen kann.
ReverseCondition false Invertiert die MACD-Steigungsinterpretation.
MacdFastPeriod 14 Schnelle EMA-Länge für den MACD-Indikator.
MacdSlowPeriod 26 Langsame EMA-Länge für den MACD-Indikator.
MacdSignalPeriod 1 Signal-EMA-Länge für den MACD-Indikator (im ursprünglichen Experten auf 1 gesetzt).
MaxVolume 5 Obere Grenze für die kumulative Positionsgröße.
VolumeMultiplier 1.6 Multiplikator, der auf die Basisgröße für jeden neuen Einstieg angewendet wird.

Hinweise und Einschränkungen

  • Der ursprüngliche MQL-Experte konnte Long- und Short-gehedgte Positionen gleichzeitig halten. StockSharp verwendet standardmäßig genettete Positionen, daher schließt dieser Port entgegengesetzte Exposure, bevor neue Trades in die andere Richtung hinzugefügt werden.
  • MACD-Werte werden nur auf geschlossenen Kerzen ausgewertet. Intrabar-Signale können später auftreten als in der tick-basierten MQL-Implementierung, aber das Verhalten ist für historische Tests weit stabiler.
  • Alle punktbasierten Distanzen werden mit dem Instrument-PriceStep multipliziert. Wenn das Wertpapier diese Metadaten nicht bereitstellt, fällt die Strategie auf einen 0.0001-Schritt zurück, passen Sie daher Parameter an, wenn Sie Instrumente mit anderen Tick-Größen handeln.
  • Wenn FixedVolume null ist, benötigt die Strategie eine nicht-null Stop-Loss-Distanz, um risikobasiertes Sizing zu berechnen. Wenn der Stop deaktiviert ist, ist das Volumen standardmäßig null und kein Trade wird gesendet.
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>
/// Dealers Trade MACD strategy converted from MQL5 implementation.
/// </summary>
public class DealersTradeMacdStrategy : Strategy
{
	private readonly StrategyParam<DataType> _candleType;
	private readonly StrategyParam<decimal> _fixedVolume;
	private readonly StrategyParam<decimal> _riskPercent;
	private readonly StrategyParam<decimal> _stopLossPoints;
	private readonly StrategyParam<decimal> _takeProfitPoints;
	private readonly StrategyParam<decimal> _trailingStopPoints;
	private readonly StrategyParam<decimal> _trailingStepPoints;
	private readonly StrategyParam<int> _maxPositions;
	private readonly StrategyParam<decimal> _intervalPoints;
	private readonly StrategyParam<decimal> _secureProfit;
	private readonly StrategyParam<bool> _accountProtection;
	private readonly StrategyParam<int> _positionsForProtection;
	private readonly StrategyParam<bool> _reverseCondition;
	private readonly StrategyParam<int> _macdFastPeriod;
	private readonly StrategyParam<int> _macdSlowPeriod;
	private readonly StrategyParam<int> _macdSignalPeriod;
	private readonly StrategyParam<decimal> _maxVolume;
	private readonly StrategyParam<decimal> _volumeMultiplier;

	private MovingAverageConvergenceDivergence _macd = null!;
	private decimal? _previousMacd;
	private decimal _lastEntryPrice;
	private int _cooldown;
	private readonly List<PositionState> _longPositions = new();
	private readonly List<PositionState> _shortPositions = new();

	/// <summary>
	/// Initializes a new instance of <see cref="DealersTradeMacdStrategy"/>.
	/// </summary>
	public DealersTradeMacdStrategy()
	{
		_candleType = Param(nameof(CandleType), TimeSpan.FromHours(4).TimeFrame())
			.SetDisplay("Candle Type", "Primary timeframe", "General");

		_fixedVolume = Param(nameof(FixedVolume), 0.1m)
			.SetDisplay("Fixed Volume", "Lot size used when above zero", "Risk");

		_riskPercent = Param(nameof(RiskPercent), 5m)
			.SetDisplay("Risk %", "Risk percent when fixed volume is zero", "Risk");

		_stopLossPoints = Param(nameof(StopLossPoints), 90m)
			.SetDisplay("Stop Loss pts", "Stop loss distance in price steps", "Risk")
			;

		_takeProfitPoints = Param(nameof(TakeProfitPoints), 30m)
			.SetDisplay("Take Profit pts", "Take profit distance in price steps", "Risk")
			;

		_trailingStopPoints = Param(nameof(TrailingStopPoints), 15m)
			.SetDisplay("Trailing Stop pts", "Trailing stop distance in price steps", "Risk");

		_trailingStepPoints = Param(nameof(TrailingStepPoints), 5m)
			.SetDisplay("Trailing Step pts", "Additional distance before trailing updates", "Risk");

		_maxPositions = Param(nameof(MaxPositions), 2)
			.SetDisplay("Max Positions", "Maximum concurrent entries", "Money Management");

		_intervalPoints = Param(nameof(IntervalPoints), 50m)
			.SetDisplay("Interval pts", "Minimum distance between new entries", "Money Management");

		_secureProfit = Param(nameof(SecureProfit), 50m)
			.SetDisplay("Secure Profit", "Profit threshold that triggers protection", "Money Management");

		_accountProtection = Param(nameof(AccountProtection), true)
			.SetDisplay("Account Protection", "Close best trade after reaching secure profit", "Money Management");

		_positionsForProtection = Param(nameof(PositionsForProtection), 3)
			.SetDisplay("Protect From", "Minimum positions before triggering protection", "Money Management");

		_reverseCondition = Param(nameof(ReverseCondition), false)
			.SetDisplay("Reverse Signal", "Invert MACD slope direction", "Trading");

		_macdFastPeriod = Param(nameof(MacdFastPeriod), 14)
			.SetDisplay("MACD Fast", "Fast EMA period", "Indicators");

		_macdSlowPeriod = Param(nameof(MacdSlowPeriod), 26)
			.SetDisplay("MACD Slow", "Slow EMA period", "Indicators");

		_macdSignalPeriod = Param(nameof(MacdSignalPeriod), 1)
			.SetDisplay("MACD Signal", "Signal EMA period", "Indicators");

		_maxVolume = Param(nameof(MaxVolume), 5m)
			.SetDisplay("Max Volume", "Absolute cap for trade volume", "Risk")
			.SetGreaterThanZero();

		_volumeMultiplier = Param(nameof(VolumeMultiplier), 1.6m)
			.SetDisplay("Volume Multiplier", "Multiplier for additional positions", "Money Management")
			.SetGreaterThanZero();
	}

	/// <summary>
	/// Candle type used for signal calculations.
	/// </summary>
	public DataType CandleType
	{
		get => _candleType.Value;
		set => _candleType.Value = value;
	}

	/// <summary>
	/// Fixed lot size. When zero risk based sizing is used.
	/// </summary>
	public decimal FixedVolume
	{
		get => _fixedVolume.Value;
		set => _fixedVolume.Value = value;
	}

	/// <summary>
	/// Percent of equity risked when sizing dynamically.
	/// </summary>
	public decimal RiskPercent
	{
		get => _riskPercent.Value;
		set => _riskPercent.Value = value;
	}

	/// <summary>
	/// Stop loss distance in price steps.
	/// </summary>
	public decimal StopLossPoints
	{
		get => _stopLossPoints.Value;
		set => _stopLossPoints.Value = value;
	}

	/// <summary>
	/// Take profit distance in price steps.
	/// </summary>
	public decimal TakeProfitPoints
	{
		get => _takeProfitPoints.Value;
		set => _takeProfitPoints.Value = value;
	}

	/// <summary>
	/// Trailing stop distance in price steps.
	/// </summary>
	public decimal TrailingStopPoints
	{
		get => _trailingStopPoints.Value;
		set => _trailingStopPoints.Value = value;
	}

	/// <summary>
	/// Extra distance required before the trailing stop moves.
	/// </summary>
	public decimal TrailingStepPoints
	{
		get => _trailingStepPoints.Value;
		set => _trailingStepPoints.Value = value;
	}

	/// <summary>
	/// Maximum number of open entries.
	/// </summary>
	public int MaxPositions
	{
		get => _maxPositions.Value;
		set => _maxPositions.Value = value;
	}

	/// <summary>
	/// Minimum price distance between sequential entries.
	/// </summary>
	public decimal IntervalPoints
	{
		get => _intervalPoints.Value;
		set => _intervalPoints.Value = value;
	}

	/// <summary>
	/// Profit target for account protection logic.
	/// </summary>
	public decimal SecureProfit
	{
		get => _secureProfit.Value;
		set => _secureProfit.Value = value;
	}

	/// <summary>
	/// Enables profit locking when enough trades are open.
	/// </summary>
	public bool AccountProtection
	{
		get => _accountProtection.Value;
		set => _accountProtection.Value = value;
	}

	/// <summary>
	/// Minimum number of positions before account protection activates.
	/// </summary>
	public int PositionsForProtection
	{
		get => _positionsForProtection.Value;
		set => _positionsForProtection.Value = value;
	}

	/// <summary>
	/// Inverts the MACD slope direction.
	/// </summary>
	public bool ReverseCondition
	{
		get => _reverseCondition.Value;
		set => _reverseCondition.Value = value;
	}

	/// <summary>
	/// MACD fast EMA period.
	/// </summary>
	public int MacdFastPeriod
	{
		get => _macdFastPeriod.Value;
		set => _macdFastPeriod.Value = value;
	}

	/// <summary>
	/// MACD slow EMA period.
	/// </summary>
	public int MacdSlowPeriod
	{
		get => _macdSlowPeriod.Value;
		set => _macdSlowPeriod.Value = value;
	}

	/// <summary>
	/// MACD signal EMA period.
	/// </summary>
	public int MacdSignalPeriod
	{
		get => _macdSignalPeriod.Value;
		set => _macdSignalPeriod.Value = value;
	}

	/// <summary>
	/// Maximum allowed total volume.
	/// </summary>
	public decimal MaxVolume
	{
		get => _maxVolume.Value;
		set => _maxVolume.Value = value;
	}

	/// <summary>
	/// Multiplier applied to the base volume for each additional entry.
	/// </summary>
	public decimal VolumeMultiplier
	{
		get => _volumeMultiplier.Value;
		set => _volumeMultiplier.Value = value;
	}

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

	/// <inheritdoc />
	protected override void OnReseted()
	{
		base.OnReseted();
		_macd?.Reset();
		_previousMacd = null;
		_lastEntryPrice = 0m;
		_cooldown = 0;
		_longPositions.Clear();
		_shortPositions.Clear();
	}

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

		_macd = new MovingAverageConvergenceDivergence(
			new ExponentialMovingAverage { Length = MacdSlowPeriod },
			new ExponentialMovingAverage { Length = MacdFastPeriod }
		);

		var subscription = SubscribeCandles(CandleType);
		subscription
			.Bind(_macd, ProcessCandle)
			.Start();
	}

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

		HandleTrailingAndExits(candle);

		if (!IsFormedAndOnlineAndAllowTrading())
		{
			_previousMacd = macdValue;
			return;
		}

		if (_cooldown > 0)
		{
			_cooldown--;
			_previousMacd = macdValue;
			return;
		}

		var openPositions = _longPositions.Count + _shortPositions.Count;
		var continueOpening = openPositions < MaxPositions;

		var direction = 0;

		if (_previousMacd is null)
		{
			_previousMacd = macdValue;
			return;
		}

		if (macdValue > _previousMacd)
			direction = 1;
		else if (macdValue < _previousMacd)
			direction = -1;

		if (ReverseCondition)
			direction = -direction;

		if (AccountProtection && openPositions > PositionsForProtection)
		{
			var totalProfit = CalculateTotalProfit(candle.ClosePrice);
			if (totalProfit >= SecureProfit)
			{
				CloseMostProfitablePosition(candle.ClosePrice);
				_previousMacd = macdValue;
				return;
			}
		}

		if (continueOpening && direction > 0 && _shortPositions.Count == 0)
			TryOpenLong(candle);
		else if (continueOpening && direction < 0 && _longPositions.Count == 0)
			TryOpenShort(candle);

		_previousMacd = macdValue;
	}

	private void HandleTrailingAndExits(ICandleMessage candle)
	{
		var step = GetPriceStep();
		var trailingDistance = TrailingStopPoints * step;
		var trailingActivation = (TrailingStopPoints + TrailingStepPoints) * step;

		// Collect exits first, then execute to avoid collection modification during enumeration
		var longExits = new List<PositionState>();
		var longSnapshot = _longPositions.ToList();
		foreach (var state in longSnapshot)
		{
			if (state.TakeProfitPrice > 0 && candle.HighPrice >= state.TakeProfitPrice)
			{
				longExits.Add(state);
				continue;
			}

			if (state.StopPrice > 0 && candle.LowPrice <= state.StopPrice)
			{
				longExits.Add(state);
				continue;
			}

			if (TrailingStopPoints > 0 && candle.ClosePrice - state.EntryPrice > trailingActivation)
			{
				var candidateStop = candle.ClosePrice - trailingDistance;
				if (state.StopPrice == 0m || state.StopPrice < candle.ClosePrice - trailingActivation)
					state.StopPrice = candidateStop;
			}
		}
		foreach (var state in longExits)
		{
			Volume = state.Volume;
			SellMarket();
			_longPositions.Remove(state);
			_lastEntryPrice = 0m;
		}

		var shortExits = new List<PositionState>();
		var shortSnapshot = _shortPositions.ToList();
		foreach (var state in shortSnapshot)
		{
			if (state.TakeProfitPrice > 0 && candle.LowPrice <= state.TakeProfitPrice)
			{
				shortExits.Add(state);
				continue;
			}

			if (state.StopPrice > 0 && candle.HighPrice >= state.StopPrice)
			{
				shortExits.Add(state);
				continue;
			}

			if (TrailingStopPoints > 0 && state.EntryPrice - candle.ClosePrice > trailingActivation)
			{
				var candidateStop = candle.ClosePrice + trailingDistance;
				if (state.StopPrice == 0m || state.StopPrice > candle.ClosePrice + trailingActivation)
					state.StopPrice = candidateStop;
			}
		}
		foreach (var state in shortExits)
		{
			Volume = state.Volume;
			BuyMarket();
			_shortPositions.Remove(state);
			_lastEntryPrice = 0m;
		}
	}

	private void TryOpenLong(ICandleMessage candle)
	{
		var step = GetPriceStep();
		var interval = IntervalPoints * step;

		if (_lastEntryPrice != 0m && Math.Abs(_lastEntryPrice - candle.ClosePrice) < interval)
			return;

		var baseVolume = FixedVolume > 0 ? FixedVolume : CalculateRiskVolume(step);
		if (baseVolume <= 0)
			return;

		var openPositions = _longPositions.Count + _shortPositions.Count;
		var lotCoefficient = openPositions == 0 ? 1m : Pow(VolumeMultiplier, openPositions + 1);
		var volume = NormalizeVolume(baseVolume * lotCoefficient);
		if (volume <= 0 || volume > MaxVolume)
			return;

		var stopDistance = StopLossPoints * step;
		var takeDistance = TakeProfitPoints * step;

		Volume = volume;
		BuyMarket();

		_longPositions.Add(new PositionState
		{
			EntryPrice = candle.ClosePrice,
			Volume = volume,
			StopPrice = stopDistance > 0 ? candle.ClosePrice - stopDistance : 0m,
			TakeProfitPrice = takeDistance > 0 ? candle.ClosePrice + takeDistance : 0m
		});

		_lastEntryPrice = candle.ClosePrice;
		_cooldown = 3;
	}

	private void TryOpenShort(ICandleMessage candle)
	{
		var step = GetPriceStep();
		var interval = IntervalPoints * step;

		if (_lastEntryPrice != 0m && Math.Abs(_lastEntryPrice - candle.ClosePrice) < interval)
			return;

		var baseVolume = FixedVolume > 0 ? FixedVolume : CalculateRiskVolume(step);
		if (baseVolume <= 0)
			return;

		var openPositions = _longPositions.Count + _shortPositions.Count;
		var lotCoefficient = openPositions == 0 ? 1m : Pow(VolumeMultiplier, openPositions + 1);
		var volume = NormalizeVolume(baseVolume * lotCoefficient);
		if (volume <= 0 || volume > MaxVolume)
			return;

		var stopDistance = StopLossPoints * step;
		var takeDistance = TakeProfitPoints * step;

		Volume = volume;
		SellMarket();

		_shortPositions.Add(new PositionState
		{
			EntryPrice = candle.ClosePrice,
			Volume = volume,
			StopPrice = stopDistance > 0 ? candle.ClosePrice + stopDistance : 0m,
			TakeProfitPrice = takeDistance > 0 ? candle.ClosePrice - takeDistance : 0m
		});

		_lastEntryPrice = candle.ClosePrice;
		_cooldown = 3;
	}

	private decimal CalculateRiskVolume(decimal priceStep)
	{
		if (StopLossPoints <= 0)
			return 0m;

		var stopDistance = StopLossPoints * priceStep;
		if (stopDistance <= 0)
			return 0m;

		if (Portfolio is null)
			return 0m;

		var equity = Portfolio.CurrentValue ?? 0m;
		if (equity <= 0)
			return 0m;

		var riskAmount = equity * (RiskPercent / 100m);
		return riskAmount / stopDistance;
	}

	private decimal CalculateTotalProfit(decimal currentPrice)
	{
		decimal profit = 0m;

		foreach (var pos in _longPositions)
			profit += (currentPrice - pos.EntryPrice) * pos.Volume;

		foreach (var pos in _shortPositions)
			profit += (pos.EntryPrice - currentPrice) * pos.Volume;

		return profit;
	}

	private void CloseMostProfitablePosition(decimal currentPrice)
	{
		PositionState best = null;
		var bestIsLong = false;
		decimal bestProfit = 0m;

		foreach (var pos in _longPositions)
		{
			var profit = (currentPrice - pos.EntryPrice) * pos.Volume;
			if (profit > bestProfit)
			{
				bestProfit = profit;
				best = pos;
				bestIsLong = true;
			}
		}

		foreach (var pos in _shortPositions)
		{
			var profit = (pos.EntryPrice - currentPrice) * pos.Volume;
			if (profit > bestProfit)
			{
				bestProfit = profit;
				best = pos;
				bestIsLong = false;
			}
		}

		if (best is null || bestProfit <= 0m)
			return;

		if (bestIsLong)
		{
			SellMarket();
			_longPositions.Remove(best);
		}
		else
		{
			BuyMarket();
			_shortPositions.Remove(best);
		}

		_lastEntryPrice = 0m;
	}

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

		var step = Security?.VolumeStep ?? 0m;
		if (step > 0)
		{
			var steps = Math.Floor(volume / step);
			volume = steps * step;
		}

		return volume;
	}

	private decimal GetPriceStep()
	{
		var step = Security?.PriceStep ?? 0m;
		if (step > 0)
			return step;

		var decimals = Security?.Decimals ?? 0;
		if (decimals > 0)
			return (decimal)Math.Pow(10, -decimals);

		return 0.0001m;
	}

	private static decimal Pow(decimal value, int power)
	{
		if (power <= 0)
			return 1m;

		return (decimal)Math.Pow((double)value, power);
	}

	private sealed class PositionState
	{
		public decimal EntryPrice { get; set; }
		public decimal Volume { get; set; }
		public decimal StopPrice { get; set; }
		public decimal TakeProfitPrice { get; set; }
	}
}