La Estrategia de número de rebote es un puerto StockSharp del indicador MetaTrader BounceNumber_V0.mq4 / BounceNumber_V1.mq4. La herramienta original era un analizador visual que contaba cuántas veces el precio tocaba un canal simétrico antes de salir de él. Esta estrategia de C# recrea el contador de rebotes con el nivel alto API, almacena los resultados en una tabla de distribución e informa cada ciclo completado a través del registro de estrategia. La implementación se mantiene fiel a la lógica de MetaTrader y la adapta al proceso basado en eventos de StockSharp.
A diferencia del indicador original, el puerto se ejecuta como un componente estratégico. Se suscribe a velas terminadas, monitorea los toques de banda y rastrea cuántos golpes alternos ocurren antes de que el precio salga del canal por el doble de su mitad de ancho. Las estadísticas recopiladas se pueden consumir desde la propiedad BounceDistribution o desde los mensajes de registro generados.
como funciona
Cuando se inicia la estrategia, valida que el instrumento expone un PriceStep distinto de cero. Las entradas basadas en puntos se basan en este valor para convertir MetaTrader "puntos" en distancias de precios decimales.
Una suscripción de vela creada a partir de CandleType alimenta el analizador de rebotes únicamente con barras completadas.
La primera vela entrante define el centro del canal (su precio de cierre). Alrededor de ese centro se crea una banda simétrica cuyo medio ancho es igual a ChannelPoints * PriceStep.
Cada nueva vela terminada incrementa el contador de ciclos y se evalúa con tres reglas:
Detección de ruptura: si el rango de la vela cruza center ± 2 * halfWidth, el ciclo actual finaliza y se registra su recuento de rebotes.
Toque de banda inferior: si la vela abarca la banda inferior y el toque anterior no fue también un toque de banda inferior, el contador de rebote aumenta en uno y la dirección cambia a "inferior".
Toque banda superior: regla simétrica para la banda superior.
Si un ciclo dura más velas que MaxHistoryCandles (y el parámetro es positivo), el canal se restablece a la fuerza, lo que garantiza que el histograma se actualice incluso cuando el precio se desvíe lateralmente para siempre.
En cada ciclo de reinicio, el diccionario de distribución se actualiza y se genera un registro de información que refleja el comportamiento de los contadores de la interfaz original.
La estrategia no realiza ningún pedido por diseño. Debe alojarse junto con otros componentes (paneles de control, interfaz de usuario, exportadores de datos) que consumen las estadísticas de BounceDistribution.
Parámetros
Nombre
Tipo
Predeterminado
MetaTrader analógico
Descripción
MaxHistoryCandles
int
10000
maxbar entrada
Número máximo de velas permitidas dentro de un ciclo antes de un reinicio forzado. Establezca en 0 para desactivar el reinicio de seguridad.
ChannelPoints
int
300
BPoints entrada
Medio ancho del canal de rebote expresado en puntos de precio (PriceStep múltiplos).
CandleType
DataType
M1 período de tiempo
TF entrada
Serie de velas utilizadas para los cálculos de rebote.
Diferencias vs. código MetaTrader
El histograma se almacena como un diccionario en lugar de objetos de texto en el gráfico. Esto hace que la información sea más fácil de exportar o visualizar en paneles de control StockSharp.
Las entradas específicas de la interfaz de usuario del indicador (colores, fuentes, botones) se eliminan porque eran cosméticas y no tienen ningún impacto en la lógica analítica.
El reinicio forzado por MaxHistoryCandles ahora es opcional (0 lo desactiva) y funciona en flujos de datos en vivo, mientras que MetaTrader procesó un bloque histórico finito.
Todos los mensajes informativos están escritos en inglés hasta AddInfoLog, lo que cumple con el requisito de comentarios/registros de código solo en inglés.
Consejos de uso
Asegúrese de que la seguridad seleccionada defina PriceStep; de lo contrario, la estrategia genera una excepción al inicio porque no se pueden calcular las compensaciones basadas en puntos.
Combine la estrategia con scripts o widgets de interfaz de usuario personalizados que lean BounceDistribution para replicar la cuadrícula de recuentos MetaTrader.
Utilice valores más pequeños para ChannelPoints al analizar el ruido intradiario y valores más grandes para períodos de tiempo más altos o instrumentos volátiles.
Para emular el escaneo histórico de la versión MQL, inicie la estrategia con HistoryBuildMode habilitado en su conector y deje que procese el rango histórico solicitado; la distribución se completará tan pronto como se entreguen las velas rellenas.
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()