Открыть на GitHub

Стратегия Martingale Breakout

Общее описание

Martingale Breakout Strategy — порт MetaTrader-советника MartinGaleBreakout.mq5 на платформу StockSharp. Алгоритм ждет аномально крупной свечи-пробоя и открывает единственную сделку в направлении пробоя. В оригинале управление позициями ведется через «магический номер», но в StockSharp это достигается изоляцией стратегии, поэтому поведение полностью совпадает.

В основе системы лежат две идеи:

  1. Фильтр пробоев. Для каждой завершённой свечи рассчитывается диапазон (High - Low) и сравнивается со средним диапазоном предыдущих десяти свечей. Если текущая свеча более чем втрое шире среднего и закрывается в сторону пробоя, формируется сигнал.
  2. Восстановление по типу мартингейла. Стратегия отслеживает нереализованную прибыль/убыток. Когда плавающий результат достигает заданного убытка, все позиции закрываются, а цель по прибыли увеличивается на размер потери. После выполнения новой цели значения порогов возвращаются к исходным, как и в версии на MQL5.

Порт сохраняет все параметры управления капиталом из исходника: процент баланса, доступный под маржу, целевую прибыль и стоп в процентах от баланса, а также множитель, расширяющий дистанцию тейк-профита при восстановлении.

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

  1. Подписаться на заданный тип свечей и обрабатывать только завершенные бары.
  2. Поддерживать кольцевой буфер из десяти диапазонов, чтобы вычислять эталонное среднее для определения «аномальности» свечи.
  3. Рассчитывать плавающий PnL через средние цены входа для лонгов и шортов. При достижении целевой прибыли или стоп-убытка немедленно закрывать позиции и сбрасывать состояние восстановления.
  4. Не открывать новые сделки, пока у стратегии уже есть позиция либо торговля недоступна по состоянию соединения.
  5. При бычьем пробое вычислять объем так, чтобы ожидаемая прибыль соответствовала текущей цели. Во время восстановления расстояние до тейк-профита умножается на RecoveryPointsMultiplier, аналог TP_Points_Multiplier в MQL5.
  6. Проверять рассчитанный объем на соответствие шагу, минимуму и максимуму инструмента, а также на достаточность маржи. При прохождении проверок отправлять рыночную заявку на покупку.
  7. При медвежьем пробое повторять процедуру и выставлять рыночную заявку на продажу.

Такой набор правил полностью воспроизводит поведение оригинального советника, включая переход в режим восстановления после срабатывания стоп-лосса.

Параметры

Параметр Описание Значение по умолчанию
TakeProfitPoints Расстояние от входа до тейк-профита в шагах цены. 50
BalancePercentAvailable Максимальный процент баланса, который можно задействовать под маржу. 50
TakeProfitPercentOfBalance Целевая прибыль в процентах от текущего баланса. 0.1
StopLossPercentOfBalance Стоп-убыток в процентах от текущего баланса. 10
RecoveryStartFraction Доля стоп-убытка, используемая до включения режима восстановления. 0.1
RecoveryPointsMultiplier Множитель дистанции тейк-профита во время восстановления. 1
CandleType Тип свечей, по которым строятся сигналы (таймфрейм, тиковые свечи и т. д.). 15-минутные свечи

Дополнительные замечания

  • Расчет объема повторяет помощник CalcLotWithTP: целевая прибыль делится на ожидаемое движение и переводится в объем с учётом шага объема инструмента.
  • Проверка маржи выполнена по аналогии с функциями CheckVolumeValue и ограничением доли баланса из оригинального кода. Заявки отклоняются, если требуемая маржа превышает выделенный процент или свободные средства портфеля.
  • Перед закрытием позиций стратегия отменяет активные заявки, что соответствует поведению CloseAllOrders в MQL5.
  • Буфер диапазонов хранит только десять значений, полностью заменяя цикл по iHigh/iLow из исходного советника и не требуя загрузки глубокой истории.
using System;

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

namespace StockSharp.Samples.Strategies;

/// <summary>
/// Breakout strategy that detects abnormally large candles and enters in the breakout direction.
/// Uses a simple martingale recovery: after a losing trade, the next entry is taken more aggressively.
/// </summary>
public class MartingaleBreakoutStrategy : Strategy
{
	private readonly StrategyParam<int> _lookback;
	private readonly StrategyParam<decimal> _breakoutMultiplier;
	private readonly StrategyParam<decimal> _takeProfitPct;
	private readonly StrategyParam<decimal> _stopLossPct;
	private readonly StrategyParam<DataType> _candleType;

	private readonly decimal[] _rangeBuffer = new decimal[10];
	private int _rangeBufferCount;
	private int _rangeBufferIndex;
	private decimal _rangeBufferSum;

	private decimal _entryPrice;
	private Sides? _entrySide;
	private bool _lastWasLoss;

	public int Lookback
	{
		get => _lookback.Value;
		set => _lookback.Value = value;
	}

	public decimal BreakoutMultiplier
	{
		get => _breakoutMultiplier.Value;
		set => _breakoutMultiplier.Value = value;
	}

	public decimal TakeProfitPct
	{
		get => _takeProfitPct.Value;
		set => _takeProfitPct.Value = value;
	}

	public decimal StopLossPct
	{
		get => _stopLossPct.Value;
		set => _stopLossPct.Value = value;
	}

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

	public MartingaleBreakoutStrategy()
	{
		_lookback = Param(nameof(Lookback), 10)
			.SetDisplay("Lookback", "Number of candles for average range", "General");

		_breakoutMultiplier = Param(nameof(BreakoutMultiplier), 3m)
			.SetDisplay("Breakout Mult", "Multiplier above avg range for breakout", "General");

		_takeProfitPct = Param(nameof(TakeProfitPct), 1m)
			.SetDisplay("Take Profit %", "Take profit as percentage of entry price", "Trading");

		_stopLossPct = Param(nameof(StopLossPct), 0.5m)
			.SetDisplay("Stop Loss %", "Stop loss as percentage of entry price", "Trading");

		_candleType = Param(nameof(CandleType), TimeSpan.FromMinutes(60).TimeFrame())
			.SetDisplay("Candle Type", "Candle type", "General");
	}

	protected override void OnStarted2(DateTime time)
	{
		base.OnStarted2(time);

		_rangeBufferCount = 0;
		_rangeBufferIndex = 0;
		_rangeBufferSum = 0m;
		_entryPrice = 0m;
		_entrySide = null;
		_lastWasLoss = false;

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

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

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

		var closePrice = candle.ClosePrice;

		// Check exit conditions first
		if (Position != 0 && _entryPrice > 0)
		{
			var tp = _lastWasLoss ? TakeProfitPct * 1.5m : TakeProfitPct;
			var sl = StopLossPct;

			if (_entrySide == Sides.Buy)
			{
				var pnlPct = (closePrice - _entryPrice) / _entryPrice * 100m;
				if (pnlPct >= tp || pnlPct <= -sl)
				{
					_lastWasLoss = pnlPct < 0;
					SellMarket();
					_entryPrice = 0;
					_entrySide = null;
					UpdateRangeStatistics(candle);
					return;
				}
			}
			else if (_entrySide == Sides.Sell)
			{
				var pnlPct = (_entryPrice - closePrice) / _entryPrice * 100m;
				if (pnlPct >= tp || pnlPct <= -sl)
				{
					_lastWasLoss = pnlPct < 0;
					BuyMarket();
					_entryPrice = 0;
					_entrySide = null;
					UpdateRangeStatistics(candle);
					return;
				}
			}
		}

		// Entry logic - only when flat
		if (Position == 0)
		{
			var range = candle.HighPrice - candle.LowPrice;

			if (_rangeBufferCount >= _rangeBuffer.Length)
			{
				var avgRange = _rangeBufferSum / _rangeBuffer.Length;

				if (range > avgRange * BreakoutMultiplier)
				{
					var body = candle.ClosePrice - candle.OpenPrice;

					if (body > 0 && body > range * 0.4m)
					{
						// Bullish breakout
						BuyMarket();
						_entryPrice = closePrice;
						_entrySide = Sides.Buy;
					}
					else if (body < 0 && Math.Abs(body) > range * 0.4m)
					{
						// Bearish breakout
						SellMarket();
						_entryPrice = closePrice;
						_entrySide = Sides.Sell;
					}
				}
			}
		}

		UpdateRangeStatistics(candle);
	}

	private void UpdateRangeStatistics(ICandleMessage candle)
	{
		var range = candle.HighPrice - candle.LowPrice;

		if (_rangeBufferCount < _rangeBuffer.Length)
		{
			_rangeBuffer[_rangeBufferIndex] = range;
			_rangeBufferSum += range;
			_rangeBufferCount++;
			_rangeBufferIndex = (_rangeBufferIndex + 1) % _rangeBuffer.Length;
			return;
		}

		_rangeBufferSum -= _rangeBuffer[_rangeBufferIndex];
		_rangeBuffer[_rangeBufferIndex] = range;
		_rangeBufferSum += range;
		_rangeBufferIndex = (_rangeBufferIndex + 1) % _rangeBuffer.Length;
	}

	/// <inheritdoc />
	protected override void OnReseted()
	{
		Array.Clear(_rangeBuffer);
		_rangeBufferCount = 0;
		_rangeBufferIndex = 0;
		_rangeBufferSum = 0m;
		_entryPrice = 0m;
		_entrySide = null;
		_lastWasLoss = false;

		base.OnReseted();
	}
}