A Estratégia de número de rejeição é uma porta StockSharp do indicador MetaTrader BounceNumber_V0.mq4 / BounceNumber_V1.mq4. A ferramenta original era um analisador visual que contava quantas vezes o preço tocou um canal simétrico antes de sair dele. Essa estratégia C# recria o contador de rejeição com o API de alto nível, armazena os resultados em uma tabela de distribuição e relata cada ciclo concluído por meio do log de estratégia. A implementação permanece fiel à lógica MetaTrader enquanto a adapta ao pipeline orientado a eventos de StockSharp.
Ao contrário do indicador original, o porto funciona como um componente estratégico. Ele assina velas finalizadas, monitora toques de banda e rastreia quantos golpes alternados ocorrem antes que o preço saia do canal duas vezes sua meia largura. As estatísticas coletadas podem ser consumidas da propriedade BounceDistribution ou das mensagens de log geradas.
Como funciona
Quando a estratégia é iniciada, ela valida que o instrumento expõe um PriceStep diferente de zero. As entradas baseadas em pontos dependem deste valor para converter MetaTrader "pontos" em distâncias de preços decimais.
Uma assinatura de vela criada a partir de CandleType alimenta o analisador de rejeição apenas com barras completas.
A primeira vela recebida define o centro do canal (seu preço de fechamento). Uma banda simétrica cuja meia largura é igual a ChannelPoints * PriceStep é criada em torno desse centro.
Cada nova vela finalizada incrementa o contador de ciclos e é avaliada com três regras:
Detecção de breakout: se o alcance da vela ultrapassar center ± 2 * halfWidth, o ciclo atual termina e sua contagem de saltos é registrada.
Toque na banda inferior: se a vela abrange a banda inferior e o toque anterior também não foi um toque na banda inferior, o contador de salto aumenta em um e a direção muda para "inferior".
Toque na faixa superior: regra simétrica para a faixa superior.
Se um ciclo durar mais velas que MaxHistoryCandles (e o parâmetro for positivo), o canal será redefinido à força, garantindo que o histograma seja atualizado mesmo quando o preço oscilar lateralmente para sempre.
A cada reinicialização do ciclo o dicionário de distribuição é atualizado e um log de informações é produzido, espelhando o comportamento dos contadores da interface original.
A estratégia não faz pedidos intencionalmente. Deve ser hospedado junto com outros componentes (dashboards, UI, exportadores de dados) que consomem as estatísticas BounceDistribution.
Parâmetros
Nome
Tipo
Padrão
MetaTrader analógico
Descrição
MaxHistoryCandles
int
10000
entrada maxbar
Número máximo de velas permitidas dentro de um ciclo antes de um reset forçado. Defina como 0 para desativar a redefinição de segurança.
ChannelPoints
int
300
entrada BPoints
Meia largura do canal de rejeição expressa em faixas de preço (PriceStep múltiplos).
CandleType
DataType
M1 prazo
entrada TF
Série de velas usada para cálculos de rejeição.
Diferenças vs. código MetaTrader
O histograma é armazenado como um dicionário em vez de objetos de texto no gráfico. Isso torna as informações mais fáceis de exportar ou visualizar em painéis StockSharp.
As entradas específicas da UI do indicador (cores, fontes, botões) são removidas porque eram cosméticas e não têm impacto na lógica analítica.
A redefinição forçada por MaxHistoryCandles agora é opcional (0 a desativa) e funciona em fluxos de dados ao vivo, enquanto MetaTrader processou um bloco histórico finito.
Todas as mensagens informativas são escritas em inglês por meio de AddInfoLog, atendendo ao requisito de comentários/logs de código somente em inglês.
Dicas de uso
Certifique-se de que a segurança selecionada defina PriceStep; caso contrário, a estratégia lançará uma exceção no início porque os deslocamentos baseados em pontos não podem ser calculados.
Combine a estratégia com widgets de UI personalizados ou scripts que leem BounceDistribution para replicar a grade de contagens MetaTrader.
Use valores menores para ChannelPoints ao analisar ruído intradiário e valores maiores para prazos mais altos ou instrumentos voláteis.
Para emular a verificação histórica da versão MQL, inicie a estratégia com HistoryBuildMode ativado em seu conector e deixe-o processar o intervalo histórico solicitado; a distribuição será preenchida assim que as velas preenchidas forem entregues.
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()