Ver en GitHub

Estrategia de Reversión Estrella Vespertina

Descripción general

Esta estrategia es un port directo del Asesor Experto EveningStar.mq5 (MQL5 id 18507). Vigila la formación clásica de velas de la Estrella Vespertina y abre una posición tan pronto como la siguiente barra comienza a cotizar. La lógica ha sido reescrita sobre la API de alto nivel de StockSharp mientras se mantienen los filtros de patrón y la gestión de riesgo originales.

Lógica de trading

  1. La estrategia se suscribe al marco temporal seleccionado por el parámetro CandleType. Todo el procesamiento ocurre únicamente en velas terminadas.
  2. Cada vez que se cierra una nueva vela, las últimas instantáneas son almacenadas en caché para que la ventana de tres velas definida por Shift pueda ser evaluada.
  3. El patrón de la Estrella Vespertina se considera válido cuando:
    • La vela N-2 (la más antigua) es alcista (open < close).
    • La vela N-1 (la del medio) satisface la preferencia Candle2Bullish (alcista por defecto).
    • La vela N (la más reciente) es bajista (open > close).
    • Si CheckCandleSizes está habilitado, la vela del medio debe tener el cuerpo más pequeño de las tres.
    • Si ConsiderGap está habilitado, debe haber un gap entre los cuerpos de las velas de la misma manera que en el robot original (el tamaño del gap es igual a un pip calculado a partir del paso de precio del instrumento).
  4. Una vez confirmado el patrón, la estrategia verifica la dirección seleccionada por Direction:
    • Short (predeterminado) abre una orden de venta, coincidiendo con el comportamiento original de la Estrella Vespertina.
    • Long permite correr la exposición exactamente opuesta (mantenido para paridad de características con la versión MQL).
  5. Antes de abrir una posición, el algoritmo opcionalmente cierra la exposición opuesta si CloseOppositePositions está configurado como true.
  6. Los precios de stop-loss y take-profit se calculan a partir de las distancias en pips (StopLossPips, TakeProfitPips) usando el mismo ajuste de 3/5 dígitos que existía en MetaTrader.
  7. El tamaño de la posición se deriva del valor actual de la cartera y RiskPercent. Si el volumen calculado es menor que el tamaño mínimo negociable, la señal es ignorada.

Gestión de posiciones

  • Cuando una posición larga está activa, la estrategia monitorea cada nueva vela. Si el precio mínimo cae por debajo del nivel del stop o el precio máximo alcanza el nivel de take-profit, toda la posición se cierra a mercado.
  • Cuando una posición corta está activa, se aplica la misma lógica con comparaciones invertidas.
  • Si el valor de la cartera o la distancia al stop son iguales a cero, el tamaño de la orden no puede calcularse, por lo tanto la entrada es omitida.

Parámetros

Nombre Predeterminado Descripción
Direction Short Elige si el patrón debe abrir una posición larga o corta.
TakeProfitPips 150 Distancia al objetivo de beneficio expresada en pips. Establecer en cero para deshabilitar.
StopLossPips 50 Distancia al stop de protección en pips. Un valor no positivo deshabilita la operación.
RiskPercent 5 Porcentaje del capital de la cartera arriesgado por operación. Usado para calcular el volumen de la orden.
Shift 1 Número de barras omitidas desde la vela más reciente antes de evaluar el patrón.
ConsiderGap true Requiere un gap entre cuerpos de velas igual que el Asesor Experto original.
Candle2Bullish true Obliga a que la vela del medio sea alcista. Deshabilitar para requerir una vela del medio bajista.
CheckCandleSizes true Asegura que la vela del medio tenga el cuerpo absoluto más pequeño.
CloseOppositePositions true Cierra la exposición opuesta antes de enviar la nueva orden.
CandleType Marco temporal 1H Series de velas usadas para análisis.

Notas

  • El tamaño del pip se deriva del paso de precio del instrumento. Para símbolos forex de 3 y 5 dígitos, un pip equivale a diez pasos de precio, reproduciendo el comportamiento del EA original.
  • Si StopLossPips es cero, el tamaño de la posición no puede calcularse y la señal es ignorada para prevenir riesgo ilimitado.
  • La estrategia recorta automáticamente el historial en caché, por lo que el uso de memoria permanece constante incluso en sesiones largas.
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>
/// Evening Star candlestick pattern strategy converted from MQL5 implementation.
/// </summary>
public class EveningStarReversalStrategy : Strategy
{
	public enum PatternDirections
	{
		Long,
		Short
	}

	private readonly StrategyParam<PatternDirections> _direction;
	private readonly StrategyParam<int> _takeProfitPips;
	private readonly StrategyParam<int> _stopLossPips;
	private readonly StrategyParam<decimal> _riskPercent;
	private readonly StrategyParam<int> _shift;
	private readonly StrategyParam<bool> _considerGap;
	private readonly StrategyParam<bool> _candle2Bullish;
	private readonly StrategyParam<bool> _checkCandleSizes;
	private readonly StrategyParam<bool> _closeOpposite;
	private readonly StrategyParam<DataType> _candleType;

	private readonly List<CandleSnapshot> _history = new();

	private decimal _pipSize;
	private decimal _entryPrice;
	private decimal _stopPrice;
	private decimal _takeProfitPrice;

	public PatternDirections Direction
	{
		get => _direction.Value;
		set => _direction.Value = value;
	}

	public int TakeProfitPips
	{
		get => _takeProfitPips.Value;
		set => _takeProfitPips.Value = value;
	}

	public int StopLossPips
	{
		get => _stopLossPips.Value;
		set => _stopLossPips.Value = value;
	}

	public decimal RiskPercent
	{
		get => _riskPercent.Value;
		set => _riskPercent.Value = value;
	}

	public int Shift
	{
		get => _shift.Value;
		set => _shift.Value = value;
	}

	public bool ConsiderGap
	{
		get => _considerGap.Value;
		set => _considerGap.Value = value;
	}

	public bool Candle2Bullish
	{
		get => _candle2Bullish.Value;
		set => _candle2Bullish.Value = value;
	}

	public bool CheckCandleSizes
	{
		get => _checkCandleSizes.Value;
		set => _checkCandleSizes.Value = value;
	}

	public bool CloseOppositePositions
	{
		get => _closeOpposite.Value;
		set => _closeOpposite.Value = value;
	}

	public DataType CandleType
	{
		get => _candleType.Value;
		set => _candleType.Value = value;
	}

	public EveningStarReversalStrategy()
	{
		_direction = Param(nameof(Direction), PatternDirections.Short)
			.SetDisplay("Signal Direction", "Side to trade when the pattern appears", "General");

		_takeProfitPips = Param(nameof(TakeProfitPips), 150)
			.SetDisplay("Take Profit (pips)", "Take profit distance in pips", "Risk Management")
			.SetGreaterThanZero();

		_stopLossPips = Param(nameof(StopLossPips), 50)
			.SetDisplay("Stop Loss (pips)", "Stop loss distance in pips", "Risk Management")
			.SetGreaterThanZero();

		_riskPercent = Param(nameof(RiskPercent), 5m)
			.SetDisplay("Risk (%)", "Risk per trade as percentage of equity", "Risk Management")
			.SetGreaterThanZero();

		_shift = Param(nameof(Shift), 1)
			.SetDisplay("Shift", "Offset for the bar sequence", "Pattern")
			.SetGreaterThanZero();

		_considerGap = Param(nameof(ConsiderGap), true)
			.SetDisplay("Consider Gap", "Require price gaps between candles", "Pattern");

		_candle2Bullish = Param(nameof(Candle2Bullish), true)
			.SetDisplay("Middle Candle Bullish", "Should the second candle close above its open", "Pattern");

		_checkCandleSizes = Param(nameof(CheckCandleSizes), true)
			.SetDisplay("Check Candle Sizes", "Ensure the middle candle has the smallest body", "Pattern");

		_closeOpposite = Param(nameof(CloseOppositePositions), true)
			.SetDisplay("Close Opposite", "Close the existing opposite position before entry", "Execution");

		_candleType = Param(nameof(CandleType), TimeSpan.FromHours(1).TimeFrame())
			.SetDisplay("Candle Type", "Candle series to process", "General");
	}

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

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

		_history.Clear();
		_pipSize = 0m;
		_entryPrice = 0m;
		_stopPrice = 0m;
		_takeProfitPrice = 0m;
	}

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

		_pipSize = CalculatePipSize();

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

		// no protection needed
	}

	private void ProcessCandle(ICandleMessage candle)
	{
		// Ensure we only process finished candles.
		if (candle.State != CandleStates.Finished)
		return;

		// Store the candle snapshot for pattern evaluation.
		_history.Add(new CandleSnapshot(candle.OpenPrice, candle.ClosePrice, candle.HighPrice, candle.LowPrice));
		TrimHistory();

		// Manage any open trade before searching for a new signal.
		HandleActivePosition(candle);

		//if (!IsFormedAndOnlineAndAllowTrading())
		//return;

		// The pattern requires three completed candles with the configured shift.
		var requiredCount = Shift + 2;
		if (_history.Count < requiredCount)
		return;

		var lastIndex = _history.Count - Shift;
		if (lastIndex < 2 || lastIndex >= _history.Count)
		return;

		var recent = _history[lastIndex];
		var middle = _history[lastIndex - 1];
		var first = _history[lastIndex - 2];

		// Validate the Evening Star structure and optional filters.
		if (!IsPatternValid(first, middle, recent))
		return;

		var isLong = Direction == PatternDirections.Long;
		var entryPrice = recent.Close;
		var stopPrice = CalculateStop(entryPrice, isLong);
		var takeProfitPrice = CalculateTake(entryPrice, isLong);

		// Size the position using the risk percentage from the portfolio value.
		var volume = CalculatePositionSize(entryPrice, stopPrice);
		if (volume <= 0m)
		return;

		if (isLong)
		{
		if (Position < 0 && !CloseOppositePositions)
		return;

		if (Position < 0 && CloseOppositePositions)
		BuyMarket();

		BuyMarket();

		_entryPrice = entryPrice;
		_stopPrice = stopPrice;
		_takeProfitPrice = takeProfitPrice;
		}
		else
		{
		if (Position > 0 && !CloseOppositePositions)
		return;

		if (Position > 0 && CloseOppositePositions)
		SellMarket();

		SellMarket();

		_entryPrice = entryPrice;
		_stopPrice = stopPrice;
		_takeProfitPrice = takeProfitPrice;
		}
	}

	private void HandleActivePosition(ICandleMessage candle)
	{
		if (Position == 0)
		{
		// Nothing is open, so cached targets must be cleared.
		ResetTargets();
		return;
		}

		if (Position > 0)
		{
		var stopHit = _stopPrice > 0m && candle.LowPrice <= _stopPrice;
		var takeHit = _takeProfitPrice > 0m && candle.HighPrice >= _takeProfitPrice;

		if (stopHit || takeHit)
		{
		SellMarket();
		ResetTargets();
		}
		}
		else if (Position < 0)
		{
		var stopHit = _stopPrice > 0m && candle.HighPrice >= _stopPrice;
		var takeHit = _takeProfitPrice > 0m && candle.LowPrice <= _takeProfitPrice;

		if (stopHit || takeHit)
		{
		BuyMarket();
		ResetTargets();
		}
		}
	}

	private bool IsPatternValid(CandleSnapshot first, CandleSnapshot middle, CandleSnapshot recent)
	{
		// Evening Star requires a bullish candle, a small-bodied candle, then a bearish candle.
		if (!(recent.Open > recent.Close && first.Open < first.Close))
		return false;

		if (CheckCandleSizes)
		{
		var lastBody = Math.Abs(recent.Open - recent.Close);
		var middleBody = Math.Abs(middle.Open - middle.Close);
		var firstBody = Math.Abs(first.Open - first.Close);

		if (lastBody < middleBody || firstBody < middleBody)
		return false;
		}

		if (Candle2Bullish)
		{
		if (middle.Open > middle.Close)
		return false;
		}
		else
		{
		if (middle.Close > middle.Open)
		return false;
		}

		if (ConsiderGap && _pipSize > 0m)
		{
		var gap = _pipSize;
		if (recent.Open >= middle.Close - gap || middle.Open <= first.Close + gap)
		return false;
		}

		return true;
	}

	private decimal CalculateStop(decimal entryPrice, bool isLong)
	{
		var distance = StopLossPips * _pipSize;
		if (distance <= 0m)
		return 0m;

		return isLong ? entryPrice - distance : entryPrice + distance;
	}

	private decimal CalculateTake(decimal entryPrice, bool isLong)
	{
		var distance = TakeProfitPips * _pipSize;
		if (distance <= 0m)
		return 0m;

		return isLong ? entryPrice + distance : entryPrice - distance;
	}

	private decimal CalculatePositionSize(decimal entryPrice, decimal stopPrice)
	{
		// Simplified: always return Volume (from base Strategy)
		return Volume;
	}

	private decimal CalculatePipSize()
	{
		var step = Security.PriceStep ?? 0m;
		if (step <= 0m)
		return 0m;

		var decimals = Security.Decimals;
		// Forex symbols use fractional pips; replicate the 3/5 digit adjustment from MQL.
		return decimals is 3 or 5 ? step * 10m : step;
	}

	private void TrimHistory()
	{
		// Keep only the most recent candles needed for pattern detection.
		var maxCount = Math.Max(Shift + 5, 10);
		if (_history.Count <= maxCount)
		return;

		while (_history.Count > maxCount)
			try { _history.RemoveAt(0); } catch { break; }
	}

	private void ResetTargets()
	{
		_entryPrice = 0m;
		_stopPrice = 0m;
		_takeProfitPrice = 0m;
	}

	// Lightweight snapshot to keep only the data required for pattern checks.
	private readonly struct CandleSnapshot
	{
		public CandleSnapshot(decimal open, decimal close, decimal high, decimal low)
		{
			Open = open;
			Close = close;
			High = high;
			Low = low;
		}

		public decimal Open { get; }
		public decimal Close { get; }
		public decimal High { get; }
		public decimal Low { get; }
	}
}