Auf GitHub ansehen

Prozentuale Crossover-Strategie

Die Strategie repliziert das Verhalten des ursprünglichen MetaTrader-Experten Exp_PercentageCrossover. Sie handelt in Richtung des Percentage Crossover-Indikators, der eine nachziehende Preislinie zeichnet, die sich nur innerhalb eines festen prozentualen Bandes um den aktuellen Schlusskurs bewegen kann. Die Neigung dieser Linie definiert den Marktzustand und löst Trades aus.

Konzept

  1. Bei jeder abgeschlossenen Kerze behält der Indikator den vorherigen Linienwert bei.
  2. Eine bullische Aktualisierung erfolgt, wenn der Schlusskurs die nachziehende Linie um mindestens percent Prozent des Preises über ihren vorherigen Wert schiebt.
  3. Eine bärische Aktualisierung erfolgt, wenn der Schlusskurs die nachziehende Linie um denselben Prozentsatz unter ihren vorherigen Wert zieht.
  4. Bleibt der Schlusskurs innerhalb des Bandes, bleibt die Linie flach und behält ihre letzte Farbe.

Die Farbe der Linie wird genauso interpretiert wie in MetaTrader:

  • Farbindex 0 (blau/violett) – die Linie steigt (bullischer Kontext).
  • Farbindex 1 (orange) – die Linie fällt (bärischer Kontext).

Handelsregeln

Long-Einstiege

  • Nur aktiviert, wenn BuyPosOpen = true.
  • Die durch SignalBar ausgewählte Kerze wird ausgewertet (1 bedeutet die letzte geschlossene Kerze).
  • Eine Long-Position wird eröffnet, wenn diese Kerze von Farbe 1 auf Farbe 0 wechselt.

Short-Einstiege

  • Nur aktiviert, wenn SellPosOpen = true.
  • Dieselbe SignalBar-Kerze wird ausgewertet.
  • Eine Short-Position wird eröffnet, wenn die Kerze von Farbe 0 auf Farbe 1 wechselt.

Positionsverwaltung

  • Wenn BuyPosClose = true, wird jede offene Long-Position geschlossen, sobald die aktuelle Kerze (nach Anwendung des SignalBar-Versatzes) Farbe 1 hat.
  • Wenn SellPosClose = true, wird jede offene Short-Position geschlossen, sobald diese Kerze Farbe 0 hat.
  • Wenn UseTimeFilter = true und die aktuelle Zeit außerhalb des konfigurierten Handelsfensters liegt, verlässt die Strategie sofort die aktive Position und ignoriert neue Signale, bis der Markt das Fenster wieder betritt.
  • Aufträge werden mit BuyMarket() und SellMarket() gesendet. Die tatsächliche Menge stammt aus der Volume-Eigenschaft der Strategie.

Parameter

Parameter Beschreibung Standard
Percent Prozentband für die nachziehende Linie. Höhere Werte lassen die Linie langsamer reagieren. 1
SignalBar Welche geschlossene Kerze analysiert wird (1 = letzte geschlossene). Muss positiv bleiben. 1
BuyPosOpen / SellPosOpen Long- bzw. Short-Einstiege aktivieren. true
BuyPosClose / SellPosClose Schließlogik für Long- bzw. Short-Positionen aktivieren. true
UseTimeFilter Das Handelsfenster aktivieren. true
StartHour / StartMinute Stunde und Minute, die das Handelsfenster öffnen, wenn der Filter aktiv ist. 0 / 0
EndHour / EndMinute Stunde und Minute, die das Handelsfenster schließen. 23 / 59
CandleType Zeitrahmen der Kerzen, die für den Indikator und die Signale verwendet werden. 4h

Hinweise

  • Der Zeitfilter folgt streng dem ursprünglichen Expert Advisor. Wenn die Startstunde größer als die Endstunde ist, erstellt die Logik ein Übernacht-Fenster, erfordert aber dennoch, dass die Minuten größer oder gleich StartMinute sind, bevor die Sitzung aktiv wird.
  • SignalBar wird nur bei abgeschlossenen Kerzen ausgewertet. Setzen Sie es auf 1, um die Standard-MetaTrader-Konfiguration zu spiegeln.
  • Die Strategie setzt keine Stop-Loss- oder Take-Profit-Niveaus. Die Risikosteuerung muss extern oder durch Anpassung des Prozentsatzes und des Handelsfensters erfolgen.
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 based on the Percentage Crossover indicator.
/// </summary>
public class PercentageCrossoverStrategy : Strategy
{
	private readonly StrategyParam<bool> _buyPosOpen;
	private readonly StrategyParam<bool> _sellPosOpen;
	private readonly StrategyParam<bool> _buyPosClose;
	private readonly StrategyParam<bool> _sellPosClose;
	private readonly StrategyParam<bool> _useTimeFilter;
	private readonly StrategyParam<int> _startHour;
	private readonly StrategyParam<int> _startMinute;
	private readonly StrategyParam<int> _endHour;
	private readonly StrategyParam<int> _endMinute;
	private readonly StrategyParam<decimal> _percent;
	private readonly StrategyParam<int> _signalBar;
	private readonly StrategyParam<DataType> _candleType;

	private readonly List<int> _colorHistory = new();

	private decimal? _previousMiddle;
	private int? _lastColor;

	public bool BuyPosOpen
	{
		get => _buyPosOpen.Value;
		set => _buyPosOpen.Value = value;
	}

	public bool SellPosOpen
	{
		get => _sellPosOpen.Value;
		set => _sellPosOpen.Value = value;
	}

	public bool BuyPosClose
	{
		get => _buyPosClose.Value;
		set => _buyPosClose.Value = value;
	}

	public bool SellPosClose
	{
		get => _sellPosClose.Value;
		set => _sellPosClose.Value = value;
	}

	public bool UseTimeFilter
	{
		get => _useTimeFilter.Value;
		set => _useTimeFilter.Value = value;
	}

	public int StartHour
	{
		get => _startHour.Value;
		set => _startHour.Value = value;
	}

	public int StartMinute
	{
		get => _startMinute.Value;
		set => _startMinute.Value = value;
	}

	public int EndHour
	{
		get => _endHour.Value;
		set => _endHour.Value = value;
	}

	public int EndMinute
	{
		get => _endMinute.Value;
		set => _endMinute.Value = value;
	}

	public decimal Percent
	{
		get => _percent.Value;
		set => _percent.Value = value;
	}

	public int SignalBar
	{
		get => _signalBar.Value;
		set => _signalBar.Value = value;
	}

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

	public PercentageCrossoverStrategy()
	{
		_buyPosOpen = Param(nameof(BuyPosOpen), true)
			.SetDisplay("Enable Buy Entries", "Allow opening long positions", "General");

		_sellPosOpen = Param(nameof(SellPosOpen), true)
			.SetDisplay("Enable Sell Entries", "Allow opening short positions", "General");

		_buyPosClose = Param(nameof(BuyPosClose), true)
			.SetDisplay("Enable Buy Exits", "Allow closing long positions", "General");

		_sellPosClose = Param(nameof(SellPosClose), true)
			.SetDisplay("Enable Sell Exits", "Allow closing short positions", "General");

		_useTimeFilter = Param(nameof(UseTimeFilter), true)
			.SetDisplay("Use Time Filter", "Restrict trading to specific hours", "Time Filter");

		_startHour = Param(nameof(StartHour), 0)
			.SetDisplay("Start Hour", "Trading window start hour", "Time Filter");

		_startMinute = Param(nameof(StartMinute), 0)
			.SetDisplay("Start Minute", "Trading window start minute", "Time Filter");

		_endHour = Param(nameof(EndHour), 23)
			.SetDisplay("End Hour", "Trading window end hour", "Time Filter");

		_endMinute = Param(nameof(EndMinute), 59)
			.SetDisplay("End Minute", "Trading window end minute", "Time Filter");

		_percent = Param(nameof(Percent), 1m)
			.SetGreaterThanZero()
			.SetDisplay("Percent", "Percentage offset for the indicator", "Indicator");

		_signalBar = Param(nameof(SignalBar), 1)
			.SetGreaterThanZero()
			.SetDisplay("Signal Bar", "Closed bars to look back for the signal", "Indicator");

		_candleType = Param(nameof(CandleType), TimeSpan.FromHours(4).TimeFrame())
			.SetDisplay("Candle Type", "Time frame for signal candles", "Data");
	}

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

	protected override void OnReseted()
	{
		base.OnReseted();
		_colorHistory.Clear();
		_previousMiddle = null;
		_lastColor = null;
	}

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

		_colorHistory.Clear();
		_previousMiddle = null;
		_lastColor = null;

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

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

		var close = candle.ClosePrice;
		var percentFactor = Percent / 100m;

		if (_previousMiddle is null)
		{
			_previousMiddle = close;
			_lastColor = 0;
			_colorHistory.Clear();
			_colorHistory.Add(0);
			return;
		}

		var previousMiddle = _previousMiddle.Value;
		var lowerBoundary = close * (1 - percentFactor);
		var upperBoundary = close * (1 + percentFactor);

		var middle = previousMiddle;

		if (lowerBoundary > previousMiddle)
			middle = lowerBoundary;
		else if (upperBoundary < previousMiddle)
			middle = upperBoundary;

		var color = _lastColor ?? 0;

		if (middle > previousMiddle)
			color = 0;
		else if (middle < previousMiddle)
			color = 1;

		_previousMiddle = middle;
		_lastColor = color;

		_colorHistory.Add(color);
		var maxSize = Math.Max(SignalBar + 2, 4);
		while (_colorHistory.Count > maxSize)
		{
			try { _colorHistory.RemoveAt(0); }
			catch { break; }
		}

		var currentIndex = _colorHistory.Count - SignalBar;
		if (currentIndex <= 0)
			return;

		var previousIndex = currentIndex - 1;
		if (previousIndex < 0)
			return;

		var currentColor = _colorHistory[currentIndex];
		var previousColor = _colorHistory[previousIndex];

		var buyOpen = BuyPosOpen && currentColor == 0 && previousColor == 1;
		var sellOpen = SellPosOpen && currentColor == 1 && previousColor == 0;
		var buyClose = BuyPosClose && currentColor == 1;
		var sellClose = SellPosClose && currentColor == 0;

		var inTradingWindow = !UseTimeFilter || IsTradingTime(candle.CloseTime);

		if (UseTimeFilter && !inTradingWindow)
		{
			if (Position > 0)
				SellMarket();
			else if (Position < 0)
				BuyMarket();

			return;
		}

		if (buyClose && Position > 0)
			SellMarket();

		if (sellClose && Position < 0)
			BuyMarket();

		if (!inTradingWindow)
			return;

		if (buyOpen && Position <= 0)
			BuyMarket();
		else if (sellOpen && Position >= 0)
			SellMarket();
	}

	private bool IsTradingTime(DateTimeOffset time)
	{
		var hour = time.Hour;
		var minute = time.Minute;

		if (StartHour < EndHour)
		{
			if (hour == StartHour && minute >= StartMinute)
				return true;

			if (hour > StartHour && hour < EndHour)
				return true;

			if (hour > StartHour && hour == EndHour && minute < EndMinute)
				return true;

			return false;
		}

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

		if (hour >= StartHour && minute >= StartMinute)
			return true;

		if (hour < EndHour)
			return true;

		if (hour == EndHour && minute < EndMinute)
			return true;

		return false;
	}
}