Auf GitHub ansehen

Händler handeln mit der Strategie MACD MQL4

Die Dealers Trade MACD MQL4-Strategie ist eine direkte Konvertierung des Expertenberaters „Dealers Trade v7.74“ für MetaTrader 4. Sie behält das Pyramiden-Geldmanagement und die MACD-Steigungslogik des ursprünglichen Systems bei und passt gleichzeitig die Positionsverwaltung an die Nettokonten von StockSharp an. Die Strategie ist für Swing-Trading auf H4/D1-Charts konzipiert und trägt kontinuierlich zum Trend bei, solange die Dynamik mit der MACD-Hauptlinie übereinstimmt.

Wie die Strategie funktioniert

  • Signalerkennung – die Strategie abonniert Kerzen des konfigurierten Zeitrahmens und berechnet einen klassischen MACD-Indikator (schneller EMA, langsamer EMA und Signal EMA). Ein steigender MACD-Hauptwert im Vergleich zum vorherigen Balken signalisiert ein zinsbullisches Momentum, während ein fallender Wert ein bärisches Momentum signalisiert. Der Parameter ReverseCondition kann verwendet werden, um die Richtung umzukehren, wenn ein konträrer Ansatz bevorzugt wird.
  • Auftragsabstände und -skalierung – es ist jeweils nur ein Richtungskorb aktiv. Wenn MACD einen langen Trend anzeigt, eröffnet die Strategie eine erste Marktkauforder. Zusätzliche Käufe werden nur gesendet, wenn der Preis um mindestens SpacingPips * PriceStep gegenüber dem letzten Einstiegspreis gesunken ist, was dem „Durchschnittsverhalten“ des MQL-Skripts entspricht. Kurze Körbe verhalten sich symmetrisch, wenn die MACD-Steigung negativ wird.
  • Lotgröße – die Basislosgröße ist entweder der feste FixedVolume oder, wenn UseRiskSizing aktiviert ist, ein Wert, der aus dem Portfolioeigenkapital und RiskPercent abgeleitet wird. Minikonten werden durch das Flag IsStandardAccount unterstützt, das die ursprüngliche Option „Konto ist normal“ emuliert. Jede zusätzliche Bestellung im selben Warenkorb wird mit LotMultiplier multipliziert und mit MaxVolume begrenzt.
  • Risikokontrollen – harte Stop-Loss- und Take-Profit-Level werden jeder Position mithilfe der Distanzen StopLossPips und TakeProfitPips zugeordnet. Sobald sich ein Trade um TrailingStopPips + SpacingPips im Gewinn bewegt hat, wird das Stop-Level verschärft, um mindestens TrailingStopPips des Gewinns beizubehalten, wodurch die Trailing-Regel aus der MetaTrader-Implementierung reproduziert wird.
  • Kontoschutz – wenn die Anzahl der offenen Trades MaxTrades - OrdersToProtect erreicht und der gesamte nicht realisierte Gewinn SecureProfit übersteigt, wird der letzte Trade geschlossen, um Gewinne zu sichern, bevor neue Aufträge berücksichtigt werden. Dies entspricht dem „AccountProtection“-Block in der Quelle EA.

Parameter

Name Standard Beschreibung
CandleType H4 Zeitrahmen, der für MACD-Berechnungen und Signalauswertung verwendet wird.
FixedVolume 0,1 Basislosgröße, wenn UseRiskSizing deaktiviert ist.
UseRiskSizing wahr Ermöglicht die ausgleichsbasierte Positionsgrößenbestimmung.
RiskPercent 2 Prozentsatz des Eigenkapitals, der zur Größe von Positionen verwendet wird, wenn UseRiskSizing wahr ist.
IsStandardAccount wahr Für Minikonten (Lots geteilt durch 10) auf „false“ setzen.
MaxVolume 5 Maximal zulässiges Volumen für eine einzelne Bestellung.
LotMultiplier 1.5 Für jeden weiteren Eintrag im Warenkorb wird ein Multiplikator auf das Basislos angewendet.
MaxTrades 5 Maximale Anzahl gleichzeitig offener Trades.
SpacingPips 4 Mindestpunktabstand zwischen aufeinanderfolgenden Einträgen.
OrdersToProtect 3 Anzahl der gehaltenen Aufträge, bevor die Schutzsperre neue Geschäfte eröffnen kann.
AccountProtection wahr Aktiviert die sichere Gewinnschutzlogik.
SecureProfit 50 Nicht realisierter Gewinn (in Kontowährung), der zum Auslösen des Schutzes erforderlich ist.
TakeProfitPips 30 Take-Profit-Distanz pro Trade, ausgedrückt in Pips.
StopLossPips 90 Stop-Loss-Distanz pro Trade, ausgedrückt in Pips.
TrailingStopPips 15 Nach der Aktivierung angewendeter Trailing-Stop-Abstand.
ReverseCondition falsch Kehrt die MACD-Steigungsinterpretation um.
MacdFast 14 Schnelle EMA-Länge für den MACD-Indikator.
MacdSlow 26 Langsame EMA-Länge für den MACD-Indikator.
MacdSignal 1 Signallänge von EMA für den Indikator MACD.

Hinweise und Einschränkungen

  • StockSharp-Strategien verwalten eine Nettoposition pro Wertpapier, daher können abgesicherte Long- und Short-Körbe nicht gleichzeitig existieren. Das ursprüngliche EA erlaubte eine Absicherung, aber die Konvertierung schließt die Gegenseite ab, bevor die Richtung geändert wird.
  • Die sichere Gewinnlogik berechnet den nicht realisierten Gewinn anhand der Metadaten des Instruments PriceStep und StepPrice. Instrumente ohne diese Informationen greifen auf einen nominalen Pip-Wert von 0,0001 mit einem Einheitswährungsschritt zurück. Passen Sie daher die Schwellenwerte entsprechend an.
  • Für die risikobasierte Größenbestimmung ist ein positiver StopLossPips-Wert erforderlich. Wenn die Stop-Distanz Null ist, wird der berechnete Risikobetrag undefiniert und die Strategie überspringt den Handel.
  • Die Strategie funktioniert nur bei geschlossenen Kerzen. Signale, die auf Intrabar-MACD-Bewegungen in MetaTrader beruhten, erscheinen in dieser Implementierung möglicherweise einen Balken später, aber das Verhalten ist beim Backtesting deutlich stabiler.
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 the original MQL4 version (Dealers Trade v7.74).
/// </summary>
public class DealersTradeMacdMql4Strategy : Strategy
{
	private readonly StrategyParam<DataType> _candleType;
	private readonly StrategyParam<decimal> _fixedVolume;
	private readonly StrategyParam<bool> _useRiskSizing;
	private readonly StrategyParam<decimal> _riskPercent;
	private readonly StrategyParam<bool> _isStandardAccount;
	private readonly StrategyParam<decimal> _maxVolume;
	private readonly StrategyParam<decimal> _lotMultiplier;
	private readonly StrategyParam<int> _maxTrades;
	private readonly StrategyParam<int> _spacingPips;
	private readonly StrategyParam<int> _ordersToProtect;
	private readonly StrategyParam<bool> _accountProtection;
	private readonly StrategyParam<decimal> _secureProfit;
	private readonly StrategyParam<int> _takeProfitPips;
	private readonly StrategyParam<int> _stopLossPips;
	private readonly StrategyParam<int> _trailingStopPips;
	private readonly StrategyParam<bool> _reverseCondition;
	private readonly StrategyParam<int> _macdFast;
	private readonly StrategyParam<int> _macdSlow;
	private readonly StrategyParam<int> _macdSignal;

	private MovingAverageConvergenceDivergence _macd;
	private List<PositionState> _positions;
	private decimal? _previousMacd;
	private decimal _pipSize;
	private decimal _stepValue;
	private int _cooldown;

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

		_fixedVolume = Param(nameof(FixedVolume), 0.1m)
			.SetDisplay("Fixed Volume", "Lot size when risk sizing is disabled", "Risk");

		_useRiskSizing = Param(nameof(UseRiskSizing), true)
			.SetDisplay("Use Risk Sizing", "Enable balance based money management", "Risk");

		_riskPercent = Param(nameof(RiskPercent), 2m)
			.SetDisplay("Risk Percent", "Percentage of equity used when sizing dynamically", "Risk");

		_isStandardAccount = Param(nameof(IsStandardAccount), true)
			.SetDisplay("Standard Account", "True for standard (1.0 lot) accounts, false for mini", "Risk");

		_maxVolume = Param(nameof(MaxVolume), 5m)
			.SetDisplay("Max Volume", "Upper cap for any single order", "Risk")
			.SetGreaterThanZero();

		_lotMultiplier = Param(nameof(LotMultiplier), 1.5m)
			.SetDisplay("Lot Multiplier", "Multiplier applied to subsequent entries", "Money Management")
			.SetGreaterThanZero();

		_maxTrades = Param(nameof(MaxTrades), 1)
			.SetDisplay("Max Trades", "Maximum simultaneous positions", "Money Management")
			.SetGreaterThanZero();

		_spacingPips = Param(nameof(SpacingPips), 200)
			.SetDisplay("Spacing (pips)", "Minimum price movement before adding", "Money Management")
			.SetNotNegative();

		_ordersToProtect = Param(nameof(OrdersToProtect), 3)
			.SetDisplay("Orders To Protect", "Number of trades kept when protection triggers", "Money Management")
			.SetNotNegative();

		_accountProtection = Param(nameof(AccountProtection), true)
			.SetDisplay("Account Protection", "Close last trade once secure profit is reached", "Money Management");

		_secureProfit = Param(nameof(SecureProfit), 50m)
			.SetDisplay("Secure Profit", "Currency profit required to lock gains", "Money Management")
			.SetNotNegative();

		_takeProfitPips = Param(nameof(TakeProfitPips), 200)
			.SetDisplay("Take Profit (pips)", "Take profit distance from entry", "Risk")
			.SetNotNegative();

		_stopLossPips = Param(nameof(StopLossPips), 500)
			.SetDisplay("Stop Loss (pips)", "Initial stop loss distance", "Risk")
			.SetNotNegative();

		_trailingStopPips = Param(nameof(TrailingStopPips), 100)
			.SetDisplay("Trailing Stop (pips)", "Trailing distance applied after activation", "Risk")
			.SetNotNegative();

		_reverseCondition = Param(nameof(ReverseCondition), false)
			.SetDisplay("Reverse Condition", "Invert MACD slope interpretation", "General");

		_macdFast = Param(nameof(MacdFast), 14)
			.SetDisplay("MACD Fast", "Fast EMA length", "Indicators")
			.SetGreaterThanZero();

		_macdSlow = Param(nameof(MacdSlow), 26)
			.SetDisplay("MACD Slow", "Slow EMA length", "Indicators")
			.SetGreaterThanZero();

		_macdSignal = Param(nameof(MacdSignal), 1)
			.SetDisplay("MACD Signal", "Signal EMA length", "Indicators")
			.SetGreaterThanZero();
	}

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

	/// <summary>
	/// Fixed order volume in lots.
	/// </summary>
	public decimal FixedVolume
	{
		get => _fixedVolume.Value;
		set => _fixedVolume.Value = value;
	}

	/// <summary>
	/// Enables balance based position sizing.
	/// </summary>
	public bool UseRiskSizing
	{
		get => _useRiskSizing.Value;
		set => _useRiskSizing.Value = value;
	}

	/// <summary>
	/// Risk percentage applied when <see cref="UseRiskSizing"/> is true.
	/// </summary>
	public decimal RiskPercent
	{
		get => _riskPercent.Value;
		set => _riskPercent.Value = value;
	}

	/// <summary>
	/// Indicates whether the account uses standard lot sizes.
	/// </summary>
	public bool IsStandardAccount
	{
		get => _isStandardAccount.Value;
		set => _isStandardAccount.Value = value;
	}

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

	/// <summary>
	/// Multiplier applied to the base size for subsequent entries.
	/// </summary>
	public decimal LotMultiplier
	{
		get => _lotMultiplier.Value;
		set => _lotMultiplier.Value = value;
	}

	/// <summary>
	/// Maximum number of simultaneously open trades.
	/// </summary>
	public int MaxTrades
	{
		get => _maxTrades.Value;
		set => _maxTrades.Value = value;
	}

	/// <summary>
	/// Minimum spacing between entries expressed in pips.
	/// </summary>
	public int SpacingPips
	{
		get => _spacingPips.Value;
		set => _spacingPips.Value = value;
	}

	/// <summary>
	/// Number of orders that should remain protected before adding new exposure.
	/// </summary>
	public int OrdersToProtect
	{
		get => _ordersToProtect.Value;
		set => _ordersToProtect.Value = value;
	}

	/// <summary>
	/// Enables the secure profit exit block.
	/// </summary>
	public bool AccountProtection
	{
		get => _accountProtection.Value;
		set => _accountProtection.Value = value;
	}

	/// <summary>
	/// Profit target used by the protection block.
	/// </summary>
	public decimal SecureProfit
	{
		get => _secureProfit.Value;
		set => _secureProfit.Value = value;
	}

	/// <summary>
	/// Take profit distance in pips.
	/// </summary>
	public int TakeProfitPips
	{
		get => _takeProfitPips.Value;
		set => _takeProfitPips.Value = value;
	}

	/// <summary>
	/// Stop loss distance in pips.
	/// </summary>
	public int StopLossPips
	{
		get => _stopLossPips.Value;
		set => _stopLossPips.Value = value;
	}

	/// <summary>
	/// Trailing stop distance in pips.
	/// </summary>
	public int TrailingStopPips
	{
		get => _trailingStopPips.Value;
		set => _trailingStopPips.Value = value;
	}

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

	/// <summary>
	/// Fast EMA period for the MACD indicator.
	/// </summary>
	public int MacdFast
	{
		get => _macdFast.Value;
		set => _macdFast.Value = value;
	}

	/// <summary>
	/// Slow EMA period for the MACD indicator.
	/// </summary>
	public int MacdSlow
	{
		get => _macdSlow.Value;
		set => _macdSlow.Value = value;
	}

	/// <summary>
	/// Signal EMA period for the MACD indicator.
	/// </summary>
	public int MacdSignal
	{
		get => _macdSignal.Value;
		set => _macdSignal.Value = value;
	}

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

	/// <inheritdoc />
	protected override void OnReseted()
	{
		base.OnReseted();
		_macd = null;
		_positions = null;
		_previousMacd = null;
		_pipSize = 0;
		_stepValue = 0;
		_cooldown = 0;
	}

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

		_positions = new List<PositionState>();

		_macd = new MovingAverageConvergenceDivergence(
			new ExponentialMovingAverage { Length = MacdSlow },
			new ExponentialMovingAverage { Length = MacdFast }
		);

		_pipSize = GetPriceStep();
		_stepValue = Security?.PriceStep ?? 0m;
		if (_stepValue <= 0m)
			_stepValue = 1m;

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

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

		if (!macdResult.IsFinal || !_macd.IsFormed)
			return;

		var macdValue = macdResult.GetValue<decimal>();

		UpdateTrailingAndStops(candle);

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

		var openTrades = _positions.Count;
		var allowNewTrade = openTrades < MaxTrades;

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

		var direction = Math.Sign(macdValue - _previousMacd.Value);
		if (ReverseCondition)
			direction = -direction;

		if (AccountProtection && openTrades >= Math.Max(1, MaxTrades - OrdersToProtect))
		{
			var totalProfit = CalculateTotalProfit(candle.ClosePrice);
			if (totalProfit >= SecureProfit)
			{
				CloseLastPosition();
				_cooldown = 3;
				_previousMacd = macdValue;
				return;
			}
		}

		if (allowNewTrade && direction > 0)
			TryOpen(Sides.Buy, candle);
		else if (allowNewTrade && direction < 0)
			TryOpen(Sides.Sell, candle);

		_previousMacd = macdValue;
	}

	private void TryOpen(Sides side, ICandleMessage candle)
	{
		var price = candle.ClosePrice;
		var spacing = SpacingPips * _pipSize;

		if (side == Sides.Buy)
		{
			var reference = GetReferencePrice(Sides.Buy);
			if (reference != 0m && reference - price < spacing)
				return;
		}
		else
		{
			var reference = GetReferencePrice(Sides.Sell);
			if (reference != 0m && price - reference < spacing)
				return;
		}

		var volume = CalculateVolume();
		if (volume <= 0m)
			return;

		var sameSideCount = CountPositions(side);
		if (sameSideCount > 0)
		{
			volume *= Pow(LotMultiplier, sameSideCount);
		}

		volume = NormalizeVolume(Math.Min(volume, MaxVolume));
		if (volume <= 0m)
			return;

		var stopDistance = StopLossPips * _pipSize;
		var takeDistance = TakeProfitPips * _pipSize;

		if (side == Sides.Buy)
			BuyMarket();
		else
			SellMarket();

		var state = new PositionState
		{
			Side = side,
			Volume = volume,
			EntryPrice = price,
			StopPrice = stopDistance > 0m ? (side == Sides.Buy ? price - stopDistance : price + stopDistance) : (decimal?)null,
			TakeProfitPrice = takeDistance > 0m ? (side == Sides.Buy ? price + takeDistance : price - takeDistance) : (decimal?)null
		};

		_positions.Add(state);
		_cooldown = 3;
	}

	private void UpdateTrailingAndStops(ICandleMessage candle)
	{
		var trailingDistance = TrailingStopPips * _pipSize;
		var activationDistance = (TrailingStopPips + SpacingPips) * _pipSize;

		for (var i = _positions.Count - 1; i >= 0; i--)
		{
			var state = _positions[i];

			if (state.Side == Sides.Buy)
			{
				if (state.TakeProfitPrice is decimal tp && candle.HighPrice >= tp)
				{
					SellMarket();
					_positions.RemoveAt(i);
					_cooldown = 3;
					continue;
				}

				if (state.StopPrice is decimal sl && candle.LowPrice <= sl)
				{
					SellMarket();
					_positions.RemoveAt(i);
					_cooldown = 3;
					continue;
				}

				if (TrailingStopPips > 0 && candle.ClosePrice - state.EntryPrice >= activationDistance)
				{
					var candidate = candle.ClosePrice - trailingDistance;
					if (state.StopPrice is null || state.StopPrice < candidate)
						state.StopPrice = candidate;
				}
			}
			else
			{
				if (state.TakeProfitPrice is decimal tp && candle.LowPrice <= tp)
				{
					BuyMarket();
					_positions.RemoveAt(i);
					_cooldown = 3;
					continue;
				}

				if (state.StopPrice is decimal sl && candle.HighPrice >= sl)
				{
					BuyMarket();
					_positions.RemoveAt(i);
					_cooldown = 3;
					continue;
				}

				if (TrailingStopPips > 0 && state.EntryPrice - candle.ClosePrice >= activationDistance)
				{
					var candidate = candle.ClosePrice + trailingDistance;
					if (state.StopPrice is null || state.StopPrice > candidate)
						state.StopPrice = candidate;
				}
			}
		}
	}

	private decimal CalculateVolume()
	{
		decimal baseVolume;

		if (UseRiskSizing)
		{
			if (Portfolio is null)
				return 0m;

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

			var rawLots = Math.Ceiling(balance * (RiskPercent / 100m) / 10000m);
			if (!IsStandardAccount)
				rawLots /= 10m;

			baseVolume = rawLots;
		}
		else
		{
			baseVolume = FixedVolume;
		}

		return baseVolume;
	}

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

		foreach (var state in _positions)
		{
			var priceDifference = state.Side == Sides.Buy
				? currentPrice - state.EntryPrice
				: state.EntryPrice - currentPrice;

			var steps = _pipSize > 0m ? priceDifference / _pipSize : priceDifference;
			profit += steps * _stepValue * state.Volume;
		}

		return profit;
	}

	private void CloseLastPosition()
	{
		if (_positions.Count == 0)
			return;

		var index = _positions.Count - 1;
		var state = _positions[index];

		if (state.Side == Sides.Buy)
			SellMarket();
		else
			BuyMarket();

		_positions.RemoveAt(index);
	}

	private decimal GetReferencePrice(Sides side)
	{
		for (var i = _positions.Count - 1; i >= 0; i--)
		{
			var state = _positions[i];
			if (state.Side == side)
				return state.EntryPrice;
		}

		return 0m;
	}

	private int CountPositions(Sides side)
	{
		var count = 0;
		for (var i = 0; i < _positions.Count; i++)
		{
			if (_positions[i].Side == side)
				count++;
		}

		return count;
	}

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

		var step = Security?.VolumeStep ?? 0m;
		if (step > 0m)
		{
			var steps = Math.Floor(volume / step);
			if (steps <= 0)
				steps = 1;
			volume = steps * step;
		}
		else
		{
			volume = Math.Round(volume, 1, MidpointRounding.AwayFromZero);
			if (volume <= 0m)
				volume = 0.1m;
		}

		return volume;
	}

	private decimal GetPriceStep()
	{
		var step = Security?.PriceStep ?? 0m;
		if (step > 0m)
			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 Sides Side { get; set; }
		public decimal Volume { get; set; }
		public decimal EntryPrice { get; set; }
		public decimal? StopPrice { get; set; }
		public decimal? TakeProfitPrice { get; set; }
	}
}