Ver en GitHub

ProMart MACD Martingale Strategy

This strategy is a StockSharp port of the historical MQL expert MartGreg_1 / ProMart. It combines two MACD configurations with a controlled martingale position sizing model. The primary MACD searches for local lows and highs in momentum, while the secondary MACD confirms the direction of the recent slope. After every closed trade the strategy either follows the indicator pattern again (when the last trade was profitable) or immediately flips direction (after a loss) while potentially doubling the next order size.

Trading Logic

  • Signals
    • Build two MACD indicators on the selected candle series:
      • MACD1 (fast=5, slow=20, signal=3) acts as the pattern detector.
      • MACD2 (fast=10, slow=15, signal=3) confirms the short-term slope.
    • Evaluate signals only on completed candles using the previous three MACD1 values and the previous two MACD2 values (mirroring the MQL logic that looked one bar back).
    • Long setup: MACD1 forms a local valley (MACD1[t-1] > MACD1[t-2] < MACD1[t-3]) and MACD2 is rising (MACD2[t-2] > MACD2[t-1]).
    • Short setup: MACD1 forms a local peak while MACD2 is falling.
    • If the most recent closed trade was profitable, the strategy waits for the next valid setup. After a losing trade it opens the opposite direction immediately, regardless of the current MACD shape, replicating the original martingale reversal.
  • Position management
    • Trades are opened with market orders and monitored on every finished candle.
    • Stop-loss and take-profit levels are calculated in price points from the entry price. If the candle’s high/low reaches either level, the position is closed at market and the trade result is recorded.
    • No new trade is opened on the same candle that closed a position; the strategy waits for the next bar, just like the MQL expert that acted on the first tick of a new bar.
  • Martingale sizing
    • A base volume is derived from the portfolio equity divided by BalanceDivider and aligned to the instrument volume step (falling back to the Volume property or the instrument minimum volume when necessary).
    • After a losing trade the next position can double the previous order volume, up to MaxDoublingCount consecutive times. Profit resets the doubling counter.
    • Volume is always capped by the instrument maximum volume to avoid oversizing.

Parameters

Parameter Description Default
BalanceDivider Divider applied to portfolio equity to compute the base order volume. 1000
MaxDoublingCount Maximum number of consecutive volume doublings after losses. 1
StopLossPoints Stop-loss distance measured in price points (PriceStep * StopLossPoints). 500
TakeProfitPoints Take-profit distance measured in price points. 1500
Macd1Fast / Macd1Slow / Macd1Signal Periods for the primary MACD that detects valleys/peaks. 5 / 20 / 3
Macd2Fast / Macd2Slow / Macd2Signal Periods for the secondary MACD slope filter. 10 / 15 / 3
CandleType Data type of the candle series (default: 1-minute time frame). TimeSpan.FromMinutes(1).TimeFrame()

Notes

  • The implementation approximates intrabar stop-loss and take-profit fills using candle highs and lows because the StockSharp example operates on finished candles.
  • Position volume falls back to the strategy Volume or the instrument minimum volume whenever portfolio data is not available.
  • No Python version is provided yet; only the C# strategy is included.
  • Always validate the configuration on historical data before enabling real trading. The martingale component significantly increases risk.
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;
			}
		}
	}
}