Ver no GitHub

Estratégia de Reversão Evening Star

Visão geral

Esta estratégia é um port direto do Assessor Especializado EveningStar.mq5 (MQL5 id 18507). Monitora a formação clássica de velas da Estrela Vespertina e abre uma posição assim que a próxima barra começa a ser negociada. A lógica foi reescrita sobre a API de alto nível do StockSharp mantendo os filtros de padrão e o gerenciamento de risco originais.

Lógica de trading

  1. A estratégia se subscreve ao período selecionado pelo parâmetro CandleType. Todo o processamento ocorre apenas em velas concluídas.
  2. A cada vez que uma nova vela fecha, os últimos snapshots são armazenados em cache para que a janela de três velas definida por Shift possa ser avaliada.
  3. O padrão Evening Star é considerado válido quando:
    • A vela N-2 (a mais antiga) é altista (open < close).
    • A vela N-1 (do meio) satisfaz a preferência Candle2Bullish (altista por padrão).
    • A vela N (a mais recente) é baixista (open > close).
    • Se CheckCandleSizes estiver habilitado, a vela do meio deve ter o menor corpo das três.
    • Se ConsiderGap estiver habilitado, deve haver uma lacuna entre os corpos das velas da mesma forma que no robô original (o tamanho da lacuna é igual a um pip calculado a partir do passo de preço do instrumento).
  4. Uma vez confirmado o padrão, a estratégia verifica a direção selecionada por Direction:
    • Short (padrão) abre uma ordem de venda, correspondendo ao comportamento original da Evening Star.
    • Long permite executar a exposição exatamente oposta (mantido para paridade de funcionalidades com a versão MQL).
  5. Antes de abrir uma posição, o algoritmo opcionalmente fecha a exposição oposta se CloseOppositePositions estiver configurado como true.
  6. Os preços de stop-loss e take-profit são calculados a partir das distâncias em pips (StopLossPips, TakeProfitPips) usando o mesmo ajuste de 3/5 dígitos que existia no MetaTrader.
  7. O tamanho da posição é derivado do valor atual da carteira e RiskPercent. Se o volume calculado for menor que o tamanho mínimo negociável, o sinal é ignorado.

Gestão de posições

  • Quando uma posição comprada está ativa, a estratégia monitora cada nova vela. Se o preço mínimo rompe o nível de stop ou o preço máximo atinge o nível de take-profit, toda a posição é fechada a mercado.
  • Quando uma posição vendida está ativa, a mesma lógica é aplicada com comparações invertidas.
  • Se o valor da carteira ou a distância ao stop forem iguais a zero, o tamanho da ordem não pode ser calculado, portanto a entrada é ignorada.

Parâmetros

Nome Padrão Descrição
Direction Short Escolhe se o padrão deve abrir uma posição comprada ou vendida.
TakeProfitPips 150 Distância ao alvo de lucro expressa em pips. Definir como zero para desabilitar.
StopLossPips 50 Distância ao stop de proteção em pips. Um valor não positivo desabilita a operação.
RiskPercent 5 Porcentagem do patrimônio da carteira arriscada por operação. Usada para calcular o volume da ordem.
Shift 1 Número de barras ignoradas da vela mais recente antes de avaliar o padrão.
ConsiderGap true Requer uma lacuna entre corpos de velas como o Assessor Especializado original.
Candle2Bullish true Força a vela do meio a ser altista. Desabilitar para exigir uma vela do meio baixista.
CheckCandleSizes true Garante que a vela do meio tenha o menor corpo absoluto.
CloseOppositePositions true Fecha a exposição oposta antes de enviar a nova ordem.
CandleType Período 1H Série de velas usada para análise.

Notas

  • O tamanho do pip é derivado do passo de preço do instrumento. Para símbolos forex de 3 e 5 dígitos, um pip equivale a dez passos de preço, reproduzindo o comportamento do EA original.
  • Se StopLossPips for zero, o tamanho da posição não pode ser calculado e o sinal é ignorado para evitar risco ilimitado.
  • A estratégia recorta automaticamente o histórico em cache, de modo que o uso de memória permanece constante mesmo em sessões longas.
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; }
	}
}