Ver no GitHub

BreakRevert Pro Strategy

BreakRevert Pro is the StockSharp conversion of the MetaTrader 5 expert advisor BreakRevertPro.mq5. The strategy combines breakout confirmation on the one-minute timeframe with broader trend and volatility context from the 15-minute and one-hour charts. Probability-style scores are reproduced through indicator-driven approximations so that the behaviour remains close to the original EA while following StockSharp high-level API patterns.

Core Logic

  1. Primary timeframe (1 minute)
    • Average True Range (ATR) estimates intraday volatility.
    • A moving average of close prices measures the short-term directional bias.
    • A second moving average tracks the frequency of large candle-to-candle moves, representing the Poisson breakout probability from the MQL code.
    • An exponential moving average of absolute price moves produces the exponential-style probability used by the original safety filter.
  2. Confirmation timeframe (15 minutes)
    • A simple moving average measures the medium-term trend direction and blocks trades against the dominant flow.
  3. Context timeframe (1 hour)
    • Hourly candles provide the higher timeframe trend and the volatility range required for breakout validation and mean-reversion flattening checks.

When the Poisson and Weibull proxy probabilities exceed the breakout threshold, the 1-minute and 15-minute trends are aligned to the upside, and hourly volatility is elevated, the strategy enters a long breakout trade. Conversely, when probabilities drop below the mean-reversion threshold and the hourly trend is flat, the strategy sells short, targeting pullbacks back into the range. Market orders are used to mirror the immediate execution style of the original expert advisor.

Risk Management

  • A configurable trade delay prevents over-trading by enforcing a pause between consecutive entries.
  • MaxPositions limits the number of simultaneous open positions. When reversing from an opposite trade the strategy closes the current exposure and opens the new direction in a single market order.
  • Dynamic volume estimation uses the account balance, ATR-derived stop distance, and the RiskPerTrade percentage to produce a conservative lot size. If the calculation fails, the minimal step volume is used as a safe default.
  • Optional safety trades can be enabled for validation or testing environments where at least one trade must appear. The direction of the safety trade follows the combined short- and medium-term trend estimate.
  • StartProtection() activates StockSharp’s built-in protection block so that unexpected connection issues will not leave positions unmanaged.

Parameters

Parameter Description
RiskPerTrade Risk per trade in percent of the portfolio value (used for dynamic lot calculation).
LookbackPeriod Number of finished candles used for moving averages and ATR calculations across all timeframes.
BreakoutThreshold Minimum composite probability required for a breakout entry.
MeanReversionThreshold Maximum probability that still allows mean-reversion shorts.
TradeDelaySeconds Minimum number of seconds between consecutive entries.
MaxPositions Maximum simultaneous positions (used for both long and short exposure).
EnableSafetyTrade Enables optional validation safety trades when no positions are open.
SafetyTradeIntervalSeconds Waiting period between safety trade checks.
CandleType Primary timeframe used for the main signal subscription (default: 1 minute).

Usage Notes

  1. Attach the strategy to an instrument that supports 1-minute data and provides 15-minute and 1-hour candles (StockSharp will aggregate higher frames automatically when the broker supplies minute bars).
  2. Set the Volume property if a fixed order size is required. Otherwise the strategy derives a conservative size from account balance and ATR.
  3. Adjust thresholds and lookback lengths according to the target market’s volatility profile. Higher volatility pairs may benefit from larger thresholds to avoid frequent false breakouts.
  4. Safety trades are primarily intended for validation scenarios where the original EA executed at least one trade even without a signal. Disable them for normal live trading environments.

The conversion retains the original idea of mixing breakout detection with reversion safeguards while relying on StockSharp’s high-level indicator framework to remain efficient and test-friendly.

using System;
using System.Linq;
using System.Collections.Generic;

using Ecng.Common;
using Ecng.Collections;
using Ecng.Serialization;

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

using StockSharp.Algo;

namespace StockSharp.Samples.Strategies;

/// <summary>
/// BreakRevert Pro strategy converted from the MetaTrader 5 expert advisor.
/// The strategy blends breakout and mean-reversion logic using multi-timeframe candles.
/// </summary>
public class BreakRevertProStrategy : Strategy
{
	private readonly StrategyParam<decimal> _riskPerTrade;
	private readonly StrategyParam<int> _lookbackPeriod;
	private readonly StrategyParam<decimal> _breakoutThreshold;
	private readonly StrategyParam<decimal> _meanReversionThreshold;
	private readonly StrategyParam<int> _tradeDelaySeconds;
	private readonly StrategyParam<int> _maxPositions;
	private readonly StrategyParam<bool> _enableSafetyTrade;
	private readonly StrategyParam<int> _safetyTradeIntervalSeconds;
	private readonly StrategyParam<DataType> _candleType;

	private ISubscriptionHandler<ICandleMessage> _m1Subscription;
	private ISubscriptionHandler<ICandleMessage> _m15Subscription;
	private ISubscriptionHandler<ICandleMessage> _h1Subscription;

	private AverageTrueRange _m1Atr;
	private SimpleMovingAverage _m1TrendAverage;
	private SimpleMovingAverage _m15TrendAverage;
	private SimpleMovingAverage _h1TrendAverage;
	private SimpleMovingAverage _eventFrequency;
	private ExponentialMovingAverage _volatilityEma;

	private decimal _poissonProbability = 0.5m;
	private decimal _weibullProbability = 0.5m;
	private decimal _exponentialProbability = 0.5m;
	private decimal _m1Trend;
	private decimal _m15Trend;
	private decimal _h1Trend;
	private decimal _h1Volatility;
	private decimal? _previousM1Close;
	private decimal _latestAtr;
	private DateTimeOffset? _lastTradeTime;
	private DateTimeOffset? _lastSafetyCheck;
	private bool _safetyTradeSent;

	/// <summary>
	/// Initializes a new instance of the <see cref="BreakRevertProStrategy"/> class.
	/// </summary>
	public BreakRevertProStrategy()
	{
		_riskPerTrade = Param(nameof(RiskPerTrade), 1m)
		.SetDisplay("Risk %", "Risk per trade as percentage of portfolio value", "Risk")
		.SetOptimize(0.5m, 5m, 0.5m);

		_lookbackPeriod = Param(nameof(LookbackPeriod), 20)
			.SetRange(10, 60)
		.SetDisplay("Lookback", "Number of finished candles used for statistics", "Signals")
		;

		_breakoutThreshold = Param(nameof(BreakoutThreshold), 0.1m)
		.SetDisplay("Breakout Threshold", "Minimum composite probability required for breakout entries", "Signals")
		.SetOptimize(0.2m, 0.8m, 0.05m);

		_meanReversionThreshold = Param(nameof(MeanReversionThreshold), 0.6m)
		.SetDisplay("Reversion Threshold", "Maximum probability that still allows mean-reversion trades", "Signals")
		.SetOptimize(0.2m, 0.8m, 0.05m);

		_tradeDelaySeconds = Param(nameof(TradeDelaySeconds), 300)
		.SetDisplay("Trade Delay", "Minimum delay between consecutive entries (seconds)", "Risk");

		_maxPositions = Param(nameof(MaxPositions), 1)
		.SetDisplay("Max Positions", "Maximum number of simultaneously open positions", "Risk");

		_enableSafetyTrade = Param(nameof(EnableSafetyTrade), true)
		.SetDisplay("Safety Trade", "Allow protective trades when validation requires at least one position", "Safety");

		_safetyTradeIntervalSeconds = Param(nameof(SafetyTradeIntervalSeconds), 900)
		.SetDisplay("Safety Interval", "Delay between safety trade checks (seconds)", "Safety");

		_candleType = Param(nameof(CandleType), TimeSpan.FromMinutes(5).TimeFrame())
		.SetDisplay("Primary Candles", "Primary timeframe for signal generation", "Data");
	}

	/// <summary>
	/// Gets or sets the risk per trade in percent.
	/// </summary>
	public decimal RiskPerTrade
	{
		get => _riskPerTrade.Value;
		set => _riskPerTrade.Value = value;
	}

	/// <summary>
	/// Gets or sets the number of candles used in rolling calculations.
	/// </summary>
	public int LookbackPeriod
	{
		get => _lookbackPeriod.Value;
		set => _lookbackPeriod.Value = value;
	}

	/// <summary>
	/// Gets or sets the breakout probability threshold.
	/// </summary>
	public decimal BreakoutThreshold
	{
		get => _breakoutThreshold.Value;
		set => _breakoutThreshold.Value = value;
	}

	/// <summary>
	/// Gets or sets the mean-reversion probability threshold.
	/// </summary>
	public decimal MeanReversionThreshold
	{
		get => _meanReversionThreshold.Value;
		set => _meanReversionThreshold.Value = value;
	}

	/// <summary>
	/// Gets or sets the minimum delay between trades.
	/// </summary>
	public int TradeDelaySeconds
	{
		get => _tradeDelaySeconds.Value;
		set => _tradeDelaySeconds.Value = value;
	}

	/// <summary>
	/// Gets or sets the maximum simultaneous positions.
	/// </summary>
	public int MaxPositions
	{
		get => _maxPositions.Value;
		set => _maxPositions.Value = value;
	}

	/// <summary>
	/// Gets or sets a value indicating whether safety trades are allowed.
	/// </summary>
	public bool EnableSafetyTrade
	{
		get => _enableSafetyTrade.Value;
		set => _enableSafetyTrade.Value = value;
	}

	/// <summary>
	/// Gets or sets the safety trade interval in seconds.
	/// </summary>
	public int SafetyTradeIntervalSeconds
	{
		get => _safetyTradeIntervalSeconds.Value;
		set => _safetyTradeIntervalSeconds.Value = value;
	}

	/// <summary>
	/// Gets or sets the primary candle type.
	/// </summary>
	public DataType CandleType
	{
		get => _candleType.Value;
		set => _candleType.Value = value;
	}

	/// <inheritdoc />
	public override IEnumerable<(Security sec, DataType dt)> GetWorkingSecurities()
	{
		return [(Security, CandleType)];
	}

	/// <inheritdoc />
	protected override void OnReseted()
	{
		base.OnReseted();

		_m1Subscription = null;
		_m15Subscription = null;
		_h1Subscription = null;

		_m1Atr = null;
		_m1TrendAverage = null;
		_m15TrendAverage = null;
		_h1TrendAverage = null;
		_eventFrequency = null;
		_volatilityEma = null;

		_poissonProbability = 0.5m;
		_weibullProbability = 0.5m;
		_exponentialProbability = 0.5m;
		_m1Trend = 0m;
		_m15Trend = 0m;
		_h1Trend = 0m;
		_h1Volatility = 0m;
		_previousM1Close = null;
		_latestAtr = 0m;
		_lastTradeTime = null;
		_lastSafetyCheck = null;
		_safetyTradeSent = false;
	}

	/// <inheritdoc />
	protected override void OnStarted2(DateTime time)
	{
		base.OnStarted2(time);

		var lookback = Math.Max(1, LookbackPeriod);

		_m1Atr = new AverageTrueRange { Length = lookback };
		_m1TrendAverage = new SimpleMovingAverage { Length = lookback };
		_m15TrendAverage = new SimpleMovingAverage { Length = lookback };
		_h1TrendAverage = new SimpleMovingAverage { Length = lookback };
		_eventFrequency = new SimpleMovingAverage { Length = lookback };
		_volatilityEma = new ExponentialMovingAverage { Length = lookback };

		// Subscribe to the main one-minute flow.
		_m1Subscription = SubscribeCandles(CandleType);
		_m1Subscription
		.Bind(_m1Atr, ProcessPrimaryCandle)
		.Start();

		// Additional fifteen-minute stream provides mid-term trend confirmation.
		_m15Subscription = SubscribeCandles(TimeSpan.FromMinutes(15).TimeFrame());
		_m15Subscription
		.Bind(ProcessM15Candle)
		.Start();

		// Hourly candles track the broader context and volatility envelope.
		_h1Subscription = SubscribeCandles(TimeSpan.FromHours(1).TimeFrame());
		_h1Subscription
		.Bind(ProcessH1Candle)
		.Start();

		StartProtection(
		takeProfit: new Unit(2, UnitTypes.Percent),
		stopLoss: new Unit(1, UnitTypes.Percent)
	);
	}

	private void ProcessPrimaryCandle(ICandleMessage candle, decimal atrValue)
	{
		if (candle.State != CandleStates.Finished)
		return;

		_latestAtr = atrValue;

		var close = candle.ClosePrice;
		var time = candle.CloseTime;
		var pip = GetPipSize();

		if (_m1TrendAverage is not null)
		{
			var trendValue = _m1TrendAverage.Process(new DecimalIndicatorValue(_m1TrendAverage, close, time) { IsFinal = true }).ToDecimal();
			if (_m1TrendAverage.IsFormed)
			_m1Trend = close - trendValue;
		}

		if (_previousM1Close is decimal previousClose)
		{
			var move = Math.Abs(close - previousClose);
			var eventValue = move >= pip * 5m ? 1m : 0m;
			if (_eventFrequency is not null)
			{
				var avg = _eventFrequency.Process(new DecimalIndicatorValue(_eventFrequency, eventValue, time) { IsFinal = true }).ToDecimal();
				if (_eventFrequency.IsFormed)
				_poissonProbability = Clamp(avg, 0m, 1m);
			}

			if (_volatilityEma is not null)
			{
				var ema = _volatilityEma.Process(new DecimalIndicatorValue(_volatilityEma, move, time) { IsFinal = true }).ToDecimal();
				if (_volatilityEma.IsFormed)
				{
					var normalized = pip > 0m ? ema / (pip * 10m) : 0m;
					_exponentialProbability = Clamp(normalized, 0m, 1m);
				}
			}
		}

		_previousM1Close = close;

		var normalizedAtr = pip > 0m ? atrValue / (pip * 10m) : 0m;
		_weibullProbability = Clamp(normalizedAtr, 0m, 1m);

		EvaluateSignals(candle);
	}

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

		if (_m15TrendAverage is null)
		return;

		var close = candle.ClosePrice;
		var trend = _m15TrendAverage.Process(new DecimalIndicatorValue(_m15TrendAverage, close, candle.CloseTime) { IsFinal = true }).ToDecimal();
		if (_m15TrendAverage.IsFormed)
		_m15Trend = close - trend;
	}

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

		_h1Volatility = candle.HighPrice - candle.LowPrice;

		if (_h1TrendAverage is null)
		return;

		var close = candle.ClosePrice;
		var trend = _h1TrendAverage.Process(new DecimalIndicatorValue(_h1TrendAverage, close, candle.CloseTime) { IsFinal = true }).ToDecimal();
		if (_h1TrendAverage.IsFormed)
		_h1Trend = close - trend;
	}

	private void EvaluateSignals(ICandleMessage candle)
	{
		var now = candle.CloseTime;
		var pip = GetPipSize();

		if (_lastTradeTime is DateTimeOffset last && (now - last).TotalSeconds < TradeDelaySeconds)
		return;

		var tradeVolume = GetTradeVolume();
		if (tradeVolume <= 0m)
		return;

		if (Position != 0)
			return;

		var breakout = IsBreakoutSignal(pip);
		var reversion = IsMeanReversionSignal(pip);

		if (breakout)
		{
			BuyMarket();
			_lastTradeTime = now;
		}
		else if (reversion)
		{
			SellMarket();
			_lastTradeTime = now;
		}
	}

	private bool IsBreakoutSignal(decimal pip)
	{
		var trendUp = _m1Trend > 0m;
		var probabilityOk = _poissonProbability >= BreakoutThreshold || _weibullProbability >= BreakoutThreshold;
		return trendUp && probabilityOk;
	}

	private bool IsMeanReversionSignal(decimal pip)
	{
		var trendDown = _m1Trend < 0m;
		var probabilityOk = _weibullProbability <= MeanReversionThreshold || _poissonProbability <= MeanReversionThreshold;
		return trendDown && probabilityOk;
	}

	private void EnterLong(decimal volume)
	{
		var totalVolume = volume;

		if (Position < 0m)
		{
			totalVolume += Math.Abs(Position);
		}

		// Execute a market order to align with the breakout signal.
		BuyMarket(totalVolume);
	}

	private void EnterShort(decimal volume)
	{
		var totalVolume = volume;

		if (Position > 0m)
		{
			totalVolume += Math.Abs(Position);
		}

		// Execute a market order to capture the expected pullback.
		SellMarket(totalVolume);
	}

	private void CheckSafetyTrade(DateTimeOffset time, decimal volume)
	{
		if (!EnableSafetyTrade || _safetyTradeSent || Position != 0m)
		return;

		if (_lastSafetyCheck is DateTimeOffset last && (time - last).TotalSeconds < SafetyTradeIntervalSeconds)
		return;

		_lastSafetyCheck = time;

		var direction = _m1Trend + _m15Trend;
		if (direction > 0m)
		{
			BuyMarket(volume);
		}
		else
		{
			SellMarket(volume);
		}

		_safetyTradeSent = true;
		_lastTradeTime = time;
	}

	private bool HasReachedMaxExposure(int direction, decimal tradeVolume)
	{
		if (MaxPositions <= 0 || tradeVolume <= 0m)
		return false;

		var limit = MaxPositions * tradeVolume;

		return direction switch
		{
			> 0 => Position >= limit,
			< 0 => -Position >= limit,
			_ => Math.Abs(Position) >= limit,
		};
	}

	private decimal GetTradeVolume()
	{
		if (Volume > 0m)
		return Volume;

		var stepVolume = Security?.VolumeStep ?? 1m;
		var lotStep = Security?.VolumeStep ?? stepVolume;
		var minVolume = Security?.MinVolume ?? stepVolume;
		var maxVolume = Security?.MaxVolume ?? decimal.MaxValue;
		var balance = Portfolio?.CurrentValue ?? Portfolio?.BeginValue ?? 0m;
		var atr = Math.Max(_latestAtr, GetPipSize());

		if (stepVolume <= 0m)
		stepVolume = 1m;

		if (lotStep <= 0m)
		lotStep = stepVolume;

		if (minVolume <= 0m)
		minVolume = stepVolume;

		if (balance <= 0m || atr <= 0m)
		return minVolume;

		var riskAmount = balance * RiskPerTrade / 100m;
		if (riskAmount <= 0m)
		return minVolume;

		var riskPerUnit = atr;
		var rawVolume = riskPerUnit > 0m ? riskAmount / riskPerUnit : minVolume;
		rawVolume = Math.Max(rawVolume, minVolume);

		var normalized = Math.Floor(rawVolume / lotStep) * lotStep;
		if (normalized <= 0m)
		normalized = minVolume;

		if (maxVolume > 0m && normalized > maxVolume)
		normalized = maxVolume;

		return normalized;
	}

	private decimal GetPipSize()
	{
		var step = Security?.PriceStep;
		if (step is null || step.Value <= 0m)
		return 0.0001m;

		return step.Value;
	}

	private static decimal Clamp(decimal value, decimal min, decimal max)
	{
		if (value < min)
		return min;

		return value > max ? max : value;
	}
}