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()