Ver en GitHub

Estrategia de Velas Ejecutoras

Esta estrategia es una conversión directa del experto MetaTrader "Executor Candles". Reacciona a un rico conjunto de patrones de reversión alcistas y bajistas de velas y puede opcionalmente confirmar operaciones con una vela de tendencia de mayor timeframe. Toda la lógica de gestión de operaciones – stops, take-profits y trailing stops – refleja el comportamiento del experto original medido en pips (pasos de precio).

Cómo funciona

  • Filtro de tendencia: Cuando UseTrendFilter está habilitado, la estrategia observa la vela terminada más reciente de TrendCandleType. Los setups largos solo se permiten si esa vela cerró alcista, mientras que los setups cortos requieren un cierre bajista. Con el filtro desactivado (predeterminado), solo se usa la lógica de patrones.
  • Patrones largos: Martillo, envolvente alcista, línea penetrante, estrella de la mañana y estructuras de estrella doji de la mañana tomadas de las últimas tres velas de trading completadas.
  • Patrones cortos: Hombre colgado, envolvente bajista, cobertura de nube oscura, estrella vespertina y confirmaciones de estrella doji vespertina.
  • Gestión de operaciones:
    • Distancias separadas de stop-loss y take-profit para posiciones largas y cortas expresadas en pips (StopLossBuyPips, TakeProfitBuyPips, StopLossSellPips, TakeProfitSellPips).
    • Trailing stops opcionales para ambas direcciones controlados por TrailingStopBuyPips, TrailingStopSellPips y el desplazamiento mínimo TrailingStepPips. Una actualización de trailing se hace solo después de que el precio avanza por la distancia del stop más el paso de trailing, replicando la lógica de MetaTrader.
    • Las órdenes se colocan con OrderVolume lotes y la posición actual se revierte completamente con órdenes de mercado cuando se activa una condición de salida.

La estrategia se suscribe al CandleType configurado para señales de trading y, si es necesario, al TrendCandleType para la vela de confirmación. Mantiene un búfer interno de las últimas tres velas de trading terminadas para evaluar los patrones de múltiples barras sin almacenar historiales largos.

Parámetros

  • CandleType – timeframe usado para detectar los patrones de velas.
  • TrendCandleType – vela de mayor timeframe usada cuando el filtro de tendencia está activo.
  • OrderVolume – tamaño de orden para entradas y salidas de mercado.
  • StopLossBuyPips, TakeProfitBuyPips, TrailingStopBuyPips – controles de riesgo para posiciones largas.
  • StopLossSellPips, TakeProfitSellPips, TrailingStopSellPips – controles de riesgo para posiciones cortas.
  • TrailingStepPips – movimiento favorable mínimo antes de que el trailing stop se ajuste.
  • UseTrendFilter – habilita o deshabilita la confirmación de mayor timeframe.

Notas

  • Todas las distancias basadas en pips se multiplican por el PriceStep del instrumento. Asegúrese de que esté configurado correctamente para niveles de riesgo precisos.
  • Las verificaciones de entrada se ejecutan en cada vela terminada; los ticks en vivo simplemente actualizan la barra más reciente sin cambiar el flujo de decisiones.
  • La estrategia emite solo órdenes de mercado y espera que la ejecución ocurra inmediatamente como en la versión de MetaTrader.
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>
/// Candle pattern strategy converted from the Executor Candles MetaTrader expert.
/// </summary>
public class ExecutorCandlesStrategy : Strategy
{
	private readonly StrategyParam<int> _stopLossBuyPips;
	private readonly StrategyParam<int> _takeProfitBuyPips;
	private readonly StrategyParam<int> _trailingStopBuyPips;
	private readonly StrategyParam<int> _stopLossSellPips;
	private readonly StrategyParam<int> _takeProfitSellPips;
	private readonly StrategyParam<int> _trailingStopSellPips;
	private readonly StrategyParam<int> _trailingStepPips;
	private readonly StrategyParam<decimal> _volume;
	private readonly StrategyParam<bool> _useTrendFilter;
	private readonly StrategyParam<DataType> _candleType;
	private readonly StrategyParam<DataType> _trendCandleType;

	private ICandleMessage _prev1;
	private ICandleMessage _prev2;
	private ICandleMessage _prev3;

	private decimal? _entryPrice;
	private decimal? _stopLevel;
	private decimal? _takeLevel;

	private bool? _trendDown;

	private decimal _priceStep;
	private decimal _tolerance;

	/// <summary>
	/// Stop loss distance for long positions in pips.
	/// </summary>
	public int StopLossBuyPips
	{
		get => _stopLossBuyPips.Value;
		set => _stopLossBuyPips.Value = value;
	}

	/// <summary>
	/// Take profit distance for long positions in pips.
	/// </summary>
	public int TakeProfitBuyPips
	{
		get => _takeProfitBuyPips.Value;
		set => _takeProfitBuyPips.Value = value;
	}

	/// <summary>
	/// Trailing stop distance for long positions in pips.
	/// </summary>
	public int TrailingStopBuyPips
	{
		get => _trailingStopBuyPips.Value;
		set => _trailingStopBuyPips.Value = value;
	}

	/// <summary>
	/// Stop loss distance for short positions in pips.
	/// </summary>
	public int StopLossSellPips
	{
		get => _stopLossSellPips.Value;
		set => _stopLossSellPips.Value = value;
	}

	/// <summary>
	/// Take profit distance for short positions in pips.
	/// </summary>
	public int TakeProfitSellPips
	{
		get => _takeProfitSellPips.Value;
		set => _takeProfitSellPips.Value = value;
	}

	/// <summary>
	/// Trailing stop distance for short positions in pips.
	/// </summary>
	public int TrailingStopSellPips
	{
		get => _trailingStopSellPips.Value;
		set => _trailingStopSellPips.Value = value;
	}

	/// <summary>
	/// Minimum trailing step in pips required before adjusting the stop.
	/// </summary>
	public int TrailingStepPips
	{
		get => _trailingStepPips.Value;
		set => _trailingStepPips.Value = value;
	}

	/// <summary>
	/// Volume used for market orders.
	/// </summary>
	public decimal OrderVolume
	{
		get => _volume.Value;
		set => _volume.Value = value;
	}

	/// <summary>
	/// Enables confirmation from a higher timeframe candle.
	/// </summary>
	public bool UseTrendFilter
	{
		get => _useTrendFilter.Value;
		set => _useTrendFilter.Value = value;
	}

	/// <summary>
	/// Trading candle type.
	/// </summary>
	public DataType CandleType
	{
		get => _candleType.Value;
		set => _candleType.Value = value;
	}

	/// <summary>
	/// Higher timeframe candle type for the trend filter.
	/// </summary>
	public DataType TrendCandleType
	{
		get => _trendCandleType.Value;
		set => _trendCandleType.Value = value;
	}

	/// <summary>
	/// Initializes a new instance of the <see cref="ExecutorCandlesStrategy"/>.
	/// </summary>
	public ExecutorCandlesStrategy()
	{
		_stopLossBuyPips = Param(nameof(StopLossBuyPips), 50)
		.SetDisplay("Stop Loss Buy", "Stop loss for long trades", "Risk");

		_takeProfitBuyPips = Param(nameof(TakeProfitBuyPips), 50)
		.SetDisplay("Take Profit Buy", "Take profit for long trades", "Risk");

		_trailingStopBuyPips = Param(nameof(TrailingStopBuyPips), 15)
		.SetDisplay("Trailing Stop Buy", "Trailing stop for long trades", "Risk");

		_stopLossSellPips = Param(nameof(StopLossSellPips), 50)
		.SetDisplay("Stop Loss Sell", "Stop loss for short trades", "Risk");

		_takeProfitSellPips = Param(nameof(TakeProfitSellPips), 50)
		.SetDisplay("Take Profit Sell", "Take profit for short trades", "Risk");

		_trailingStopSellPips = Param(nameof(TrailingStopSellPips), 15)
		.SetDisplay("Trailing Stop Sell", "Trailing stop for short trades", "Risk");

		_trailingStepPips = Param(nameof(TrailingStepPips), 5)
		.SetDisplay("Trailing Step", "Minimum trailing step", "Risk");

		_volume = Param(nameof(OrderVolume), 1m)
		.SetGreaterThanZero()
		.SetDisplay("Volume", "Order volume", "Trading");

		_useTrendFilter = Param(nameof(UseTrendFilter), false)
		.SetDisplay("Use Trend Filter", "Enable higher timeframe confirmation", "Filters");

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

		_trendCandleType = Param(nameof(TrendCandleType), TimeSpan.FromMinutes(15).TimeFrame())
		.SetDisplay("Trend Candle Type", "Higher timeframe", "General");
	}

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

		return [(Security, CandleType)];
	}

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

		_prev1 = null;
		_prev2 = null;
		_prev3 = null;
		_entryPrice = null;
		_stopLevel = null;
		_takeLevel = null;
		_trendDown = null;
		_priceStep = 0m;
		_tolerance = 0m;
	}

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

		_priceStep = Security?.PriceStep ?? 0.0001m;
		if (_priceStep <= 0m)
		_priceStep = 0.0001m;

		_tolerance = _priceStep / 2m;

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

		if (UseTrendFilter)
		{
			var trendSubscription = SubscribeCandles(TrendCandleType);
			trendSubscription.Bind(ProcessTrendCandle).Start();
		}
		else
		{
			_trendDown = false;
		}
	}

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

		_trendDown = candle.OpenPrice >= candle.ClosePrice;
	}

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

		_prev3 = _prev2;
		_prev2 = _prev1;
		_prev1 = candle;

		if (!IsFormedAndOnlineAndAllowTrading())
		return;

		if (UseTrendFilter && _trendDown == null)
		return;

		if (Position == 0)
		TryOpenPosition();

		ManageActivePosition(candle);
	}

	private void TryOpenPosition()
	{
		if (_prev1 == null || _prev2 == null)
		return;

		var trendDown = _trendDown ?? false;

		if (IsHammer(trendDown, _prev1, _prev2))
		{
			OpenLong(_prev1.ClosePrice);
			return;
		}

		if (IsBullishEngulfing(trendDown, _prev1, _prev2))
		{
			OpenLong(_prev1.ClosePrice);
			return;
		}

		if (IsPiercing(trendDown, _prev1, _prev2))
		{
			OpenLong(_prev1.ClosePrice);
			return;
		}

		if (_prev3 != null && IsMorningStar(_prev1, _prev2, _prev3))
		{
			OpenLong(_prev1.ClosePrice);
			return;
		}

		if (_prev3 != null && IsMorningDojiStar(_prev1, _prev2, _prev3))
		{
			OpenLong(_prev1.ClosePrice);
			return;
		}

		if (IsHangingMan(trendDown, _prev1, _prev2))
		{
			OpenShort(_prev1.ClosePrice);
			return;
		}

		if (IsBearishEngulfing(trendDown, _prev1, _prev2))
		{
			OpenShort(_prev1.ClosePrice);
			return;
		}

		if (IsDarkCloudCover(trendDown, _prev1, _prev2))
		{
			OpenShort(_prev1.ClosePrice);
			return;
		}

		if (_prev3 != null && IsEveningStar(_prev1, _prev2, _prev3))
		{
			OpenShort(_prev1.ClosePrice);
			return;
		}

		if (_prev3 != null && IsEveningDojiStar(_prev1, _prev2, _prev3))
		OpenShort(_prev1.ClosePrice);
	}

	private void OpenLong(decimal price)
	{
		BuyMarket(OrderVolume);

		_entryPrice = price;
		_stopLevel = StopLossBuyPips > 0 ? price - StopLossBuyPips * _priceStep : null;
		_takeLevel = TakeProfitBuyPips > 0 ? price + TakeProfitBuyPips * _priceStep : null;
	}

	private void OpenShort(decimal price)
	{
		SellMarket(OrderVolume);

		_entryPrice = price;
		_stopLevel = StopLossSellPips > 0 ? price + StopLossSellPips * _priceStep : null;
		_takeLevel = TakeProfitSellPips > 0 ? price - TakeProfitSellPips * _priceStep : null;
	}

	private void ManageActivePosition(ICandleMessage candle)
	{
		if (Position > 0)
		{
			HandleLongPosition(candle);
		}
		else if (Position < 0)
		{
			HandleShortPosition(candle);
		}
		else
		{
			ResetPositionState();
		}
	}

	private void HandleLongPosition(ICandleMessage candle)
	{
		if (_stopLevel.HasValue && candle.LowPrice <= _stopLevel.Value)
		{
			SellMarket(Math.Abs(Position));
			ResetPositionState();
			return;
		}

		if (_takeLevel.HasValue && candle.HighPrice >= _takeLevel.Value)
		{
			SellMarket(Math.Abs(Position));
			ResetPositionState();
			return;
		}

		if (_entryPrice == null)
		return;

		if (TrailingStopBuyPips <= 0 || TrailingStepPips <= 0)
		return;

		var trailingDistance = TrailingStopBuyPips * _priceStep;
		var trailingStep = TrailingStepPips * _priceStep;
		var progress = candle.ClosePrice - _entryPrice.Value;

		if (progress <= trailingDistance + trailingStep)
		return;

		var newStop = candle.ClosePrice - trailingDistance;

		if (!_stopLevel.HasValue || newStop - _stopLevel.Value >= trailingStep)
		_stopLevel = newStop;
	}

	private void HandleShortPosition(ICandleMessage candle)
	{
		if (_stopLevel.HasValue && candle.HighPrice >= _stopLevel.Value)
		{
			BuyMarket(Math.Abs(Position));
			ResetPositionState();
			return;
		}

		if (_takeLevel.HasValue && candle.LowPrice <= _takeLevel.Value)
		{
			BuyMarket(Math.Abs(Position));
			ResetPositionState();
			return;
		}

		if (_entryPrice == null)
		return;

		if (TrailingStopSellPips <= 0 || TrailingStepPips <= 0)
		return;

		var trailingDistance = TrailingStopSellPips * _priceStep;
		var trailingStep = TrailingStepPips * _priceStep;
		var progress = _entryPrice.Value - candle.ClosePrice;

		if (progress <= trailingDistance + trailingStep)
		return;

		var newStop = candle.ClosePrice + trailingDistance;

		if (!_stopLevel.HasValue || _stopLevel.Value - newStop >= trailingStep)
		_stopLevel = newStop;
	}

	private void ResetPositionState()
	{
		if (Position == 0)
		{
			_entryPrice = null;
			_stopLevel = null;
			_takeLevel = null;
		}
	}

	private bool IsHammer(bool trendDown, ICandleMessage current, ICandleMessage previous)
	{
		if (AreEqual(current.ClosePrice, current.OpenPrice))
		return false;

		if (current.ClosePrice > current.OpenPrice && previous.OpenPrice > previous.ClosePrice)
		{
			var body = current.ClosePrice - current.OpenPrice;
			if (body <= 0m)
			return false;

			var upper = (current.HighPrice - current.ClosePrice) * 100m / body;
			var lower = (current.OpenPrice - current.LowPrice) * 100m / body;

			return upper > 200m && lower < 15m;
		}

		return false;
	}

	private bool IsBullishEngulfing(bool trendDown, ICandleMessage current, ICandleMessage previous)
	{
		if (AreEqual(previous.OpenPrice, previous.ClosePrice))
		return false;

		if (current.ClosePrice > current.OpenPrice && previous.OpenPrice > previous.ClosePrice)
		{
			if (current.ClosePrice < previous.OpenPrice)
			return false;

			if (current.OpenPrice > previous.ClosePrice)
			return false;

			var prevBody = previous.OpenPrice - previous.ClosePrice;
			var currBody = current.ClosePrice - current.OpenPrice;

			if (prevBody == 0m)
			return false;

			return currBody / prevBody > 1.5m;
		}

		return false;
	}

	private bool IsPiercing(bool trendDown, ICandleMessage current, ICandleMessage previous)
	{
		if (AreEqual(previous.HighPrice, previous.LowPrice))
		return false;

		if (current.ClosePrice > current.OpenPrice && previous.OpenPrice > previous.ClosePrice)
		{
			var body = previous.OpenPrice - previous.ClosePrice;
			var range = previous.HighPrice - previous.LowPrice;

			if (range == 0m)
			return false;

			var ratio = body / range;
			var midpoint = previous.ClosePrice + body / 2m;

			return ratio > 0.6m && current.OpenPrice < previous.LowPrice && current.ClosePrice > midpoint;
		}

		return false;
	}

	private bool IsMorningStar(ICandleMessage current, ICandleMessage middle, ICandleMessage older)
	{
		if (AreEqual(older.OpenPrice, older.ClosePrice))
		return false;

		if (AreEqual(older.HighPrice, older.LowPrice))
		return false;

		if (AreEqual(middle.HighPrice, middle.LowPrice))
		return false;

		if (AreEqual(current.HighPrice, current.LowPrice))
		return false;

		if (older.OpenPrice > older.ClosePrice && middle.ClosePrice > middle.OpenPrice && current.ClosePrice > current.OpenPrice)
		{
			if (middle.ClosePrice >= older.ClosePrice)
			return false;

			if (current.OpenPrice <= middle.ClosePrice)
			return false;

			var numerator = Math.Abs(older.OpenPrice - current.ClosePrice) + Math.Abs(current.OpenPrice - older.ClosePrice);
			var denominator = older.OpenPrice - older.ClosePrice;

			if (denominator == 0m)
			return false;

			var olderRatio = denominator / (older.HighPrice - older.LowPrice);
			var middleRatio = (middle.ClosePrice - middle.OpenPrice) / (middle.HighPrice - middle.LowPrice);
			var currentRatio = (current.ClosePrice - current.OpenPrice) / (current.HighPrice - current.LowPrice);

			return numerator / denominator < 0.1m && olderRatio > 0.8m && middleRatio < 0.3m && currentRatio > 0.8m;
		}

		return false;
	}

	private bool IsMorningDojiStar(ICandleMessage current, ICandleMessage middle, ICandleMessage older)
	{
		if (AreEqual(older.OpenPrice, older.ClosePrice))
		return false;

		if (older.OpenPrice <= older.ClosePrice)
		return false;

		if (!AreEqual(middle.ClosePrice, middle.OpenPrice))
		return false;

		if (current.ClosePrice <= current.OpenPrice)
		return false;

		if (middle.ClosePrice > older.ClosePrice)
		return false;

		if (current.OpenPrice < middle.ClosePrice)
		return false;

		var numerator = Math.Abs(older.OpenPrice - current.ClosePrice) + Math.Abs(current.OpenPrice - older.ClosePrice);
		var denominator = older.OpenPrice - older.ClosePrice;

		if (denominator == 0m)
		return false;

		return numerator / denominator < 0.1m;
	}

	private bool IsHangingMan(bool trendDown, ICandleMessage current, ICandleMessage previous)
	{
		if (AreEqual(current.OpenPrice, current.ClosePrice))
		return false;

		if (current.OpenPrice > current.ClosePrice && previous.OpenPrice < previous.ClosePrice)
		{
			var body = current.OpenPrice - current.ClosePrice;
			if (body <= 0m)
			return false;

			var upper = (current.HighPrice - current.OpenPrice) * 100m / body;
			var lower = (current.ClosePrice - current.LowPrice) * 100m / body;

			return upper < 15m && lower > 200m;
		}

		return false;
	}

	private bool IsBearishEngulfing(bool trendDown, ICandleMessage current, ICandleMessage previous)
	{
		if (AreEqual(previous.ClosePrice, previous.OpenPrice))
		return false;

		if (current.OpenPrice > current.ClosePrice && previous.ClosePrice > previous.OpenPrice)
		{
			if (current.OpenPrice < previous.ClosePrice)
			return false;

			if (current.ClosePrice > previous.OpenPrice)
			return false;

			var prevBody = previous.ClosePrice - previous.OpenPrice;
			var currBody = current.OpenPrice - current.ClosePrice;

			if (prevBody == 0m)
			return false;

			return currBody / prevBody > 1.5m;
		}

		return false;
	}

	private bool IsDarkCloudCover(bool trendDown, ICandleMessage current, ICandleMessage previous)
	{
		if (AreEqual(previous.HighPrice, previous.LowPrice))
		return false;

		if (current.OpenPrice > current.ClosePrice && previous.ClosePrice > previous.OpenPrice)
		{
			var body = previous.ClosePrice - previous.OpenPrice;
			var range = previous.HighPrice - previous.LowPrice;

			if (range == 0m)
			return false;

			var ratio = body / range;
			var midpoint = previous.OpenPrice + (previous.ClosePrice - previous.OpenPrice) / 2m;

			return ratio > 0.6m && current.OpenPrice > previous.HighPrice && current.ClosePrice < midpoint;
		}

		return false;
	}

	private bool IsEveningStar(ICandleMessage current, ICandleMessage middle, ICandleMessage older)
	{
		if (AreEqual(older.ClosePrice, older.OpenPrice))
		return false;

		if (AreEqual(older.HighPrice, older.LowPrice))
		return false;

		if (AreEqual(current.HighPrice, current.LowPrice))
		return false;

		if (older.OpenPrice < older.ClosePrice && middle.ClosePrice < middle.OpenPrice && current.ClosePrice < current.OpenPrice)
		{
			if (middle.ClosePrice <= older.ClosePrice)
			return false;

			if (current.OpenPrice >= middle.ClosePrice)
			return false;

			var numerator = Math.Abs(older.OpenPrice - current.ClosePrice) + Math.Abs(current.OpenPrice - older.ClosePrice);
			var denominator = older.ClosePrice - older.OpenPrice;

			if (denominator == 0m)
			return false;

			var olderRatio = (older.ClosePrice - older.OpenPrice) / (older.HighPrice - older.LowPrice);
			var middleRatio = (middle.OpenPrice - middle.ClosePrice) / (middle.HighPrice - middle.LowPrice);
			var currentRatio = (current.OpenPrice - current.ClosePrice) / (current.HighPrice - current.LowPrice);

			return numerator / denominator < 0.1m && olderRatio > 0.8m && middleRatio < 0.3m && currentRatio > 0.8m;
		}

		return false;
	}

	private bool IsEveningDojiStar(ICandleMessage current, ICandleMessage middle, ICandleMessage older)
	{
		if (AreEqual(older.OpenPrice, older.ClosePrice))
		return false;

		if (older.OpenPrice >= older.ClosePrice)
		return false;

		if (!AreEqual(middle.ClosePrice, middle.OpenPrice))
		return false;

		if (current.ClosePrice >= current.OpenPrice)
		return false;

		if (middle.ClosePrice < older.ClosePrice)
		return false;

		if (current.OpenPrice > middle.ClosePrice)
		return false;

		var numerator = Math.Abs(older.OpenPrice - current.ClosePrice) + Math.Abs(current.OpenPrice - older.ClosePrice);
		var denominator = older.OpenPrice - older.ClosePrice;

		if (denominator == 0m)
		return false;

		return numerator / denominator < 0.1m;
	}

	private bool AreEqual(decimal a, decimal b)
	{
		return Math.Abs(a - b) <= _tolerance;
	}
}