Auf GitHub ansehen

ProMart MACD Martingale-Strategie

Diese Strategie ist ein StockSharp-Port des historischen MQL-Experten MartGreg_1 / ProMart. Sie kombiniert zwei MACD-Konfigurationen mit einem kontrollierten Martingale-Positionierungsmodell. Der primäre MACD sucht nach lokalen Tiefst- und Höchstwerten im Momentum, während der sekundäre MACD die Richtung der jüngsten Steigung bestätigt. Nach jedem geschlossenen Trade folgt die Strategie dem Indikatormuster erneut (wenn der letzte Trade profitabel war) oder dreht die Richtung sofort um (nach einem Verlust), während sie möglicherweise die nächste Ordergröße verdoppelt.

Handelslogik

  • Signale
    • Zwei MACD-Indikatoren auf der ausgewählten Kerzenserie aufbauen:
      • MACD1 (schnell=5, langsam=20, Signal=3) fungiert als Musterdetektor.
      • MACD2 (schnell=10, langsam=15, Signal=3) bestätigt die kurzfristige Steigung.
    • Signale nur auf abgeschlossenen Kerzen mit den vorherigen drei MACD1-Werten und den vorherigen zwei MACD2-Werten auswerten (spiegelt die MQL-Logik wider, die einen Bar zurückschaute).
    • Long-Setup: MACD1 bildet ein lokales Tal (MACD1[t-1] > MACD1[t-2] < MACD1[t-3]) und MACD2 steigt (MACD2[t-2] > MACD2[t-1]).
    • Short-Setup: MACD1 bildet einen lokalen Gipfel, während MACD2 fällt.
    • Wenn der letzte geschlossene Trade profitabel war, wartet die Strategie auf das nächste gültige Setup. Nach einem Verlusttrade öffnet sie sofort die Gegenrichtung, unabhängig von der aktuellen MACD-Form, und repliziert die ursprüngliche Martingale-Umkehr.
  • Positionsmanagement
    • Trades werden mit Market-Orders geöffnet und auf jeder abgeschlossenen Kerze überwacht.
    • Stop-Loss- und Take-Profit-Niveaus werden in Preispunkten vom Einstiegspreis berechnet. Wenn das Hoch/Tief der Kerze eines der Niveaus erreicht, wird die Position zum Marktpreis geschlossen und das Trade-Ergebnis erfasst.
    • Kein neuer Trade wird auf derselben Kerze geöffnet, die eine Position geschlossen hat; die Strategie wartet auf den nächsten Bar, genau wie der MQL-Experte, der beim ersten Tick eines neuen Bars agierte.
  • Martingale-Sizing
    • Ein Basisvolumen wird aus dem Portfolio-Eigenkapital dividiert durch BalanceDivider abgeleitet und auf den Volumen-Schritt des Instruments ausgerichtet (fällt auf die Volume-Eigenschaft oder das Instrument-Mindestvolumen zurück, wenn nötig).
    • Nach einem Verlusttrade kann die nächste Position das vorherige Ordervolumen verdoppeln, bis zu MaxDoublingCount mal hintereinander. Gewinn setzt den Verdopplungszähler zurück.
    • Volumen ist immer durch das maximale Instrument-Volumen begrenzt, um Überdimensionierung zu vermeiden.

Parameter

Parameter Beschreibung Standard
BalanceDivider Teiler, der auf das Portfolio-Eigenkapital zur Berechnung des Basis-Ordervolumens angewendet wird. 1000
MaxDoublingCount Maximale Anzahl aufeinanderfolgender Volumenverdopplungen nach Verlusten. 1
StopLossPoints Stop-Loss-Abstand in Preispunkten (PriceStep * StopLossPoints). 500
TakeProfitPoints Take-Profit-Abstand in Preispunkten. 1500
Macd1Fast / Macd1Slow / Macd1Signal Perioden für den primären MACD, der Täler/Gipfel erkennt. 5 / 20 / 3
Macd2Fast / Macd2Slow / Macd2Signal Perioden für den sekundären MACD-Steigungsfilter. 10 / 15 / 3
CandleType Datentyp der Kerzenserie (Standard: 1-Minuten-Zeitrahmen). TimeSpan.FromMinutes(1).TimeFrame()

Hinweise

  • Die Implementierung approximiert Intrabar-Stop-Loss- und Take-Profit-Füllungen mit Kerzenhochs und -tiefs, da das StockSharp-Beispiel auf abgeschlossenen Kerzen arbeitet.
  • Das Positionsvolumen fällt auf die Volume-Eigenschaft der Strategie oder das Instrument-Mindestvolumen zurück, wenn Portfolio-Daten nicht verfügbar sind.
  • Noch keine Python-Version vorhanden; nur die C#-Strategie ist enthalten.
  • Die Konfiguration immer auf historischen Daten validieren, bevor der echte Handel aktiviert wird. Die Martingale-Komponente erhöht das Risiko erheblich.
using System;
using System.Collections.Generic;

using Ecng.Common;

using StockSharp.Algo.Indicators;
using StockSharp.Algo.Strategies;
using StockSharp.BusinessEntities;
using StockSharp.Messages;

namespace StockSharp.Samples.Strategies;

/// <summary>
/// MACD-based martingale strategy. Uses two MACD indicators to detect turning points.
/// Doubles volume after a loss up to MaxDoublingCount times.
/// </summary>
public class ProMartMacdMartingaleStrategy : Strategy
{
	private readonly StrategyParam<int> _maxDoublingCount;
	private readonly StrategyParam<int> _macd1Fast;
	private readonly StrategyParam<int> _macd1Slow;
	private readonly StrategyParam<int> _macd2Fast;
	private readonly StrategyParam<int> _macd2Slow;
	private readonly StrategyParam<DataType> _candleType;

	private readonly List<decimal> _macd1History = new();
	private readonly List<decimal> _macd2History = new();

	private decimal _entryPrice;
	private bool _inPosition;
	private bool _isLong;
	private bool _lastTradeWasLoss;
	private int _martingaleCounter;
	private decimal _currentVolume;

	public int MaxDoublingCount
	{
		get => _maxDoublingCount.Value;
		set => _maxDoublingCount.Value = value;
	}

	public int Macd1Fast
	{
		get => _macd1Fast.Value;
		set => _macd1Fast.Value = value;
	}

	public int Macd1Slow
	{
		get => _macd1Slow.Value;
		set => _macd1Slow.Value = value;
	}

	public int Macd2Fast
	{
		get => _macd2Fast.Value;
		set => _macd2Fast.Value = value;
	}

	public int Macd2Slow
	{
		get => _macd2Slow.Value;
		set => _macd2Slow.Value = value;
	}

	public DataType CandleType
	{
		get => _candleType.Value;
		set => _candleType.Value = value;
	}

	public ProMartMacdMartingaleStrategy()
	{
		_maxDoublingCount = Param(nameof(MaxDoublingCount), 2)
			.SetNotNegative()
			.SetDisplay("Max Doubling", "Maximum number of volume doublings after losses.", "Risk");

		_macd1Fast = Param(nameof(Macd1Fast), 5)
			.SetGreaterThanZero()
			.SetDisplay("MACD1 Fast", "Fast EMA period for the primary MACD.", "Signal");

		_macd1Slow = Param(nameof(Macd1Slow), 20)
			.SetGreaterThanZero()
			.SetDisplay("MACD1 Slow", "Slow EMA period for the primary MACD.", "Signal");

		_macd2Fast = Param(nameof(Macd2Fast), 10)
			.SetGreaterThanZero()
			.SetDisplay("MACD2 Fast", "Fast EMA period for the secondary MACD.", "Filter");

		_macd2Slow = Param(nameof(Macd2Slow), 15)
			.SetGreaterThanZero()
			.SetDisplay("MACD2 Slow", "Slow EMA period for the secondary MACD.", "Filter");

		_candleType = Param(nameof(CandleType), TimeSpan.FromHours(4).TimeFrame())
			.SetDisplay("Candle Type", "Data type used for signal generation.", "General");
	}

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

	/// <inheritdoc />
	protected override void OnReseted()
	{
		base.OnReseted();
		_macd1History.Clear();
		_macd2History.Clear();
		_entryPrice = 0;
		_inPosition = false;
		_isLong = false;
		_lastTradeWasLoss = false;
		_martingaleCounter = 0;
		_currentVolume = 0;
	}

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

		_macd1History.Clear();
		_macd2History.Clear();
		_inPosition = false;
		_isLong = false;
		_lastTradeWasLoss = false;
		_martingaleCounter = 0;
		_currentVolume = Volume;
		_entryPrice = 0;

		var macd1 = new MovingAverageConvergenceDivergence(
			new ExponentialMovingAverage { Length = Macd1Slow },
			new ExponentialMovingAverage { Length = Macd1Fast });

		var macd2 = new MovingAverageConvergenceDivergence(
			new ExponentialMovingAverage { Length = Macd2Slow },
			new ExponentialMovingAverage { Length = Macd2Fast });

		var subscription = SubscribeCandles(CandleType);
		subscription
			.Bind(macd1, macd2, ProcessCandle)
			.Start();

		var area = CreateChartArea();
		if (area != null)
		{
			DrawCandles(area, subscription);
			DrawIndicator(area, macd1);
			DrawOwnTrades(area);
		}
	}

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

		_macd1History.Add(macd1Value);
		_macd2History.Add(macd2Value);

		if (_macd1History.Count > 4)
			_macd1History.RemoveAt(0);
		if (_macd2History.Count > 3)
			_macd2History.RemoveAt(0);

		// Check exit for open position
		if (_inPosition)
		{
			var pnl = _isLong
				? candle.ClosePrice - _entryPrice
				: _entryPrice - candle.ClosePrice;

			// Detect reversal to exit
			var shouldExit = false;
			if (_macd1History.Count >= 3)
			{
				var m0 = _macd1History[^1];
				var m1 = _macd1History[^2];
				var m2 = _macd1History[^3];

				if (_isLong && m0 < m1 && m1 > m2)
					shouldExit = true;
				else if (!_isLong && m0 > m1 && m1 < m2)
					shouldExit = true;
			}

			if (shouldExit)
			{
				if (_isLong)
					SellMarket();
				else
					BuyMarket();

				_lastTradeWasLoss = pnl < 0;
				if (_lastTradeWasLoss && _martingaleCounter < MaxDoublingCount)
				{
					_currentVolume *= 2;
					_martingaleCounter++;
				}
				else
				{
					_currentVolume = Volume;
					_martingaleCounter = 0;
				}

				_inPosition = false;
				return;
			}
		}

		// Check entry
		if (!_inPosition && _macd1History.Count >= 3 && _macd2History.Count >= 2)
		{
			var m0 = _macd1History[^1];
			var m1 = _macd1History[^2];
			var m2 = _macd1History[^3];
			var f0 = _macd2History[^1];
			var f1 = _macd2History[^2];

			// MACD1 turns up from bottom + MACD2 confirms
			var buySignal = m0 > m1 && m1 < m2 && f1 > f0;
			var sellSignal = m0 < m1 && m1 > m2 && f1 < f0;

			if (buySignal && Position <= 0)
			{
				BuyMarket();
				_inPosition = true;
				_isLong = true;
				_entryPrice = candle.ClosePrice;
			}
			else if (sellSignal && Position >= 0)
			{
				SellMarket();
				_inPosition = true;
				_isLong = false;
				_entryPrice = candle.ClosePrice;
			}
		}
	}
}