Auf GitHub ansehen

Strategie Fractals bei Schlusspreisen

Übersicht

Diese Strategie ist ein StockSharp-Port des MetaTrader 5-Expertenberaters "Fractals at Close prices" von Vladimir Karputov. Sie analysiert fünf aufeinanderfolgende Schlusspreise, um Fraktale im Stil von Bill Williams zu erkennen, die streng auf Schlusskursen statt auf Hochs oder Tiefs aufgebaut werden. Die beiden jüngsten bullischen und bärischen Fraktale werden verglichen, um den aktiven Trend zu bestimmen. Wenn das neueste bullische Fraktal über dem vorherigen erscheint, eröffnet die Strategie eine Long-Position. Wenn das neueste bärische Fraktal unter dem vorherigen entsteht, wird eine Short-Position eröffnet. Entgegengesetzte Positionen werden immer geschlossen, bevor ein neuer Trade eingegangen wird, sodass die Strategie jederzeit höchstens in einer Richtung aktiv ist.

Trades sind nur zwischen der konfigurierbaren Start- und Endstunde erlaubt. Wenn die aktuelle Stunde außerhalb dieses Fensters liegt, werden alle offenen Positionen sofort geschlossen, was das Verhalten des ursprünglichen EA repliziert. Der Zeitfilter unterstützt Intraday-Fenster (Start < Ende), nächtliche Sitzungen, die Mitternacht überschreiten (Start > Ende), und ganztägigen Handel (Start == Ende).

Indikatorlogik

  • Jede abgeschlossene Kerze wird einer rollierenden Fünf-Element-Warteschlange von Schlusspreisen hinzugefügt.
  • Sobald fünf Werte verfügbar sind, wird der mittlere Schluss (zwei Kerzen zurück) bewertet:
    • Ein bullisches Fraktal wird registriert, wenn der mittlere Schluss streng größer als die beiden älteren Schlüsse und größer oder gleich den beiden neueren Schlüssen ist.
    • Ein bärisches Fraktal wird registriert, wenn der mittlere Schluss streng kleiner als die beiden älteren Schlüsse und kleiner oder gleich den beiden neueren Schlüssen ist.
  • Die neuesten und vorherigen bullischen Fraktale sowie die neuesten und vorherigen bärischen Fraktale werden für den späteren Vergleich gespeichert.
  • Ein bullischer Trend wird erkannt, wenn das neueste bullische Fraktal höher als das vorherige ist. Ein bärischer Trend wird erkannt, wenn das neueste bärische Fraktal niedriger als das vorherige ist.

Handelsregeln

  1. Long-Einträge
    • Alle aktiven Short-Positionen zum Marktpreis schließen.
    • Wenn keine Long-Position offen ist, OrderVolume zum Marktpreis am Schluss kaufen, der die bullische Fraktal-Sequenz bestätigt hat.
  2. Short-Einträge
    • Alle aktiven Long-Positionen zum Marktpreis schließen.
    • Wenn keine Short-Position offen ist, OrderVolume zum Marktpreis verkaufen, wenn eine bärische Fraktal-Sequenz bestätigt wird.
  3. Sitzungssteuerung
    • Vor der Anwendung von Signalen überprüft die Strategie, ob candle.OpenTime.Hour innerhalb des Handelsfensters liegt. Falls nicht, wird CloseAllPositions aufgerufen und die Bar ignoriert.

Risikomanagement

  • Stop-Loss- und Take-Profit-Abstände werden in Pips ausgedrückt. Die Implementierung reproduziert den MT5-Ansatz: Der Symbolpunkt wird mit zehn multipliziert, wenn das Instrument 3 oder 5 Dezimalstellen hat. Der resultierende Pip-Wert wird dann mit den konfigurierten Abständen multipliziert.
  • Beim Eingehen einer Position werden die anfänglichen Stop-Loss- und Take-Profit-Levels intern gespeichert. Da StockSharp MT5-Schutzaufträge nicht automatisch verwaltet, überwacht die Strategie abgeschlossene Kerzen und schließt zum Marktpreis, wenn deren Preisspanne das gespeicherte Level berührt.
  • Trailing Stops folgen den ursprünglichen EA-Regeln. Ein neuer Stop wird als close ± TrailingStop berechnet, sobald der Gewinn TrailingStop + TrailingStep übersteigt. Der Trailing Stop wird nur vorgerückt, wenn die Bewegung vom vorherigen Stop mindestens TrailingStep beträgt.
  • Wenn die Handelszeiten enden, werden alle Positionen unabhängig vom Trailing-Status geschlossen. Dies repliziert das EA-Verhalten, das CloseAllPositions außerhalb der erlaubten Sitzung aufruft.

Parameter

Name Beschreibung Standard
OrderVolume Volumen für jeden Marktauftrag. 0.1
StartHour Stunde (0-23), ab der der Handel aktiv wird. Wenn gleich EndHour, läuft die Strategie den ganzen Tag. 10
EndHour Stunde (0-23), ab der keine neuen Signale mehr akzeptiert werden. 22
StopLossPips Stop-Loss-Abstand in Pips. 0 deaktiviert den Stop. 30
TakeProfitPips Take-Profit-Abstand in Pips. 0 deaktiviert den Take. 50
TrailingStopPips Basis-Trailing-Stop-Abstand in Pips. 0 deaktiviert das Trailing. 15
TrailingStepPips Zusätzlicher Gewinn (in Pips), der erforderlich ist, bevor der Trailing Stop vorgerückt wird. 5
CandleType Kerzen-Datentyp, der von der Strategie abonniert wird. Standard ist 1-Stunden-Zeitrahmen-Kerzen. 1 hour TimeFrame

Implementierungshinweise

  • Die Strategie verwendet SubscribeCandles mit der High-Level-API und registriert keine Indikatoren manuell, gemäß den Projektrichtlinien.
  • Schutzzausgänge (Stop, Take-Profit, Trailing Stop) werden durch das Senden von Marktaufträgen nach Beendigung einer Kerze ausgeführt, da StockSharp MT5-Schutzaufträge nicht automatisch verwaltet.
  • Sitzungsfilterung, Fraktal-Erkennung und Trailing-Logik folgen streng der EA-Struktur, einschließlich des Schließens aller Positionen, wenn der Stundenfilter nicht erfüllt ist.
  • Die Pip-Skalierungslogik spiegelt die MT5-Implementierung wider, indem der Symbolpunkt bei 3- oder 5-dezimalen Instrumenten mit zehn multipliziert wird, um äquivalente Preisabstände sicherzustellen.

Verwendungstipps

  1. Strategie einem Symbol zuordnen und OrderVolume auf die bevorzugte Lotgröße setzen.
  2. Einen Kerzentyp wählen, der dem in MetaTrader 5 verwendeten Zeitrahmen entspricht (der ursprüngliche EA funktioniert auf jedem Zeitrahmen).
  3. Das Handelsfenster an die Broker-Sitzung oder die gewünschten Stunden anpassen.
  4. Die pip-basierten Abstände entsprechend der Instrument-Volatilität einstellen. Größere TrailingStepPips reduzieren die Trailing-Häufigkeit, während kleinere Werte den Stop näher am Preis halten.
  5. Logs auf Ein- und Ausstiege überwachen; die Strategie zeichnet Trades im optionalen Diagrammbereich zur schnellen visuellen Überprüfung.
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>
/// Strategy converted from the MT5 "Fractals at Close prices" expert advisor.
/// Detects bullish and bearish fractal sequences built on close prices and trades trend reversals.
/// Includes configurable trading hours and manual risk management with trailing stops.
/// </summary>
public class FractalsAtClosePricesStrategy : Strategy
{
	private readonly StrategyParam<decimal> _orderVolume;
	private readonly StrategyParam<int> _startHour;
	private readonly StrategyParam<int> _endHour;
	private readonly StrategyParam<int> _stopLossPips;
	private readonly StrategyParam<int> _takeProfitPips;
	private readonly StrategyParam<int> _trailingStopPips;
	private readonly StrategyParam<int> _trailingStepPips;
	private readonly StrategyParam<DataType> _candleType;

	private readonly List<decimal> _closeWindow = new(6);

	private decimal? _lastUpperFractal;
	private decimal? _previousUpperFractal;
	private decimal? _lastLowerFractal;
	private decimal? _previousLowerFractal;

	private decimal _pipValue;
	private decimal _stopLossDistance;
	private decimal _takeProfitDistance;
	private decimal _trailingStopDistance;
	private decimal _trailingStepDistance;

	private decimal? _entryPrice;
	private decimal? _longStop;
	private decimal? _longTake;
	private decimal? _shortStop;
	private decimal? _shortTake;

	/// <summary>
	/// Trading volume used for every market order.
	/// </summary>
	public decimal OrderVolume
	{
		get => _orderVolume.Value;
		set => _orderVolume.Value = value;
	}

	/// <summary>
	/// Hour when the strategy can start opening positions.
	/// </summary>
	public int StartHour
	{
		get => _startHour.Value;
		set => _startHour.Value = value;
	}

	/// <summary>
	/// Hour when the strategy stops opening positions.
	/// </summary>
	public int EndHour
	{
		get => _endHour.Value;
		set => _endHour.Value = value;
	}

	/// <summary>
	/// Stop-loss size expressed in pips.
	/// </summary>
	public int StopLossPips
	{
		get => _stopLossPips.Value;
		set => _stopLossPips.Value = value;
	}

	/// <summary>
	/// Take-profit size expressed in pips.
	/// </summary>
	public int TakeProfitPips
	{
		get => _takeProfitPips.Value;
		set => _takeProfitPips.Value = value;
	}

	/// <summary>
	/// Trailing stop distance expressed in pips.
	/// </summary>
	public int TrailingStopPips
	{
		get => _trailingStopPips.Value;
		set => _trailingStopPips.Value = value;
	}

	/// <summary>
	/// Minimum price improvement required before moving the trailing stop.
	/// </summary>
	public int TrailingStepPips
	{
		get => _trailingStepPips.Value;
		set => _trailingStepPips.Value = value;
	}

	/// <summary>
	/// Candle type used by the strategy.
	/// </summary>
	public DataType CandleType
	{
		get => _candleType.Value;
		set => _candleType.Value = value;
	}

	/// <summary>
	/// Initializes <see cref="FractalsAtClosePricesStrategy"/> parameters.
	/// </summary>
	public FractalsAtClosePricesStrategy()
	{
		_orderVolume = Param(nameof(OrderVolume), 0.1m)
		.SetGreaterThanZero()
		.SetDisplay("Order Volume", "Volume used for entries", "General")
		;

		_startHour = Param(nameof(StartHour), 0)
		.SetRange(0, 23)
		.SetDisplay("Start Hour", "Hour when trading can start (0-23)", "Trading Hours");

		_endHour = Param(nameof(EndHour), 0)
		.SetRange(0, 23)
		.SetDisplay("End Hour", "Hour when trading stops (0-23)", "Trading Hours");

		_stopLossPips = Param(nameof(StopLossPips), 200)
		.SetRange(0, 1000)
		.SetDisplay("Stop Loss (pips)", "Stop-loss distance in pips", "Risk Management")
		;

		_takeProfitPips = Param(nameof(TakeProfitPips), 400)
		.SetRange(0, 1000)
		.SetDisplay("Take Profit (pips)", "Take-profit distance in pips", "Risk Management")
		;

		_trailingStopPips = Param(nameof(TrailingStopPips), 15)
		.SetRange(0, 1000)
		.SetDisplay("Trailing Stop (pips)", "Base distance for the trailing stop", "Risk Management")
		;

		_trailingStepPips = Param(nameof(TrailingStepPips), 5)
		.SetRange(0, 1000)
		.SetDisplay("Trailing Step (pips)", "Additional move required before trailing", "Risk Management")
		;

		_candleType = Param(nameof(CandleType), TimeSpan.FromHours(4).TimeFrame())
		.SetDisplay("Candle Type", "Type of candles processed by the strategy", "General");
	}

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

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

		_closeWindow.Clear();
		_lastUpperFractal = null;
		_previousUpperFractal = null;
		_lastLowerFractal = null;
		_previousLowerFractal = null;

		_pipValue = 0m;
		_stopLossDistance = 0m;
		_takeProfitDistance = 0m;
		_trailingStopDistance = 0m;
		_trailingStepDistance = 0m;

		_entryPrice = null;
		ResetRiskLevels();
	}

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

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

		_pipValue = priceStep;
		if (decimals == 3 || decimals == 5)
		{
			// MT5 version multiplies point value by 10 when the symbol uses 3 or 5 decimals.
			_pipValue *= 10m;
		}

		_stopLossDistance = StopLossPips == 0 ? 0m : StopLossPips * _pipValue;
		_takeProfitDistance = TakeProfitPips == 0 ? 0m : TakeProfitPips * _pipValue;
		_trailingStopDistance = TrailingStopPips == 0 ? 0m : TrailingStopPips * _pipValue;
		_trailingStepDistance = TrailingStepPips == 0 ? 0m : TrailingStepPips * _pipValue;

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

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

	private void ProcessCandle(ICandleMessage candle)
	{
		if (candle.State != CandleStates.Finished)
		{
			return;
		}

		UpdateFractals(candle);

		if (!IsWithinTradingHours(candle.OpenTime))
		{
			CloseAllPositions();
			return;
		}

		ApplyRiskManagement(candle);

		// no bound indicators, skip IsFormedAndOnlineAndAllowTrading()

		ExecuteEntries(candle);
	}

	private void UpdateFractals(ICandleMessage candle)
	{
		// Maintain a rolling window of the five most recent closes.
		_closeWindow.Add(candle.ClosePrice);
		while (_closeWindow.Count > 5)
			_closeWindow.RemoveAt(0);

		if (_closeWindow.Count < 5)
		{
			return;
		}

		var window = _closeWindow;
		var center = window[2];

		var isUpper = center > window[0]
		&& center > window[1]
		&& center >= window[3]
		&& center >= window[4];

		if (isUpper)
		{
			_previousUpperFractal = _lastUpperFractal;
			_lastUpperFractal = center;
		}

		var isLower = center < window[0]
		&& center < window[1]
		&& center <= window[3]
		&& center <= window[4];

		if (isLower)
		{
			_previousLowerFractal = _lastLowerFractal;
			_lastLowerFractal = center;
		}
	}

	private bool IsWithinTradingHours(DateTimeOffset time)
	{
		var hour = time.Hour;

		if (StartHour == EndHour)
		{
			// Trade the entire day when start and end hours are equal.
			return true;
		}

		if (StartHour < EndHour)
		{
			return hour >= StartHour && hour < EndHour;
		}

		return hour >= StartHour || hour < EndHour;
	}

	private void ApplyRiskManagement(ICandleMessage candle)
	{
		if (Position > 0)
		{
			if (_longStop is decimal stop && candle.LowPrice <= stop)
			{
				// Close the long position if the stop-loss level is breached.
				SellMarket(Position);
				ResetRiskLevels();
				return;
			}

			if (_longTake is decimal take && candle.HighPrice >= take)
			{
				// Close the long position when the take-profit level is hit.
				SellMarket(Position);
				ResetRiskLevels();
				return;
			}

			UpdateLongTrailingStop(candle);
		}
		else if (Position < 0)
		{
			if (_shortStop is decimal stop && candle.HighPrice >= stop)
			{
				// Cover the short position if the stop-loss level is breached.
				BuyMarket(Math.Abs(Position));
				ResetRiskLevels();
				return;
			}

			if (_shortTake is decimal take && candle.LowPrice <= take)
			{
				// Cover the short position when the take-profit level is hit.
				BuyMarket(Math.Abs(Position));
				ResetRiskLevels();
				return;
			}

			UpdateShortTrailingStop(candle);
		}
	}

	private void UpdateLongTrailingStop(ICandleMessage candle)
	{
		if (_trailingStopDistance <= 0m || _entryPrice is not decimal entry)
		{
			return;
		}

		var profitDistance = candle.ClosePrice - entry;
		if (profitDistance <= _trailingStopDistance + _trailingStepDistance)
		{
			return;
		}

		var targetStop = candle.ClosePrice - _trailingStopDistance;
		if (_longStop is decimal currentStop && currentStop >= candle.ClosePrice - (_trailingStopDistance + _trailingStepDistance))
		{
			// Skip updates until price improved by the trailing step.
			return;
		}

		_longStop = targetStop;
	}

	private void UpdateShortTrailingStop(ICandleMessage candle)
	{
		if (_trailingStopDistance <= 0m || _entryPrice is not decimal entry)
		{
			return;
		}

		var profitDistance = entry - candle.ClosePrice;
		if (profitDistance <= _trailingStopDistance + _trailingStepDistance)
		{
			return;
		}

		var targetStop = candle.ClosePrice + _trailingStopDistance;
		if (_shortStop is decimal currentStop && currentStop <= candle.ClosePrice + (_trailingStopDistance + _trailingStepDistance))
		{
			// Skip updates until price improved by the trailing step.
			return;
		}

		_shortStop = targetStop;
	}

	private void ExecuteEntries(ICandleMessage candle)
	{
		// Only trade when flat to avoid too frequent reversals.
		if (Position != 0)
			return;

		var bullishTrend = _lastLowerFractal is decimal lastLow
		&& _previousLowerFractal is decimal prevLow
		&& prevLow < lastLow;

		if (bullishTrend && OrderVolume > 0m)
		{
			BuyMarket(OrderVolume);
			_entryPrice = candle.ClosePrice;
			_longStop = _stopLossDistance > 0m ? candle.ClosePrice - _stopLossDistance : null;
			_longTake = _takeProfitDistance > 0m ? candle.ClosePrice + _takeProfitDistance : null;
			_shortStop = null;
			_shortTake = null;
			return;
		}

		var bearishTrend = _lastUpperFractal is decimal lastUp
		&& _previousUpperFractal is decimal prevUp
		&& prevUp > lastUp;

		if (bearishTrend && OrderVolume > 0m)
		{
			SellMarket(OrderVolume);
			_entryPrice = candle.ClosePrice;
			_shortStop = _stopLossDistance > 0m ? candle.ClosePrice + _stopLossDistance : null;
			_shortTake = _takeProfitDistance > 0m ? candle.ClosePrice - _takeProfitDistance : null;
			_longStop = null;
			_longTake = null;
		}
	}

	private void CloseAllPositions()
	{
		if (Position > 0)
		{
			SellMarket(Position);
		}
		else if (Position < 0)
		{
			BuyMarket(Math.Abs(Position));
		}

		ResetRiskLevels();
	}

	private void CloseLongPosition()
	{
		if (Position > 0)
		{
			SellMarket(Position);
			ResetRiskLevels();
		}
	}

	private void CloseShortPosition()
	{
		if (Position < 0)
		{
			BuyMarket(Math.Abs(Position));
			ResetRiskLevels();
		}
	}

	private void ResetRiskLevels()
	{
		_longStop = null;
		_longTake = null;
		_shortStop = null;
		_shortTake = null;
		_entryPrice = null;
	}
}