View on GitHub

MartinGale Breakout Strategy

Overview

The MartinGale Breakout Strategy is a breakout-following system converted from the MetaTrader 4 expert advisor MartinGaleBreakout. The original robot enters positions after detecting abnormally large candles and applies a martingale-style recovery mechanism to regain previous losses. This StockSharp port reproduces the behaviour using the high-level strategy API with candle subscriptions and money-management parameters.

The strategy monitors a configurable candle series, looking for candles whose range is at least three times greater than the average range of the previous ten bars. When such a candle closes strongly in one direction, the strategy opens a market position in that direction. If the position is closed with a loss that exceeds a configurable threshold, the recovery mode increases the take-profit distance to compensate for the realised drawdown.

Trading Logic

  1. Subscribe to the selected candle series (15-minute candles by default).
  2. Maintain the most recent 11 finished candles to evaluate abnormal volatility.
  3. Detect a bullish breakout when:
    • The current candle is three times larger than the average range of the previous ten candles.
    • The candle closes in the upper half of its range.
  4. Detect a bearish breakout using the symmetric conditions.
  5. Open a market position in the breakout direction if:
    • No other position is currently open.
    • The estimated capital exposure is below the configured balance percentage.
  6. Close positions and reset profit/loss targets when:
    • Floating profit reaches the take-profit threshold.
    • Floating loss reaches the stop-loss threshold.
  7. When a stop-loss occurs, switch to recovery mode:
    • Increase the take-profit distance by the configured multiplier.
    • Expand the stop-loss limit to the maximum allowed percentage.
    • Continue trading until the next target is reached, then reset to the base configuration.

Parameters

Name Description Default
TakeProfitPoints Base take-profit distance expressed in instrument points. 50
BalancePercentageAvailable Maximum share of the account balance that can be allocated to a single trade. 50%
TakeProfitBalancePercent Target profit as a percentage of account balance. 0.1%
StopLossBalancePercent Maximum drawdown before triggering recovery. 10%
StartRecoveryFactor Portion of the stop-loss used before activating recovery mode. 0.2
TakeProfitPointsMultiplier Multiplier applied to the take-profit distance while recovering. 1
CandleType Candle series used for breakout calculations. 15-minute

Position Sizing and Risk Control

  • The strategy calculates the required volume to achieve the configured monetary take-profit using the instrument tick size and tick value.
  • Volumes are normalised to exchange constraints (step, minimum, maximum).
  • Estimated capital exposure must not exceed the configured balance percentage.
  • Recovery mode dynamically expands the take-profit target after a loss, emulating the original martingale behaviour while keeping positions limited to a single open trade.

Notes

  • The strategy relies on portfolio balance information; initialise it with a portfolio connection before starting.
  • Commission handling mirrors the original EA by focusing on floating P&L derived from the current position.
  • No pending orders are used—entries and exits are performed with market orders only.
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();
	}
}