Auf GitHub ansehen

Nacht-Flat-Trade-Strategie

Die Nacht-Flat-Trade-Strategie reproduziert den klassischen MQL5-Expertenberater, der nach engen nächtlichen Ranges auf EURUSD H1-Kerzen sucht. Sie konzentriert sich auf die Stunde rund um den Handelstageswechsel und wartet darauf, dass der Preis zu den Rändern eines engen Konsolidierungskanals zurückkehrt und auf eine Ausbruchsfortsetzung setzt. Die StockSharp-Version bewahrt die ursprünglichen Ideen und stützt sich auf High-Level-Kerzenabonnements, Indikatorbindungen und Parameterobjekte für bessere Konfigurierbarkeit.

Überblick

  • Markt und Zeitrahmen: Für EURUSD auf dem H1-Zeitrahmen konzipiert, aber jedes Instrument mit einem klar definierten Preisschritt kann verwendet werden.
  • Sitzungsfenster: Einstiege sind nur während eines Zwei-Stunden-Fensters erlaubt, das um die konfigurierte OpenHour beginnt und bei OpenHour + 1 endet (Börsenzeit).
  • Range-Filter: Der Hoch-Tief-Bereich der letzten drei abgeschlossenen Kerzen muss zwischen DiffMinPips und DiffMaxPips bleiben (in Preiseinheiten umgerechnet).
  • Richtungsneigung: Nur Long oder nur Short je nachdem, wo der letzte Schluss innerhalb der qualifizierenden Range liegt.

Handelslogik

  1. Range-Grenzen berechnen

    • Die Strategie bindet sich an die eingebauten Highest- und Lowest-Indikatoren (Länge = 3), um das höchste Hoch und tiefste Tief über die letzten drei Kerzen zu erhalten.
    • Der Abstand zwischen diesen Grenzen ist die Arbeitsrange, die für alle nachfolgenden Prüfungen verwendet wird.
  2. Einstiegsbedingungen

    • Long-Setup: Während der aktiven Sitzung, wenn der Schlusskurs über dem Range-Tief liegt, aber noch innerhalb des unteren Viertels (lowest + range/4), öffnet die Strategie eine Long-Position mit einem anfänglichen Schutz-Stop bei lowest - range/3.
    • Short-Setup: Symmetrisch dazu, wenn der Schluss unter dem Range-Hoch liegt, aber noch im oberen Viertel (highest - range/4), wird eine Short-Position mit einem Stop bei highest + range/3 eröffnet.
  3. Ausstiegsverwaltung

    • Stop-Loss: Stops werden intern simuliert und lösen einen Marktausstieg aus, wenn die nächste Kerze den gespeicherten Schwellenwert verletzt.
    • Take-Profit: Wenn TakeProfitPips > 0, wird ein zusätzliches festes Take-Profit-Niveau (in Pips) relativ zum Einstiegspreis erstellt.
    • Trailing-Stop: Wenn sowohl TrailingStopPips als auch TrailingStepPips positiv sind, wird der Stop erst angepasst, nachdem der Preis um TrailingStop + TrailingStep Pips zugunsten des Trades vorgerückt ist. Nachfolgende Anpassungen erfordern einen zusätzlichen Fortschritt von TrailingStepPips, um das ursprüngliche stufenweise Trailing-Verhalten widerzuspiegeln.
  4. Wiedereinstiegskontrolle

    • Der Algorithmus wartet immer, bis die aktuelle Position vollständig geschlossen ist, bevor er nach einem neuen Signal sucht, um das System zwischen Trades flat zu halten, wie beim Referenz-Expertenberater.

Parameter

Parameter Beschreibung Standard
CandleType Zu abonnierende Kerzenserie (Standard H1). 1-Stunden-Kerzen
TakeProfitPips Optionaler Take-Profit-Abstand in Pips. 50
TrailingStopPips Abstand zwischen Preis und Trailing-Stop in Pips (0 deaktiviert Trailing). 15
TrailingStepPips Zusätzliche Pips vor jeder Trailing-Stop-Aktualisierung. 5
DiffMinPips Minimal zulässige Drei-Kerzen-Range (Pips). 18
DiffMaxPips Maximal zulässige Drei-Kerzen-Range (Pips). 28
OpenHour Sitzungsstartsstunde in Börsenzeit (Einstiege bis OpenHour + 1 erlaubt). 0

Indikatoren

  • Highest(Length = 3) zum Überwachen der letzten Range-Obergrenze.
  • Lowest(Length = 3) zum Überwachen der letzten Range-Untergrenze.

Implementierungshinweise

  • Die Pip-Konvertierung passt sich automatisch an Instrumente mit 3 oder 5 Dezimalstellen an, indem der gemeldete Preisschritt mit 10 multipliziert wird, genau wie die ursprüngliche MQ5-Implementierung.
  • Da StockSharp in diesem Beispiel auf abgeschlossenen Kerzen operiert, werden Intra-Kerzen-Einstiegsbedingungen mit dem Schlusskurs angenähert. Dies hält die Logik deterministisch und bleibt dem Zweck des Quellcodes treu.
  • Alle Risikoparameter sind über StrategyParam<T>-Objekte exponiert, wodurch sie in der UI sichtbar und bereit für Optimierung oder Batch-Experimente sind.
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>
/// Night session flat trading strategy that enters near range extremes.
/// </summary>
public class NightFlatTradeStrategy : Strategy
{
	private readonly StrategyParam<int> _rangeLength;

	private readonly StrategyParam<DataType> _candleType;
	private readonly StrategyParam<decimal> _takeProfitPips;
	private readonly StrategyParam<decimal> _trailingStopPips;
	private readonly StrategyParam<decimal> _trailingStepPips;
	private readonly StrategyParam<decimal> _diffMinPips;
	private readonly StrategyParam<decimal> _diffMaxPips;
	private readonly StrategyParam<int> _openHour;

	private Highest _highest = null!;
	private Lowest _lowest = null!;

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

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

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

	public decimal TrailingStopPips
	{
		get => _trailingStopPips.Value;
		set => _trailingStopPips.Value = value;
	}

	public decimal TrailingStepPips
	{
		get => _trailingStepPips.Value;
		set => _trailingStepPips.Value = value;
	}

	public decimal DiffMinPips
	{
		get => _diffMinPips.Value;
		set => _diffMinPips.Value = value;
	}

	public decimal DiffMaxPips
	{
		get => _diffMaxPips.Value;
		set => _diffMaxPips.Value = value;
	}

	public int OpenHour
	{
		get => _openHour.Value;
		set => _openHour.Value = value;
	}

	/// <summary>
	/// Number of candles used to form the overnight range.
	/// </summary>
	public int RangeLength
	{
		get => _rangeLength.Value;
		set => _rangeLength.Value = value;
	}

	public NightFlatTradeStrategy()
	{
		_candleType = Param(nameof(CandleType), TimeSpan.FromMinutes(5).TimeFrame())
			.SetDisplay("Candle Type", "Type of candles used for the setup", "General");

		_takeProfitPips = Param(nameof(TakeProfitPips), 50m)
			.SetRange(0m, 500m)
			.SetDisplay("Take Profit (pips)", "Take profit distance in pips", "Risk");

		_trailingStopPips = Param(nameof(TrailingStopPips), 15m)
			.SetRange(0m, 200m)
			.SetDisplay("Trailing Stop (pips)", "Trailing stop distance in pips", "Risk");

		_trailingStepPips = Param(nameof(TrailingStepPips), 5m)
			.SetRange(0m, 200m)
			.SetDisplay("Trailing Step (pips)", "Extra advance required to shift the trailing stop", "Risk");

		_diffMinPips = Param(nameof(DiffMinPips), 18m)
			.SetGreaterThanZero()
			.SetDisplay("Min Range (pips)", "Minimum three-candle range in pips", "Setup");

		_diffMaxPips = Param(nameof(DiffMaxPips), 28m)
			.SetGreaterThanZero()
			.SetDisplay("Max Range (pips)", "Maximum three-candle range in pips", "Setup");

		_openHour = Param(nameof(OpenHour), 0)
			.SetRange(0, 23)
			.SetDisplay("Open Hour", "Hour (exchange time) when entries become active", "Schedule");

		_rangeLength = Param(nameof(RangeLength), 3)
			.SetGreaterThanZero()
			.SetDisplay("Range Length", "Number of candles composing the range", "Setup");
	}

	public override IEnumerable<(Security sec, DataType dt)> GetWorkingSecurities()
	{
		return [(Security, CandleType)];
	}

	protected override void OnReseted()
	{
		base.OnReseted();

		_highest = null!;
		_lowest = null!;
		_pipSize = 0m;
		_entryPrice = 0m;
		_stopPrice = null;
		_takeProfitPrice = null;
	}

	protected override void OnStarted2(DateTime time)
	{
		base.OnStarted2(time);

		_highest = new Highest { Length = RangeLength };
		_lowest = new Lowest { Length = RangeLength };

		var priceStep = Security?.PriceStep ?? 0m;
		var decimals = Security?.Decimals;

		if (priceStep <= 0m)
			priceStep = 0.0001m;

		_pipSize = priceStep;

		if (decimals.HasValue && (decimals.Value == 3 || decimals.Value == 5))
			_pipSize *= 10m;

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

		var area = CreateChartArea();
		if (area != null)
		{
			DrawCandles(area, subscription);
			DrawIndicator(area, _highest);
			DrawIndicator(area, _lowest);
			DrawOwnTrades(area);
		}
	}

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

		// Manage active trades before scanning for new setups.
		HandleExistingPosition(candle);

		if (Position != 0m)
			return;

		if (_highest == null || _lowest == null)
			return;

		if (!_highest.IsFormed || !_lowest.IsFormed)
			return;

		if (!IsFormedAndOnlineAndAllowTrading())
			return;

		var diff = highestValue - lowestValue;
		if (diff <= 0m)
			return;

		var quarter = diff / 4m;
		var closePrice = candle.ClosePrice;

		if (closePrice > lowestValue && closePrice <= lowestValue + quarter)
		{
			BuyMarket();
			_entryPrice = closePrice;
			_stopPrice = lowestValue - diff / 3m;
			_takeProfitPrice = TakeProfitPips > 0m ? closePrice + ToPrice(TakeProfitPips) : null;
			return;
		}

		if (closePrice < highestValue && closePrice >= highestValue - quarter)
		{
			SellMarket();
			_entryPrice = closePrice;
			_stopPrice = highestValue + diff / 3m;
			_takeProfitPrice = TakeProfitPips > 0m ? closePrice - ToPrice(TakeProfitPips) : null;
		}
	}

	private void HandleExistingPosition(ICandleMessage candle)
	{
		if (Position > 0m)
		{
			UpdateTrailingForLong(candle);

			if (_stopPrice.HasValue && candle.LowPrice <= _stopPrice.Value)
			{
				SellMarket(Math.Abs(Position));
				ResetTradeState();
				return;
			}

			if (_takeProfitPrice.HasValue && candle.HighPrice >= _takeProfitPrice.Value)
			{
				SellMarket(Math.Abs(Position));
				ResetTradeState();
			}
		}
		else if (Position < 0m)
		{
			UpdateTrailingForShort(candle);

			if (_stopPrice.HasValue && candle.HighPrice >= _stopPrice.Value)
			{
				BuyMarket(Math.Abs(Position));
				ResetTradeState();
				return;
			}

			if (_takeProfitPrice.HasValue && candle.LowPrice <= _takeProfitPrice.Value)
			{
				BuyMarket(Math.Abs(Position));
				ResetTradeState();
			}
		}
	}

	private void UpdateTrailingForLong(ICandleMessage candle)
	{
		if (TrailingStopPips <= 0m || TrailingStepPips <= 0m || _stopPrice == null)
			return;

		var trailingDistance = ToPrice(TrailingStopPips);
		var stepDistance = ToPrice(TrailingStepPips);

		var advance = candle.HighPrice - _entryPrice;
		if (advance < trailingDistance + stepDistance)
			return;

		var newStop = candle.HighPrice - trailingDistance;

		if (newStop <= _stopPrice.Value || newStop - _stopPrice.Value < stepDistance)
			return;

		// Raise the stop only after price travels an additional step distance.
		_stopPrice = newStop;
	}

	private void UpdateTrailingForShort(ICandleMessage candle)
	{
		if (TrailingStopPips <= 0m || TrailingStepPips <= 0m || _stopPrice == null)
			return;

		var trailingDistance = ToPrice(TrailingStopPips);
		var stepDistance = ToPrice(TrailingStepPips);

		var advance = _entryPrice - candle.LowPrice;
		if (advance < trailingDistance + stepDistance)
			return;

		var newStop = candle.LowPrice + trailingDistance;

		if (newStop >= _stopPrice.Value || _stopPrice.Value - newStop < stepDistance)
			return;

		// Lower the stop only after price moves the additional step distance in favor of the trade.
		_stopPrice = newStop;
	}

	private decimal ToPrice(decimal pips)
	{
		if (pips <= 0m)
			return 0m;

		var pip = _pipSize > 0m ? _pipSize : 0.0001m;
		return pips * pip;
	}

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