Auf GitHub ansehen

Bounce-Nummern-Strategie

Überblick

Die Bounce Number Strategy ist ein StockSharp-Port des MetaTrader-Indikators BounceNumber_V0.mq4 / BounceNumber_V1.mq4. Das ursprüngliche Tool war ein visueller Analysator, der zählte, wie oft der Preis einen symmetrischen Kanal berührte, bevor er aus ihm ausbrach. Diese C#-Strategie erstellt den Absprungzähler mit dem übergeordneten API neu, speichert die Ergebnisse in einer Verteilungstabelle und meldet jeden abgeschlossenen Zyklus über das Strategieprotokoll. Die Implementierung bleibt der Logik von MetaTrader treu und passt sie gleichzeitig an die ereignisgesteuerte Pipeline von StockSharp an.

Im Gegensatz zum ursprünglichen Indikator läuft der Port als Strategiekomponente. Es abonniert fertige Kerzen, überwacht Bandberührungen und verfolgt, wie viele abwechselnde Treffer auftreten, bevor der Preis den Kanal um das Doppelte seiner halben Breite verlässt. Die gesammelten Statistiken können aus der Eigenschaft BounceDistribution oder aus den generierten Protokollmeldungen entnommen werden.

Wie es funktioniert

  1. Wenn die Strategie startet, validiert sie, dass das Instrument einen PriceStep ungleich Null bereitstellt. Punktbasierte Eingaben basieren auf diesem Wert, um MetaTrader „Punkte“ in dezimale Preisabstände umzuwandeln.
  2. Ein aus CandleType erstelltes Kerzenabonnement versorgt den Bounce-Analysator nur mit abgeschlossenen Balken.
  3. Die erste eingehende Kerze definiert die Kanalmitte (ihren Schlusskurs). Um dieses Zentrum herum wird ein symmetrisches Band mit einer Halbwertsbreite von ChannelPoints * PriceStep erstellt.
  4. Jede neue fertige Kerze erhöht den Zykluszähler und wird nach drei Regeln ausgewertet:
    • Breakout-Erkennung: Wenn die Spanne der Kerze center ± 2 * halfWidth überschreitet, endet der aktuelle Zyklus und die Anzahl der Absprünge wird aufgezeichnet.
    • Berührung des unteren Bandes: Wenn die Kerze das untere Band überspannt und die vorherige Berührung nicht auch eine Berührung des unteren Bandes war, erhöht sich der Sprungzähler um eins und die Richtung wechselt auf „unten“.
    • Berührung des oberen Bandes: Symmetrische Regel für das obere Band.
  5. Wenn ein Zyklus mehr Kerzen als MaxHistoryCandles dauert (und der Parameter positiv ist), wird der Kanal zwangsweise zurückgesetzt, um sicherzustellen, dass das Histogramm auch dann aktualisiert wird, wenn der Preis für immer seitwärts driftet.
  6. Bei jedem Zurücksetzen des Zyklus wird das Verteilungswörterbuch aktualisiert und ein Informationsprotokoll erstellt, das das Verhalten der ursprünglichen Schnittstellenzähler widerspiegelt.

Die Strategie erteilt absichtlich keine Aufträge. Es sollte zusammen mit anderen Komponenten (Dashboards, Benutzeroberfläche, Datenexporteure) gehostet werden, die die BounceDistribution-Statistiken nutzen.

Parameter

Name Typ Standard MetaTrader analog Beschreibung
MaxHistoryCandles int 10000 maxbar Eingabe Maximal zulässige Anzahl von Kerzen innerhalb eines Zyklus vor einem erzwungenen Reset. Auf 0 einstellen, um den Sicherheitsreset zu deaktivieren.
ChannelPoints int 300 BPoints Eingabe Halbe Breite des Absprungkanals, ausgedrückt in Preispunkten (PriceStep Vielfache).
CandleType DataType M1 Zeitrahmen TF Eingabe Für die Absprungberechnungen verwendete Kerzenserie.

Unterschiede zum MetaTrader-Code

  • Das Histogramm wird als Wörterbuch statt als Textobjekte im Diagramm gespeichert. Dadurch lassen sich die Informationen einfacher exportieren oder in StockSharp-Dashboards visualisieren.
  • UI-spezifische Eingaben vom Indikator (Farben, Schriftarten, Schaltflächen) werden entfernt, da sie kosmetischer Natur waren und keinen Einfluss auf die Analyselogik haben.
  • Das erzwungene Zurücksetzen durch MaxHistoryCandles ist jetzt optional (0 deaktiviert es) und funktioniert bei Live-Datenströmen, während MetaTrader einen endlichen historischen Block verarbeitet hat.
  • Alle informativen Nachrichten werden bis AddInfoLog auf Englisch verfasst, was der Anforderung für nur englischsprachige Codekommentare/Protokolle entspricht.

Anwendungstipps

  • Stellen Sie sicher, dass die ausgewählte Sicherheit PriceStep definiert; Andernfalls löst die Strategie beim Start eine Ausnahme aus, da punktbasierte Offsets nicht berechnet werden können.
  • Kombinieren Sie die Strategie mit benutzerdefinierten UI-Widgets oder Skripten, die BounceDistribution lauten, um das Zählraster MetaTrader zu replizieren.
  • Verwenden Sie kleinere Werte für ChannelPoints, wenn Sie Intraday-Rauschen analysieren, und größere Werte für längere Zeitrahmen oder volatile Instrumente.
  • Um den historischen Scan der Version MQL zu emulieren, starten Sie die Strategie mit aktiviertem HistoryBuildMode in Ihrem Connector und lassen Sie ihn den angeforderten historischen Bereich verarbeiten. Die Verteilung wird gefüllt, sobald die nachgefüllten Kerzen geliefert werden.
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>
/// Port of the "BounceNumber" MetaTrader indicator that counts how many times price bounces inside a channel before breaking it.
/// The strategy keeps track of the touch statistics and logs the distribution after each completed cycle.
/// </summary>
public class BounceNumberStrategy : Strategy
{
	private readonly StrategyParam<int> _maxHistoryCandles;
	private readonly StrategyParam<int> _channelPoints;
	private readonly StrategyParam<DataType> _candleType;

	private readonly Dictionary<int, int> _bounceDistribution = new();

	private decimal? _channelCenter;
	private int _bounceCount;
	private int _lastTouchDirection;
	private int _candlesInCycle;

	/// <summary>
	/// Maximum number of candles allowed inside one channel cycle before it is forcefully reset.
	/// </summary>
	public int MaxHistoryCandles
	{
		get => _maxHistoryCandles.Value;
		set => _maxHistoryCandles.Value = value;
	}

	/// <summary>
	/// Half-width of the bounce channel expressed in price points.
	/// </summary>
	public int ChannelPoints
	{
		get => _channelPoints.Value;
		set => _channelPoints.Value = value;
	}

	/// <summary>
	/// Candle series that feeds the bounce counter.
	/// </summary>
	public DataType CandleType
	{
		get => _candleType.Value;
		set => _candleType.Value = value;
	}

	/// <summary>
	/// Provides read-only access to the accumulated bounce distribution.
	/// </summary>
	public IReadOnlyDictionary<int, int> BounceDistribution => _bounceDistribution;

	/// <summary>
	/// Initializes a new instance of the <see cref="BounceNumberStrategy"/> class.
	/// </summary>
	public BounceNumberStrategy()
	{
		_maxHistoryCandles = Param(nameof(MaxHistoryCandles), 10000)
			.SetNotNegative()
			.SetDisplay("Max History Candles", "Maximum number of candles inspected inside a single channel cycle", "General")
			;

		_channelPoints = Param(nameof(ChannelPoints), 10)
			.SetRange(10, 5000)
			.SetDisplay("Channel Half-Width", "Half height of the bounce channel measured in price points", "General")
			;

		_candleType = Param(nameof(CandleType), TimeSpan.FromMinutes(5).TimeFrame())
			.SetDisplay("Candle Type", "Timeframe used to perform the bounce analysis", "Data");
	}

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

		_bounceDistribution.Clear();
		_channelCenter = null;
		_bounceCount = 0;
		_lastTouchDirection = 0;
		_candlesInCycle = 0;
	}

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

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

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

		var channelHalf = GetChannelHalfWidth();

		if (channelHalf <= 0m)
			return;

		if (_channelCenter is null)
		{
			ResetChannel(candle.ClosePrice, channelHalf);
			return;
		}

		_candlesInCycle++;

		var center = _channelCenter.Value;
		var upperBand = center + channelHalf;
		var lowerBand = center - channelHalf;
		var breakUpper = center + channelHalf * 2m;
		var breakLower = center - channelHalf * 2m;

		var candleHigh = candle.HighPrice;
		var candleLow = candle.LowPrice;

		var breakoutUp = candleHigh >= breakUpper;
		var breakoutDown = candleLow <= breakLower;

		if (breakoutUp || breakoutDown || (_candlesInCycle >= MaxHistoryCandles && MaxHistoryCandles > 0))
		{
			RegisterBounceResult();
			ResetChannel(candle.ClosePrice, channelHalf);
			return;
		}

		var touchedLower = candleLow <= lowerBand && candleHigh >= lowerBand;
		var touchedUpper = candleHigh >= upperBand && candleLow <= upperBand;

		if (touchedLower && _lastTouchDirection >= 0)
		{
			_bounceCount++;
			_lastTouchDirection = -1;

			if (Position <= 0)
			{
				if (Position < 0)
					BuyMarket();
				BuyMarket();
			}
		}
		else if (touchedUpper && _lastTouchDirection <= 0)
		{
			_bounceCount++;
			_lastTouchDirection = 1;

			if (Position >= 0)
			{
				if (Position > 0)
					SellMarket();
				SellMarket();
			}
		}

		if (breakoutUp && Position <= 0)
		{
			if (Position < 0)
				BuyMarket();
			BuyMarket();
		}
		else if (breakoutDown && Position >= 0)
		{
			if (Position > 0)
				SellMarket();
			SellMarket();
		}
	}

	private void RegisterBounceResult()
	{
		if (!_bounceDistribution.TryGetValue(_bounceCount, out var occurrences))
			occurrences = 0;

		_bounceDistribution[_bounceCount] = occurrences + 1;

		LogInfo($"Channel cycle finished with {_bounceCount} bounce(s). Total occurrences for this count: {_bounceDistribution[_bounceCount]}.");
	}

	private void ResetChannel(decimal center, decimal channelHalf)
	{
		_channelCenter = center;
		_bounceCount = 0;
		_lastTouchDirection = 0;
		_candlesInCycle = 0;

		LogInfo($"Channel reset around price {center} with half-width {channelHalf}.");
	}

	private decimal GetChannelHalfWidth()
	{
		var priceStep = Security?.PriceStep;

		if (priceStep is null || priceStep.Value <= 0m)
			return ChannelPoints;

		return ChannelPoints * priceStep.Value;
	}
}