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
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.
Ein aus CandleType erstelltes Kerzenabonnement versorgt den Bounce-Analysator nur mit abgeschlossenen Balken.
Die erste eingehende Kerze definiert die Kanalmitte (ihren Schlusskurs). Um dieses Zentrum herum wird ein symmetrisches Band mit einer Halbwertsbreite von ChannelPoints * PriceStep erstellt.
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.
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.
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;
}
}
import clr
clr.AddReference("StockSharp.Messages")
clr.AddReference("StockSharp.Algo")
clr.AddReference("StockSharp.Algo.Indicators")
clr.AddReference("StockSharp.Algo.Strategies")
from System import TimeSpan
from StockSharp.Messages import DataType, CandleStates
from StockSharp.Algo.Strategies import Strategy
class bounce_number_strategy(Strategy):
def __init__(self):
super(bounce_number_strategy, self).__init__()
self._candle_type = self.Param("CandleType", DataType.TimeFrame(TimeSpan.FromMinutes(5)))
self._max_history_candles = self.Param("MaxHistoryCandles", 10000)
self._channel_points = self.Param("ChannelPoints", 10)
self._channel_center = None
self._bounce_count = 0
self._last_touch_direction = 0
self._candles_in_cycle = 0
@property
def CandleType(self):
return self._candle_type.Value
@CandleType.setter
def CandleType(self, value):
self._candle_type.Value = value
@property
def MaxHistoryCandles(self):
return self._max_history_candles.Value
@MaxHistoryCandles.setter
def MaxHistoryCandles(self, value):
self._max_history_candles.Value = value
@property
def ChannelPoints(self):
return self._channel_points.Value
@ChannelPoints.setter
def ChannelPoints(self, value):
self._channel_points.Value = value
def OnReseted(self):
super(bounce_number_strategy, self).OnReseted()
self._channel_center = None
self._bounce_count = 0
self._last_touch_direction = 0
self._candles_in_cycle = 0
def OnStarted2(self, time):
super(bounce_number_strategy, self).OnStarted2(time)
self._channel_center = None
self._bounce_count = 0
self._last_touch_direction = 0
self._candles_in_cycle = 0
subscription = self.SubscribeCandles(self.CandleType)
subscription.Bind(self._process_candle).Start()
def _get_channel_half_width(self):
return float(self.ChannelPoints)
def _reset_channel(self, center):
self._channel_center = center
self._bounce_count = 0
self._last_touch_direction = 0
self._candles_in_cycle = 0
def _process_candle(self, candle):
if candle.State != CandleStates.Finished:
return
channel_half = self._get_channel_half_width()
if channel_half <= 0:
return
close = float(candle.ClosePrice)
high = float(candle.HighPrice)
low = float(candle.LowPrice)
if self._channel_center is None:
self._reset_channel(close)
return
self._candles_in_cycle += 1
center = self._channel_center
upper_band = center + channel_half
lower_band = center - channel_half
break_upper = center + channel_half * 2.0
break_lower = center - channel_half * 2.0
breakout_up = high >= break_upper
breakout_down = low <= break_lower
max_hist = self.MaxHistoryCandles
if breakout_up or breakout_down or (self._candles_in_cycle >= max_hist and max_hist > 0):
self._reset_channel(close)
return
touched_lower = low <= lower_band and high >= lower_band
touched_upper = high >= upper_band and low <= upper_band
if touched_lower and self._last_touch_direction >= 0:
self._bounce_count += 1
self._last_touch_direction = -1
if self.Position <= 0:
if self.Position < 0:
self.BuyMarket()
self.BuyMarket()
elif touched_upper and self._last_touch_direction <= 0:
self._bounce_count += 1
self._last_touch_direction = 1
if self.Position >= 0:
if self.Position > 0:
self.SellMarket()
self.SellMarket()
if breakout_up and self.Position <= 0:
if self.Position < 0:
self.BuyMarket()
self.BuyMarket()
elif breakout_down and self.Position >= 0:
if self.Position > 0:
self.SellMarket()
self.SellMarket()
def CreateClone(self):
return bounce_number_strategy()