Auf GitHub ansehen

Evening Star Umkehr-Strategie

Übersicht

Diese Strategie ist ein direkter Port des Expert Advisors EveningStar.mq5 (MQL5-ID 18507). Sie überwacht die klassische Evening-Star-Kerzenformation und eröffnet eine Position, sobald der nächste Bar zu handeln beginnt. Die Logik wurde auf der High-Level-API von StockSharp neu geschrieben, während die ursprünglichen Musterfilter und das Risikomanagement beibehalten wurden.

Handelslogik

  1. Die Strategie abonniert den durch den Parameter CandleType gewählten Zeitrahmen. Die gesamte Verarbeitung erfolgt nur bei abgeschlossenen Kerzen.
  2. Jedes Mal, wenn eine neue Kerze schließt, werden die letzten Snapshots gecacht, damit das durch Shift definierte Drei-Kerzen-Fenster ausgewertet werden kann.
  3. Das Evening-Star-Muster gilt als gültig, wenn:
    • Kerze N-2 (älteste) bullisch ist (open < close).
    • Kerze N-1 (mittlere) die Präferenz Candle2Bullish erfüllt (standardmäßig bullisch).
    • Kerze N (aktuellste) bärisch ist (open > close).
    • Wenn CheckCandleSizes aktiviert ist, muss die mittlere Kerze den kleinsten Körper der drei haben.
    • Wenn ConsiderGap aktiviert ist, muss es einen Gap zwischen den Kerzenkörpern geben, wie im originalen Roboter (Gapgröße entspricht einem Pip, berechnet aus dem Preisschritt des Instruments).
  4. Sobald das Muster bestätigt ist, prüft die Strategie die durch Direction gewählte Richtung:
    • Short (Standard) eröffnet eine Verkaufsorder, was dem ursprünglichen Evening-Star-Verhalten entspricht.
    • Long ermöglicht das Fahren der genau entgegengesetzten Exposure (für Feature-Parität mit der MQL-Version beibehalten).
  5. Vor dem Eröffnen einer Position schließt der Algorithmus optional die entgegengesetzte Exposure, wenn CloseOppositePositions auf true gesetzt ist.
  6. Stop-Loss- und Take-Profit-Preise werden aus den Pip-Abständen (StopLossPips, TakeProfitPips) mit derselben 3/5-Ziffern-Anpassung berechnet, die in MetaTrader vorhanden war.
  7. Die Positionsgröße wird aus dem aktuellen Portfoliowert und RiskPercent abgeleitet. Wenn das berechnete Volumen kleiner als die minimale handelbare Größe ist, wird das Signal ignoriert.

Positionsmanagement

  • Wenn eine Long-Position aktiv ist, überwacht die Strategie jede neue Kerze. Wenn der Tiefstkurs das Stop-Niveau unterschreitet oder der Höchstkurs das Take-Profit-Niveau erreicht, wird die gesamte Position zum Marktpreis geschlossen.
  • Wenn eine Short-Position aktiv ist, wird dieselbe Logik mit umgekehrten Vergleichen angewendet.
  • Wenn der Portfoliowert oder der Stop-Abstand null ist, kann die Ordergröße nicht berechnet werden, daher wird der Einstieg übersprungen.

Parameter

Name Standard Beschreibung
Direction Short Wählt, ob das Muster eine Long- oder Short-Position eröffnen soll.
TakeProfitPips 150 Abstand zum Gewinnziel in Pips. Auf null setzen, um zu deaktivieren.
StopLossPips 50 Abstand zum Schutz-Stop in Pips. Ein nicht-positiver Wert deaktiviert den Trade.
RiskPercent 5 Prozentsatz des Portfolio-Eigenkapitals, das pro Trade riskiert wird. Wird zur Berechnung des Ordervolumens verwendet.
Shift 1 Anzahl der Bars, die von der aktuellsten Kerze übersprungen werden, bevor das Muster ausgewertet wird.
ConsiderGap true Erfordert einen Gap zwischen Kerzenkörpern, genau wie der originale Expert Advisor.
Candle2Bullish true Zwingt die mittlere Kerze, bullisch zu sein. Deaktivieren, um eine bärische mittlere Kerze zu fordern.
CheckCandleSizes true Stellt sicher, dass die mittlere Kerze den kleinsten absoluten Körper hat.
CloseOppositePositions true Schließt die entgegengesetzte Exposure, bevor die neue Order gesendet wird.
CandleType 1H-Zeitrahmen Kerzenserie für die Analyse.

Hinweise

  • Die Pip-Größe wird aus dem Preisschritt des Instruments abgeleitet. Für 3- und 5-stellige Forex-Symbole entspricht ein Pip zehn Preisschritten, was das Verhalten des originalen EA reproduziert.
  • Wenn StopLossPips null ist, kann die Positionsgröße nicht berechnet werden, und das Signal wird ignoriert, um unbegrenzte Risiken zu verhindern.
  • Die Strategie kürzt den gecachten Verlauf automatisch, sodass der Speicherverbrauch auch bei langen Sitzungen konstant bleibt.
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; }
	}
}