Auf GitHub ansehen

Drei-Kerzen-Umkehr-Strategie

Diese Strategie ist ein getreuer StockSharp-Port des MQL5-Expertenberaters Exp_ThreeCandles. Sie sucht nach einer klassischen Drei-Kerzen-Umkehr:

  1. Zwei aufeinanderfolgende Kerzen in eine Richtung.
  2. Eine dritte Kerze, die die Richtung wechselt und jenseits der mittleren Kerze schließt.
  3. Optionale Volumenbestätigung, es sei denn, die älteste Kerze im Muster ist außergewöhnlich groß.

Wenn eine bullische Konfiguration erscheint, schließt der Algorithmus Short-Exposure und kann eine Long-Position eingehen. Eine bärische Konfiguration tut das Gegenteil. Schützende Stop-Loss- und Take-Profit-Level werden mit dem aktuellen Instrumentenpreisschritt angewendet.

Mustererkennung

Die Strategie hält ein rollierendes Fenster der neuesten SignalBar + 3 fertiggestellten Kerzen. Bei jedem neuen Balken prüft sie die Kerze beim SignalBar-Versatz (Standard: 1 Balken zurück) und die drei älteren Kerzen:

  • Bullische Umkehr (potentielles Long):
    • Die zwei älteren Kerzen (SignalBar + 3 und SignalBar + 2) sind bärisch.
    • Die mittlere Kerze schließt über dem Tief der ältesten Kerze.
    • Die letzte Kerze vor dem Signal (SignalBar + 1) ist bullisch und schließt über der Eröffnung der mittleren Kerze.
  • Bärische Umkehr (potentielles Short):
    • Spiegelbild-Logik des bullischen Falls.

Ein Volumenfilter spiegelt den ursprünglichen Indikator. Der Filter wird übersprungen, wenn MaxBarSize (in Preisschritten) vom Bereich der ältesten Kerze überschritten wird oder wenn VolumeFilter auf None gesetzt ist. Andernfalls muss die Umkehr älteres Volumen < mittleres Volumen ODER aktuelles Volumen > mittleres Volumen ODER aktuelles Volumen > ältestes Volumen erfüllen. Tick- und Real-Volumen werden auf das aggregierte Kerzenvolumen abgebildet, da StockSharp die beiden im High-Level-Kerzenstrom nicht unterscheidet.

Handelsverwaltung

  • Wenn AllowSellExit aktiviert ist, deckt ein bullisches Muster sofort jede Short-Position ab, bevor ein Long-Einstieg in Betracht gezogen wird. AllowBuyExit verhält sich bei Longs bei bärischen Mustern gleich.
  • Neue Positionen werden nur geöffnet, wenn die aktuelle Position flach ist und das entsprechende Allow*Entry-Flag wahr ist. Die Ordergröße verwendet die Standard-Volumeneinstellungen der Strategie.
  • Stop-Loss- und Take-Profit-Distanzen (StopLossPips, TakeProfitPips) werden in Preisschritten ausgedrückt und bei jeder fertiggestellten Kerze überwacht.
  • Der zuletzt verarbeitete bullische/bärische Signalzeitpunkt wird zwischengespeichert, um doppelte Aktionen zu vermeiden, während eine Kerze weiterhin Ticks auslöst.

Parameter

Name Standard Beschreibung
CandleType 4-Stunden-Zeitrahmen Von der Strategie verarbeitete Kerzenserie.
SignalBar 1 Wie viele Balken zurück das Signal ausgewertet wird. Muss ≥ 0 sein.
MaxBarSize 300 Wenn der älteste Balkenbereich (konvertiert mit PriceStep) diesen Wert überschreitet, wird der Volumenfilter übersprungen. Auf 0 setzen, um immer zu überspringen.
VolumeFilter Tick Volumenmodus (Tick, Real oder None). Sowohl Tick als auch Real verwenden TotalVolume aus Kerzen.
AllowBuyEntry true Long-Einstiege bei bullischen Mustern aktivieren.
AllowSellEntry true Short-Einstiege bei bärischen Mustern aktivieren.
AllowBuyExit true Schließen von Long-Positionen bei bärischen Mustern erlauben.
AllowSellExit true Schließen von Short-Positionen bei bullischen Mustern erlauben.
StopLossPips 1000 Stop-Loss-Distanz in Preisschritten (0 deaktiviert).
TakeProfitPips 2000 Take-Profit-Distanz in Preisschritten (0 deaktiviert).

Konvertierungshinweise

  • Geldverwaltungsroutinen aus der ursprünglichen MQL5-Include-Datei wurden durch StockSharp's BuyMarket/SellMarket-Aufrufe ersetzt. Die Positionsgröße folgt daher dem Standard-Volumen der Engine.
  • Das Signaltiming spiegelt den Expertenberater wider, indem die Kerze beim SignalBar-Versatz ausgewertet und der vorherige Signalzeitstempel gehalten wird.
  • E-Mail-, Push- und Soundbenachrichtigungen vom MQL-Indikator werden absichtlich weggelassen.
  • Volumenmodi werden beibehalten, aber beide werden auf das aggregierte Kerzenvolumen abgebildet, da separate Tick- und Real-Volumen in der High-Level-API nicht verfügbar sind.
  • Alle Kommentare wurden gemäß den Projektrichtlinien auf Englisch umgeschrieben.

Diese Implementierung bleibt dem ursprünglichen Verhalten treu und hält sich dabei an StockSharp's High-Level-Abonnementmodell.

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