Ver en GitHub

Estrategia de Reversión de Tres Velas

Esta estrategia es un port fiel de StockSharp del asesor experto MQL5 Exp_ThreeCandles. Busca una reversión clásica de tres velas:

  1. Dos velas consecutivas en una dirección.
  2. Una tercera vela que invierte la dirección y cierra más allá de la barra del medio.
  3. Confirmación opcional de volumen a menos que la barra más antigua del patrón sea excepcionalmente grande.

Cuando aparece una configuración alcista el algoritmo cierra la exposición corta y puede entrar en una posición larga. Una configuración bajista hace lo contrario. Los niveles de stop-loss y take-profit protectores se aplican usando el paso de precio actual del instrumento.

Detección del patrón

La estrategia mantiene una ventana deslizante de las SignalBar + 3 velas terminadas más recientes. En cada nueva barra verifica la vela en el desplazamiento SignalBar (por defecto: 1 barra atrás) y las tres velas más antiguas:

  • Reversión alcista (potencial compra):
    • Las dos velas más antiguas (SignalBar + 3 y SignalBar + 2) son bajistas.
    • La vela del medio cierra por encima del mínimo de la barra más antigua.
    • La vela más reciente antes de la señal (SignalBar + 1) es alcista y cierra por encima de la apertura de la vela del medio.
  • Reversión bajista (potencial venta):
    • Lógica espejo del caso alcista.

Un filtro de volumen refleja el indicador original. El filtro se omite cuando MaxBarSize (en pasos de precio) es superado por el rango de la vela más antigua o cuando VolumeFilter está configurado como None. De lo contrario, la reversión debe satisfacer volumen antiguo < volumen medio O volumen reciente > volumen medio O volumen reciente > volumen más antiguo. El volumen tick y real se mapean al volumen agregado de la vela porque StockSharp no distingue los dos en el flujo de velas de alto nivel.

Gestión de operaciones

  • Si AllowSellExit está habilitado, un patrón alcista cubre inmediatamente cualquier posición corta antes de considerar una entrada larga. AllowBuyExit se comporta igual para posiciones largas en patrones bajistas.
  • Las nuevas posiciones solo se abren cuando la posición actual es plana y la bandera Allow*Entry correspondiente es verdadera. El tamaño de la orden usa la configuración de volumen estándar de la estrategia.
  • Las distancias de stop-loss y take-profit (StopLossPips, TakeProfitPips) se expresan en pasos de precio y se monitorean en cada vela terminada.
  • El último tiempo de señal alcista/bajista procesado se almacena en caché para evitar acciones duplicadas mientras una vela sigue generando ticks.

Parámetros

Nombre Predeterminado Descripción
CandleType Marco temporal de 4 horas Serie de velas procesada por la estrategia.
SignalBar 1 Cuántas barras atrás se evalúa la señal. Debe ser ≥ 0.
MaxBarSize 300 Si el rango de la barra más antigua (convertido con PriceStep) supera este valor, el filtro de volumen se omite. Configurar en 0 para siempre omitir.
VolumeFilter Tick Modo de volumen (Tick, Real o None). Tanto Tick como Real usan TotalVolume de las velas.
AllowBuyEntry true Habilitar entradas largas en patrones alcistas.
AllowSellEntry true Habilitar entradas cortas en patrones bajistas.
AllowBuyExit true Permitir cerrar posiciones largas en patrones bajistas.
AllowSellExit true Permitir cerrar posiciones cortas en patrones alcistas.
StopLossPips 1000 Distancia de stop-loss en pasos de precio (0 deshabilita).
TakeProfitPips 2000 Distancia de take-profit en pasos de precio (0 deshabilita).

Notas de conversión

  • Las rutinas de gestión de dinero del archivo de inclusión original MQL5 fueron reemplazadas por llamadas BuyMarket/SellMarket de StockSharp. Por lo tanto, el tamaño de posición sigue el volumen predeterminado del motor.
  • El tiempo de señal refleja el asesor experto evaluando la barra en el desplazamiento SignalBar y manteniendo la marca de tiempo de la señal anterior.
  • Las alertas de correo electrónico, push y sonido del indicador MQL se omiten intencionalmente.
  • Los modos de volumen se preservan pero ambos se mapean al volumen agregado de la vela porque los volúmenes tick y real separados no están disponibles en la API de alto nivel.
  • Todos los comentarios fueron reescritos en inglés según lo requerido por las pautas del proyecto.

Esta implementación se mantiene cerca del comportamiento original mientras adhiere al modelo de suscripción de alto nivel de StockSharp.

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);
}