View on GitHub

Three Candles Reversal Strategy

This strategy is a faithful StockSharp port of the MQL5 expert advisor Exp_ThreeCandles. It looks for a classic three-candle reversal:

  1. Two consecutive candles in one direction.
  2. A third candle that flips the direction and closes beyond the middle bar.
  3. Optional volume confirmation unless the oldest bar in the pattern is exceptionally large.

When a bullish configuration appears the algorithm closes short exposure and can enter a long position. A bearish configuration does the opposite. Protective stop-loss and take-profit levels are applied using the current instrument price step.

Pattern detection

The strategy keeps a rolling window of the most recent SignalBar + 3 finished candles. On every new bar it checks the candle at SignalBar offset (default: 1 bar back) and the three older candles:

  • Bullish reversal (potential long):
    • The two older candles (SignalBar + 3 and SignalBar + 2) are bearish.
    • The middle candle closes above the low of the oldest bar.
    • The most recent candle before the signal (SignalBar + 1) is bullish and closes above the open of the middle bar.
  • Bearish reversal (potential short):
    • Mirror logic of the bullish case.

A volume filter mirrors the original indicator. The filter is skipped when MaxBarSize (in price steps) is exceeded by the oldest candle range or when VolumeFilter is set to None. Otherwise the reversal must satisfy older volume < middle volume OR recent volume > middle volume OR recent volume > oldest volume. Tick and real volume are mapped to the candle's aggregated volume because StockSharp does not distinguish the two in the high level candle stream.

Trade management

  • If AllowSellExit is enabled, a bullish pattern immediately covers any short position before considering a long entry. AllowBuyExit behaves the same for longs on bearish patterns.
  • New positions are only opened when the current position is flat and the corresponding Allow*Entry flag is true. Order size uses the strategy's standard volume settings.
  • Stop-loss and take-profit distances (StopLossPips, TakeProfitPips) are expressed in price steps and monitored on every finished candle.
  • The last processed bullish/bearish signal time is cached to avoid duplicate actions while a candle keeps triggering ticks.

Parameters

Name Default Description
CandleType 4 hour time frame Candle series processed by the strategy.
SignalBar 1 How many bars back the signal is evaluated. Must be ≥ 0.
MaxBarSize 300 If the oldest bar range (converted with PriceStep) exceeds this value the volume filter is skipped. Set to 0 to always skip.
VolumeFilter Tick Volume mode (Tick, Real, or None). Both Tick and Real use TotalVolume from candles.
AllowBuyEntry true Enable long entries on bullish patterns.
AllowSellEntry true Enable short entries on bearish patterns.
AllowBuyExit true Allow closing long positions on bearish patterns.
AllowSellExit true Allow closing short positions on bullish patterns.
StopLossPips 1000 Stop-loss distance in price steps (0 disables).
TakeProfitPips 2000 Take-profit distance in price steps (0 disables).

Conversion notes

  • Money-management routines from the original MQL5 include file were replaced by StockSharp's BuyMarket/SellMarket calls. Position size therefore follows the engine's default volume.
  • Signal timing mirrors the expert advisor by evaluating the bar at SignalBar offset and keeping the previous signal timestamp.
  • Email, push, and sound alerts from the MQL indicator are intentionally omitted.
  • Volume modes are preserved but both map to the candle's aggregate volume because separate tick and real volumes are not available in the high-level API.
  • All comments were rewritten in English as required by the project guidelines.

This implementation stays close to the original behaviour while adhering to StockSharp's high-level subscription model.

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;

namespace StockSharp.Samples.Strategies;

/// <summary>
/// Translates the classic Three Candles reversal expert advisor from MQL5.
/// The strategy searches for two candles in one direction followed by a strong opposite candle and trades the expected reversal.
/// </summary>
public class ThreeCandlesReversalStrategy : Strategy
{
	public enum ThreeCandlesVolumeTypes
	{
		Tick,
		Real,
		None,
	}

	private readonly List<CandleSample> _candles = new();
	private static readonly object _sync = new();

	private readonly StrategyParam<DataType> _candleType;
	private readonly StrategyParam<int> _signalBar;
	private readonly StrategyParam<int> _maxBarSize;
	private readonly StrategyParam<ThreeCandlesVolumeTypes> _volumeFilter;
	private readonly StrategyParam<bool> _allowBuyEntry;
	private readonly StrategyParam<bool> _allowSellEntry;
	private readonly StrategyParam<bool> _allowBuyExit;
	private readonly StrategyParam<bool> _allowSellExit;
	private readonly StrategyParam<decimal> _stopLossPips;
	private readonly StrategyParam<decimal> _takeProfitPips;

	private DateTimeOffset? _lastBullishSignalTime;
	private DateTimeOffset? _lastBearishSignalTime;
	private decimal _entryPrice;

	public DataType CandleType { get => _candleType.Value; set => _candleType.Value = value; }
	public int SignalBar { get => _signalBar.Value; set => _signalBar.Value = value; }
	public int MaxBarSize { get => _maxBarSize.Value; set => _maxBarSize.Value = value; }
	public ThreeCandlesVolumeTypes VolumeFilter { get => _volumeFilter.Value; set => _volumeFilter.Value = value; }
	public bool AllowBuyEntry { get => _allowBuyEntry.Value; set => _allowBuyEntry.Value = value; }
	public bool AllowSellEntry { get => _allowSellEntry.Value; set => _allowSellEntry.Value = value; }
	public bool AllowBuyExit { get => _allowBuyExit.Value; set => _allowBuyExit.Value = value; }
	public bool AllowSellExit { get => _allowSellExit.Value; set => _allowSellExit.Value = value; }
	public decimal StopLossPips { get => _stopLossPips.Value; set => _stopLossPips.Value = value; }
	public decimal TakeProfitPips { get => _takeProfitPips.Value; set => _takeProfitPips.Value = value; }

	public ThreeCandlesReversalStrategy()
	{
		_candleType = Param(nameof(CandleType), TimeSpan.FromMinutes(15).TimeFrame())
			.SetDisplay("Candle Type", "Time frame for the candle subscription", "General");
		_signalBar = Param(nameof(SignalBar), 1)
			.SetRange(0, 20)
			.SetDisplay("Signal Bar", "Historical offset where the signal is evaluated", "Pattern");
		_maxBarSize = Param(nameof(MaxBarSize), 300)
			.SetRange(0, 100000)
			.SetDisplay("Max Bar Size", "Disable the volume filter when the oldest candle range exceeds this value (in price steps)", "Pattern");
		_volumeFilter = Param(nameof(VolumeFilter), ThreeCandlesVolumeTypes.Tick)
			.SetDisplay("Volume Filter", "Volume filter used to confirm the reversal", "Pattern");
		_allowBuyEntry = Param(nameof(AllowBuyEntry), true)
			.SetDisplay("Allow Buy Entry", "Enable long entries on bullish signals", "Trading");
		_allowSellEntry = Param(nameof(AllowSellEntry), true)
			.SetDisplay("Allow Sell Entry", "Enable short entries on bearish signals", "Trading");
		_allowBuyExit = Param(nameof(AllowBuyExit), true)
			.SetDisplay("Allow Buy Exit", "Close long positions when a bearish pattern appears", "Trading");
		_allowSellExit = Param(nameof(AllowSellExit), true)
			.SetDisplay("Allow Sell Exit", "Close short positions when a bullish pattern appears", "Trading");
		_stopLossPips = Param(nameof(StopLossPips), 1000m)
			.SetRange(0m, 100000m)
			.SetDisplay("Stop Loss", "Distance to the protective stop in price steps", "Risk");
		_takeProfitPips = Param(nameof(TakeProfitPips), 2000m)
			.SetRange(0m, 100000m)
			.SetDisplay("Take Profit", "Distance to the profit target in price steps", "Risk");
	}

	public override IEnumerable<(Security sec, DataType dt)> GetWorkingSecurities()
		=> [(Security, CandleType)];

	protected override void OnReseted()
	{
		base.OnReseted();

		_candles.Clear();
		_lastBullishSignalTime = null;
		_lastBearishSignalTime = null;
		_entryPrice = 0m;
	}

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

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

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

		lock (_sync)
		{
			var closeTime = candle.CloseTime != default
				? candle.CloseTime
				: candle.OpenTime + (CandleType.Arg is TimeSpan tf ? tf : TimeSpan.Zero);

			_candles.Add(new CandleSample(
				candle.OpenTime,
				closeTime,
				candle.OpenPrice,
				candle.HighPrice,
				candle.LowPrice,
				candle.ClosePrice,
				candle.TotalVolume));

			var required = SignalBar + 5;
			while (_candles.Count > required)
				_candles.RemoveAt(0);

			if (_candles.Count < required)
				return;

			var priceStep = Security?.PriceStep ?? 1m;
			if (priceStep <= 0m)
				priceStep = 1m;

			if (CheckRiskManagement(candle, priceStep))
				return;

			var buffer = _candles.ToArray();
			var bullishSignal = IsBullishSignal(buffer, priceStep);
			var bearishSignal = IsBearishSignal(buffer, priceStep);

			if (bullishSignal)
			{
				var signalCandle = GetSeries(buffer, SignalBar);
				HandleBullish(signalCandle);
			}

			if (bearishSignal)
			{
				var signalCandle = GetSeries(buffer, SignalBar);
				HandleBearish(signalCandle);
			}
		}
	}

	private bool CheckRiskManagement(ICandleMessage candle, decimal priceStep)
	{
		if (Position == 0m || _entryPrice == 0m)
		return false;

		var stopDistance = StopLossPips > 0m ? StopLossPips * priceStep : 0m;
		var takeDistance = TakeProfitPips > 0m ? TakeProfitPips * priceStep : 0m;

		if (Position > 0m)
		{
		var stopTriggered = stopDistance > 0m && candle.LowPrice <= _entryPrice - stopDistance;
		var takeTriggered = takeDistance > 0m && candle.HighPrice >= _entryPrice + takeDistance;

		if (stopTriggered || takeTriggered)
		{
		SellMarket();
		ResetTradeState();
		return true;
		}
		}
		else if (Position < 0m)
		{
		var stopTriggered = stopDistance > 0m && candle.HighPrice >= _entryPrice + stopDistance;
		var takeTriggered = takeDistance > 0m && candle.LowPrice <= _entryPrice - takeDistance;

		if (stopTriggered || takeTriggered)
		{
		BuyMarket();
		ResetTradeState();
		return true;
		}
		}

		return false;
	}

	private void HandleBullish(CandleSample signalCandle)
	{
		var signalTime = signalCandle.CloseTime;
		if (_lastBullishSignalTime == signalTime)
			return;

		if (AllowSellExit && Position < 0m)
		{
			BuyMarket();
			ResetTradeState();
		}

		if (AllowBuyEntry && Position == 0m)
		{
			BuyMarket();
			_entryPrice = signalCandle.ClosePrice;
		}

		_lastBullishSignalTime = signalTime;
	}

	private void HandleBearish(CandleSample signalCandle)
	{
		var signalTime = signalCandle.CloseTime;
		if (_lastBearishSignalTime == signalTime)
			return;

		if (AllowBuyExit && Position > 0m)
		{
			SellMarket();
			ResetTradeState();
		}

		if (AllowSellEntry && Position == 0m)
		{
			SellMarket();
			_entryPrice = signalCandle.ClosePrice;
		}

		_lastBearishSignalTime = signalTime;
	}

	private bool IsBullishSignal(IReadOnlyList<CandleSample> candles, decimal priceStep)
	{
		var last = GetSeries(candles, SignalBar + 1);
		var middle = GetSeries(candles, SignalBar + 2);
		var oldest = GetSeries(candles, SignalBar + 3);

		if (!(oldest.OpenPrice > oldest.ClosePrice &&
			middle.OpenPrice > middle.ClosePrice &&
			middle.ClosePrice > oldest.LowPrice &&
			last.OpenPrice < last.ClosePrice &&
			last.ClosePrice > middle.OpenPrice))
		{
			return false;
		}

		if (!ShouldApplyVolumeFilter(oldest, priceStep))
			return true;

		var volOldest = oldest.Volume;
		var volMiddle = middle.Volume;
		var volLast = last.Volume;

		return volOldest < volMiddle || volLast > volMiddle || volLast > volOldest;
	}

	private bool IsBearishSignal(IReadOnlyList<CandleSample> candles, decimal priceStep)
	{
		var last = GetSeries(candles, SignalBar + 1);
		var middle = GetSeries(candles, SignalBar + 2);
		var oldest = GetSeries(candles, SignalBar + 3);

		if (!(oldest.OpenPrice < oldest.ClosePrice &&
			middle.OpenPrice < middle.ClosePrice &&
			middle.ClosePrice < oldest.HighPrice &&
			last.OpenPrice > last.ClosePrice &&
			last.ClosePrice < middle.OpenPrice))
		{
			return false;
		}

		if (!ShouldApplyVolumeFilter(oldest, priceStep))
			return true;

		var volOldest = oldest.Volume;
		var volMiddle = middle.Volume;
		var volLast = last.Volume;

		return volOldest < volMiddle || volLast > volMiddle || volLast > volOldest;
	}

	private bool ShouldApplyVolumeFilter(CandleSample oldest, decimal priceStep)
	{
		if (VolumeFilter == ThreeCandlesVolumeTypes.None)
			return false;

		if (MaxBarSize <= 0)
			return false;

		var range = oldest.HighPrice - oldest.LowPrice;
		var threshold = MaxBarSize * priceStep;

		if (range > threshold)
			return false;

		return true;
	}

	private static CandleSample GetSeries(IReadOnlyList<CandleSample> candles, int index)
	{
		var idx = candles.Count - 1 - index;
		return candles[idx];
	}

	private void ResetTradeState()
	{
		_entryPrice = 0m;
	}

	private readonly record struct CandleSample(
		DateTimeOffset OpenTime,
		DateTimeOffset CloseTime,
		decimal OpenPrice,
		decimal HighPrice,
		decimal LowPrice,
		decimal ClosePrice,
		decimal Volume);
}