Открыть на GitHub

Стратегия ProMart MACD Martingale

Эта стратегия — перенос на StockSharp исторического советника MQL MartGreg_1 / ProMart. Она объединяет две конфигурации MACD с управляемой моделью мартингейла. Основной MACD ищет локальные минимумы и максимумы импульса, а вспомогательный MACD подтверждает направление последнего наклона. После каждой закрытой сделки стратегия либо снова следует сигналу индикаторов (если предыдущая сделка была прибыльной), либо сразу переворачивает позицию (после убытка) с возможным удвоением объёма следующего ордера.

Логика торговли

  • Сигналы
    • На выбранной серии свечей строятся два индикатора MACD:
      • MACD1 (быстрая = 5, медленная = 20, сигнал = 3) выступает детектором паттернов.
      • MACD2 (быстрая = 10, медленная = 15, сигнал = 3) подтверждает краткосрочный наклон.
    • Сигналы оцениваются только на завершённых свечах, используя предыдущие три значения MACD1 и два значения MACD2 (что повторяет работу исходного эксперта, который смотрел на один бар назад).
    • Лонг: MACD1 образует локальную «впадину» (MACD1[t-1] > MACD1[t-2] < MACD1[t-3]), а MACD2 растёт (MACD2[t-2] > MACD2[t-1]).
    • Шорт: MACD1 формирует локальную «вершину», а MACD2 снижается.
    • Если последняя закрытая сделка прибыльная, стратегия ждёт следующего валидного паттерна. После убыточной сделки позиция открывается немедленно в противоположную сторону вне зависимости от текущей формы MACD, что копирует мартингейл-логику исходника.
  • Управление позицией
    • Сделки открываются рыночными ордерами и контролируются на закрытии каждой свечи.
    • Уровни стоп-лосса и тейк-профита рассчитываются в пунктах от цены входа. Если максимум или минимум свечи достигает уровня, позиция закрывается по рынку, а результат фиксируется.
    • Новая сделка не открывается на той же свече, что закрыла позицию: стратегия ждёт следующий бар, как и в MQL-версии, срабатывавшей на первом тике нового бара.
  • Мартингейл и объём
    • Базовый объём определяется как отношение стоимости портфеля к параметру BalanceDivider с приведением к шагу объёма инструмента (с запасным вариантом — свойство Volume стратегии или минимальный объём инструмента).
    • После убыточной сделки следующий объём может удвоиться относительно предыдущего, максимум MaxDoublingCount раз подряд. Прибыль сбрасывает счётчик удвоений.
    • Объём всегда ограничивается максимальным объёмом инструмента, чтобы избежать переоценки позиции.

Параметры

Параметр Описание Значение по умолчанию
BalanceDivider Делитель стоимости портфеля для вычисления базового объёма ордера. 1000
MaxDoublingCount Максимальное число последовательных удвоений объёма после убытков. 1
StopLossPoints Размер стоп-лосса в пунктах (PriceStep * StopLossPoints). 500
TakeProfitPoints Размер тейк-профита в пунктах. 1500
Macd1Fast / Macd1Slow / Macd1Signal Периоды основного MACD, который ищет впадины и вершины. 5 / 20 / 3
Macd2Fast / Macd2Slow / Macd2Signal Периоды вспомогательного MACD для фильтра наклона. 10 / 15 / 3
CandleType Тип свечей для расчётов (по умолчанию — минутные). TimeSpan.FromMinutes(1).TimeFrame()

Примечания

  • Реализация аппроксимирует исполнение стоп-ордеров по максимумам и минимумам свечи, потому что пример StockSharp работает на закрытых свечах.
  • Если данные по портфелю недоступны, объём сделки берётся из свойства Volume стратегии или минимального объёма инструмента.
  • Версия на Python пока отсутствует, доступна только C#-реализация.
  • Перед запуском на реальных деньгах обязательно протестируйте стратегию на истории. Мартингейл резко увеличивает риск.
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;
			}
		}
	}
}