Открыть на GitHub

Стратегия MartinGale Breakout

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

MartinGale Breakout Strategy — это портированный в StockSharp советник MetaTrader 4 MartinGaleBreakout. Алгоритм отслеживает аномальные свечи и после их появления открывает позицию по направлению пробоя. При достижении определённого убытка включается режим восстановления: целевой профит увеличивается так, чтобы компенсировать предыдущие потери, что воспроизводит мартингейл-подход оригинала.

Стратегия анализирует выбранный таймфрейм (по умолчанию 15-минутные свечи) и хранит последние 11 завершённых свечей. Если диапазон текущей свечи превышает средний диапазон предыдущих десяти свечей более чем в три раза и цена закрытия находится в доминирующей половине диапазона, формируется сигнал на вход. Далее рассчитывается объём позиции, необходимый для достижения денежной цели по профиту, при соблюдении ограничений по доступной части баланса.

Логика работы

  1. Подписка на поток свечей заданного типа.
  2. Накопление 11 завершённых свечей и пересчёт среднего диапазона для определения аномальности.
  3. Определение бычьего пробоя, если свеча закрылась в верхней половине диапазона и в три раза больше среднего диапазона.
  4. Определение медвежьего пробоя при зеркальных условиях.
  5. Открытие рыночной позиции только при отсутствии открытых позиций и если требуемый капитал не превышает заданный процент от баланса.
  6. Контроль плавающей прибыли/убытка по текущей позиции и принудительное закрытие при достижении целей по прибыли или убытку.
  7. При срабатывании стоп-лосса:
    • Позиция закрывается рыночным ордером.
    • Цель по прибыли увеличивается на величину понесённого убытка.
    • Лимит по убытку устанавливается в максимальное значение и активируется режим восстановления.
  8. После успешного выхода из восстановления параметры цели и стоп-лосса сбрасываются к исходным значениям.

Настройки

Параметр Описание Значение по умолчанию
TakeProfitPoints Базовое расстояние до тейк-профита в пунктах инструмента. 50
BalancePercentageAvailable Максимальная доля баланса, доступная для открытия новой позиции. 50%
TakeProfitBalancePercent Целевой профит в процентах от баланса. 0.1%
StopLossBalancePercent Предельная просадка перед переходом к восстановлению. 10%
StartRecoveryFactor Доля стоп-лосса, при которой активируется режим восстановления. 0.2
TakeProfitPointsMultiplier Множитель расстояния до тейк-профита во время восстановления. 1
CandleType Тип свечей, используемых для поиска пробоя. 15 минут

Управление рисками

  • Объём сделки рассчитывается через тик-размер и тик-стоимость, чтобы получить заданную денежную прибыль при достижении цели.
  • Объём корректируется с учётом шага, минимального и максимального значения объёма, заданных биржей.
  • Перед открытием позиции оценивается требуемый капитал; если он превышает допустимую долю баланса, вход игнорируется.
  • Восстановление после стоп-лосса увеличивает дистанцию тейк-профита, имитируя мартингейл, но количество одновременно открытых позиций ограничено одной.

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

  • Для корректной работы необходимо, чтобы у стратегии был доступ к актуальной информации о портфеле.
  • Комиссии учитываются косвенно — через расчёт плавающего результата по текущей позиции.
  • Используются только рыночные ордера, отложенные заявки не формируются.
using System;
using System.Collections.Generic;

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

namespace StockSharp.Samples.Strategies;

/// <summary>
/// Breakout strategy with martingale-style recovery.
/// Detects abnormally large candles relative to recent history and enters in the breakout direction.
/// After a stop-loss, enters recovery mode with a wider take-profit target.
/// </summary>
public class MartinGaleBreakoutStrategy : Strategy
{
	private readonly StrategyParam<int> _requiredHistory;
	private readonly StrategyParam<decimal> _breakoutFactor;
	private readonly StrategyParam<decimal> _takeProfitPct;
	private readonly StrategyParam<decimal> _stopLossPct;
	private readonly StrategyParam<decimal> _recoveryMultiplier;
	private readonly StrategyParam<DataType> _candleType;

	private readonly List<decimal> _ranges = new();
	private decimal _entryPrice;
	private Sides? _entrySide;
	private bool _recovering;

	public int RequiredHistory
	{
		get => _requiredHistory.Value;
		set => _requiredHistory.Value = value;
	}

	public decimal BreakoutFactor
	{
		get => _breakoutFactor.Value;
		set => _breakoutFactor.Value = value;
	}

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

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

	public decimal RecoveryMultiplier
	{
		get => _recoveryMultiplier.Value;
		set => _recoveryMultiplier.Value = value;
	}

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

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

		_breakoutFactor = Param(nameof(BreakoutFactor), 2.5m)
			.SetDisplay("Breakout Factor", "Multiplier for abnormal candle detection", "General");

		_takeProfitPct = Param(nameof(TakeProfitPct), 0.5m)
			.SetDisplay("TP %", "Take profit percent of entry", "Trading");

		_stopLossPct = Param(nameof(StopLossPct), 0.3m)
			.SetDisplay("SL %", "Stop loss percent of entry", "Trading");

		_recoveryMultiplier = Param(nameof(RecoveryMultiplier), 1.5m)
			.SetDisplay("Recovery Mult", "TP multiplier during recovery", "Trading");

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

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

		_ranges.Clear();
		_entryPrice = 0;
		_entrySide = null;
		_recovering = 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 close = candle.ClosePrice;
		var range = candle.HighPrice - candle.LowPrice;

		// Check exit
		if (Position != 0 && _entryPrice > 0)
		{
			var tpPct = _recovering ? TakeProfitPct * RecoveryMultiplier : TakeProfitPct;

			if (_entrySide == Sides.Buy)
			{
				var pnl = (close - _entryPrice) / _entryPrice * 100m;
				if (pnl >= tpPct || pnl <= -StopLossPct)
				{
					var wasLoss = pnl < 0;
					SellMarket();
					_entryPrice = 0;
					_entrySide = null;
					_recovering = wasLoss;
					AddRange(range);
					return;
				}
			}
			else if (_entrySide == Sides.Sell)
			{
				var pnl = (_entryPrice - close) / _entryPrice * 100m;
				if (pnl >= tpPct || pnl <= -StopLossPct)
				{
					var wasLoss = pnl < 0;
					BuyMarket();
					_entryPrice = 0;
					_entrySide = null;
					_recovering = wasLoss;
					AddRange(range);
					return;
				}
			}
		}

		// Entry - only when flat
		if (Position == 0 && _ranges.Count >= RequiredHistory)
		{
			decimal sum = 0;
			for (int i = 0; i < _ranges.Count; i++)
				sum += _ranges[i];
			var avgRange = sum / _ranges.Count;

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

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

		AddRange(range);
	}

	private void AddRange(decimal range)
	{
		_ranges.Add(range);
		while (_ranges.Count > RequiredHistory)
			_ranges.RemoveAt(0);
	}

	/// <inheritdoc />
	protected override void OnReseted()
	{
		_ranges.Clear();
		_entryPrice = 0;
		_entrySide = null;
		_recovering = false;

		base.OnReseted();
	}
}