Auf GitHub ansehen

PriceChannel Signal v2 Strategie

Überblick

PriceChannel Signal v2 ist ein trendfolgendes Breakout-System, das auf einem modifizierten Donchian-Kanal basiert. Der ursprüngliche MQL5-Expertenberater achtet auf Übergänge im Kanaltrend, optionale Wiedereintrittsbedingungen, wenn der Preis wieder durch die Bänder drückt, und schützende Ausstiegsniveaus, die aus derselben Spanne abgeleitet werden. Der StockSharp-Port behält das ursprüngliche Verhalten bei: Er handelt jeweils eine einzelne Position, reagiert nur auf abgeschlossene Kerzen und kann auf ein Intraday-Fenster beschränkt werden.

Handelslogik

  1. Donchian Kanalhoch und -tief werden über die konfigurierten ChannelPeriod berechnet.
  2. Der Rohbereich wird um zwei Multiplikatoren verschoben:
    • Risikofaktor – komprimiert die Eintrittsbänder in Richtung des Kanalmedians.
    • Exit Level – bildet ein zweites Paar innerer Bänder, die Ausgänge auslösen.
  3. Ein Trendstatus wird beibehalten:
    • Wenn der Schlusskurs das obere Einstiegsband durchbricht, wird der Trend bullisch.
    • Wenn der Schlusskurs unter das untere Einstiegsband fällt, wird der Trend rückläufig.
    • Ansonsten wird der bisherige Trend beibehalten.
  4. Aus diesem Zustand generierte Signale:
    • Long-Einstieg – Trend wechselt von bärisch zu bullisch.
    • Kurzer Einstieg – Trend wechselt von bullisch zu bärisch.
    • Langer Wiedereinstieg – optional, der Preis schließt wieder über dem oberen Band, während der Trend bereits bullisch ist.
    • Kurzer Wiedereinstieg – optional, der Preis schließt wieder unter dem unteren Band, während der Trend bereits rückläufig ist.
    • Langer Ausstieg – optional, der Preis schließt unterhalb des bullischen Ausstiegsbandes, nachdem er beim vorherigen Balken darüber lag.
    • Short Exit – optional, der Preis schließt über dem rückläufigen Ausstiegsband, nachdem er beim vorherigen Balken darunter lag.
  5. Es ist nur eine Bestellung pro Balken und pro Richtung zulässig. Die Strategie weigert sich, eine neue Position zu eröffnen, wenn bereits eine andere aktiv ist.
  6. Wenn der Intraday-Zeitfilter aktiviert ist, werden alle oben genannten Signale außerhalb des konfigurierten Fensters ignoriert.

Parameter

Parameter Beschreibung
ChannelPeriod Donchian Lookback-Länge, die zur Berechnung des Preiskanals und der Ausstiegsbänder verwendet wird.
RiskFactor Verschiebung der Eintrittsbänder (0–10). Niedrigere Werte verbreitern die Bänder, höhere Werte verengen sie.
ExitLevel Verschiebung der Ausgangsbänder. Muss größer als RiskFactor sein, um innerhalb des Eingabebereichs zu bleiben.
UseReEntry Ermöglicht Wiedereinstiegsgeschäfte, wenn der Preis wieder durch das aktive Band fällt.
UseExitSignals Ermöglicht die Ausgangslogik basierend auf den inneren Schutzbändern.
CandleType Zeitrahmen, der zum Erstellen von Kerzen und zum Ausführen der Indikatoren verwendet wird.
UseTimeControl Schaltet das Intraday-Handelsfenster um.
StartHour / StartMinute Inklusive Beginn des Handelsfensters bei aktiver Zeitsteuerung.
EndHour / EndMinute Exklusives Ende des Handelsfensters bei aktiver Zeitsteuerung.

Ein- und Ausreiseregeln

  • Einstieg in eine Long-Position: Der Trend schlägt in Richtung Aufwärtstrend um oder es kommt zu einer Wiedereintrittsbedingung, die aktuelle Position ist flach und der Balken befindet sich innerhalb des zulässigen Zeitfensters.
  • Enter Short: Der Trend dreht sich zu einem Abwärtstrend um oder die Bedingung für einen Short-Wiedereintritt wird ausgelöst, die aktuelle Position ist flach und der Balken befindet sich innerhalb des zulässigen Zeitfensters.
  • Exit Long: UseExitSignals ist aktiviert und der Schlusskurs fällt unter das Ausstiegsband, nachdem er beim vorherigen Balken darüber lag.
  • Exit Short: UseExitSignals ist aktiviert und der Schlusskurs steigt über das Ausstiegsband, nachdem er beim vorherigen Balken darunter lag.

Zusätzliche Hinweise

  • Die Strategie arbeitet mit Marktaufträgen und baut keine Pyramidenpositionen auf.
  • Indikatorwerte werden nur bei fertigen Kerzen verarbeitet, um ein Neuzeichnen innerhalb des Balkens zu vermeiden.
  • Sofern nicht ausdrücklich angegeben, beträgt das Volumen standardmäßig 1 Vertrag.
  • Die Zeitsteuerung folgt dem ursprünglichen EA-Verhalten: Die Endzeit ist exklusiv und ein Umbruch über Mitternacht wird unterstützt.
using System;
using System.Collections.Generic;

using Ecng.Common;

using StockSharp.Algo.Strategies;
using StockSharp.BusinessEntities;
using StockSharp.Messages;

namespace StockSharp.Samples.Strategies;

/// <summary>
/// Price Channel Signal v2 strategy that reacts to Donchian channel breakouts.
/// </summary>
public class PriceChannelSignalV2Strategy : Strategy
{
	private readonly StrategyParam<int> _channelPeriod;
	private readonly StrategyParam<DataType> _candleType;

	private readonly Queue<decimal> _highHistory = new();
	private readonly Queue<decimal> _lowHistory = new();
	private int _previousTrend;
	private decimal? _previousClose;

	/// <summary>
	/// Channel lookback length.
	/// </summary>
	public int ChannelPeriod
	{
		get => _channelPeriod.Value;
		set => _channelPeriod.Value = value;
	}

	/// <summary>
	/// Candle type used for indicator calculations.
	/// </summary>
	public DataType CandleType
	{
		get => _candleType.Value;
		set => _candleType.Value = value;
	}

	/// <summary>
	/// Initialize a new instance of <see cref="PriceChannelSignalV2Strategy"/>.
	/// </summary>
	public PriceChannelSignalV2Strategy()
	{
		_channelPeriod = Param(nameof(ChannelPeriod), 20)
			.SetGreaterThanZero()
			.SetDisplay("Channel Period", "Donchian lookback used for Price Channel", "Price Channel");

		_candleType = Param(nameof(CandleType), TimeSpan.FromMinutes(60).TimeFrame())
			.SetDisplay("Candle Type", "Primary timeframe for Price Channel", "General");
	}

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

		_previousTrend = 0;
		_previousClose = null;
		_highHistory.Clear();
		_lowHistory.Clear();

		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;

		if (_highHistory.Count < ChannelPeriod)
		{
			EnqueueCandle(candle);
			return;
		}

		var highs = _highHistory.ToArray();
		var lows = _lowHistory.ToArray();
		var channelHigh = GetMax(highs);
		var channelLow = GetMin(lows);
		var range = channelHigh - channelLow;
		if (range <= 0m)
		{
			_previousClose = candle.ClosePrice;
			EnqueueCandle(candle);
			return;
		}

		var mid = (channelHigh + channelLow) / 2m;

		// Update trend state based on channel breakout
		var trend = _previousTrend;
		if (candle.ClosePrice > channelHigh + range * 0.05m)
			trend = 1;
		else if (candle.ClosePrice < channelLow - range * 0.05m)
			trend = -1;

		var volume = Volume;
		if (volume <= 0)
			volume = 1;

		// Trend reversal signals
		var changedPosition = false;

		if (trend > 0 && _previousTrend <= 0)
		{
			if (Position <= 0)
			{
				BuyMarket(Position < 0 ? Math.Abs(Position) + volume : volume);
				changedPosition = true;
			}
		}
		else if (trend < 0 && _previousTrend >= 0)
		{
			if (Position >= 0)
			{
				SellMarket(Position > 0 ? Math.Abs(Position) + volume : volume);
				changedPosition = true;
			}
		}

		// Exit on mid-line cross
		if (!changedPosition && Position > 0 && _previousClose is decimal pc1 && pc1 >= mid && candle.ClosePrice < mid)
		{
			SellMarket(Math.Abs(Position));
		}
		else if (!changedPosition && Position < 0 && _previousClose is decimal pc2 && pc2 <= mid && candle.ClosePrice > mid)
		{
			BuyMarket(Math.Abs(Position));
		}

		_previousTrend = trend;
		_previousClose = candle.ClosePrice;
		EnqueueCandle(candle);
	}

	private void EnqueueCandle(ICandleMessage candle)
	{
		_highHistory.Enqueue(candle.HighPrice);
		_lowHistory.Enqueue(candle.LowPrice);

		while (_highHistory.Count > ChannelPeriod)
			_highHistory.Dequeue();

		while (_lowHistory.Count > ChannelPeriod)
			_lowHistory.Dequeue();
	}

	private static decimal GetMax(IEnumerable<decimal> values)
	{
		var max = decimal.MinValue;

		foreach (var value in values)
		{
			if (value > max)
				max = value;
		}

		return max;
	}

	private static decimal GetMin(IEnumerable<decimal> values)
	{
		var min = decimal.MaxValue;

		foreach (var value in values)
		{
			if (value < min)
				min = value;
		}

		return min;
	}

	/// <inheritdoc />
	protected override void OnReseted()
	{
		_previousTrend = 0;
		_previousClose = null;
		_highHistory.Clear();
		_lowHistory.Clear();

		base.OnReseted();
	}
}