Стратегия BB Breakout Momentum Squeeze
Стратегия BB Breakout Momentum Squeeze объединяет осциллятор пробоя полос Боллинджера с фильтром «сжатия» волатильности. «Сжатие» определяется, когда полосы Боллинджера выходят за пределы каналов Кельтнера, что указывает на потенциальное расширение. При таком расширении и пересечении бычьего осциллятора выше порога открывается длинная позиция, а пересечение медвежьего осциллятора запускает короткую. Выход основан на полосе ATR и цели по соотношению риск‑прибыль.
Подробности
- Условия входа:
- Сжатие отсутствует (полосы Боллинджера вне каналов Кельтнера).
- Лонг: бычий осциллятор пересекает порог сверху.
- Шорт: медвежий осциллятор пересекает порог сверху.
- Направление: обе стороны.
- Условия выхода:
- Цена достигает стопа ATR или цели по риск‑прибыль.
- Стопы: полоса ATR с целью риск‑прибыль.
- Параметры по умолчанию:
BbLength= 14BbMultiplier= 1.0Threshold= 50SqueezeLength= 20SqueezeBbMultiplier= 2.0KcMultiplier= 2.0AtrLength= 30AtrMultiplier= 1.4RrRatio= 1.5
- Фильтры:
- Категория: Волатильностный пробой
- Направление: Обе
- Индикаторы: Полосы Боллинджера, каналы Кельтнера, ATR
- Стопы: Да
- Сложность: Средняя
- Таймфрейм: Любой
- Сезонность: Нет
- Нейросети: Нет
- Дивергенция: Нет
- Уровень риска: Средний
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>
/// Bollinger Breakout + Momentum Squeeze strategy.
/// </summary>
public class BbBreakoutMomentumSqueezeStrategy : Strategy
{
private readonly StrategyParam<DataType> _candleType;
private readonly StrategyParam<int> _bbLength;
private readonly StrategyParam<decimal> _bbMultiplier;
private readonly StrategyParam<decimal> _threshold;
private readonly StrategyParam<int> _squeezeLength;
private readonly StrategyParam<decimal> _squeezeBbMultiplier;
private readonly StrategyParam<decimal> _kcMultiplier;
private readonly StrategyParam<int> _atrLength;
private readonly StrategyParam<decimal> _atrMultiplier;
private readonly StrategyParam<decimal> _rrRatio;
private BollingerBands _bbBreakout;
private BollingerBands _squeezeBb;
private KeltnerChannels _keltner;
private AverageTrueRange _atr;
private SimpleMovingAverage _bullNum;
private SimpleMovingAverage _bullDen;
private SimpleMovingAverage _bearNum;
private SimpleMovingAverage _bearDen;
private SimpleMovingAverage _upperBandMa;
private SimpleMovingAverage _lowerBandMa;
private decimal? _prevBull;
private decimal? _prevBear;
/// <summary>
/// Initializes a new instance of the <see cref="BbBreakoutMomentumSqueezeStrategy"/>.
/// </summary>
public BbBreakoutMomentumSqueezeStrategy()
{
_candleType = Param(nameof(CandleType), TimeSpan.FromMinutes(5).TimeFrame())
.SetDisplay("Candle Type", "Type of candles to use", "General");
_bbLength = Param(nameof(BbLength), 10)
.SetGreaterThanZero()
.SetDisplay("BB Breakout Length", "Length for Bollinger breakout calculation", "BB Breakout")
.SetOptimize(10, 30, 2);
_bbMultiplier = Param(nameof(BbMultiplier), 1.0m)
.SetDisplay("BB Breakout Mult", "Bollinger breakout multiplier", "BB Breakout");
_threshold = Param(nameof(Threshold), 0m)
.SetRange(0m, 100m)
.SetDisplay("Threshold", "Middle line threshold", "BB Breakout");
_squeezeLength = Param(nameof(SqueezeLength), 20)
.SetGreaterThanZero()
.SetDisplay("Squeeze Length", "Length for squeeze calculation", "Squeeze");
_squeezeBbMultiplier = Param(nameof(SqueezeBbMultiplier), 2.0m)
.SetDisplay("Bollinger Mult", "Bollinger Band std multiplier for squeeze", "Squeeze");
_kcMultiplier = Param(nameof(KcMultiplier), 2.0m)
.SetDisplay("Keltner Mult", "Keltner Channel multiplier", "Squeeze");
_atrLength = Param(nameof(AtrLength), 30)
.SetGreaterThanZero()
.SetDisplay("ATR Length", "ATR calculation length", "ATR");
_atrMultiplier = Param(nameof(AtrMultiplier), 1.4m)
.SetDisplay("ATR Multiplier", "ATR stop multiplier", "ATR");
_rrRatio = Param(nameof(RrRatio), 1.5m)
.SetDisplay("RR Ratio", "Risk reward ratio", "Risk");
}
/// <summary>
/// Candle type for strategy calculation.
/// </summary>
public DataType CandleType { get => _candleType.Value; set => _candleType.Value = value; }
/// <summary>
/// Bollinger breakout length.
/// </summary>
public int BbLength { get => _bbLength.Value; set => _bbLength.Value = value; }
/// <summary>
/// Bollinger breakout multiplier.
/// </summary>
public decimal BbMultiplier { get => _bbMultiplier.Value; set => _bbMultiplier.Value = value; }
/// <summary>
/// Midline threshold for bull/bear oscillator.
/// </summary>
public decimal Threshold { get => _threshold.Value; set => _threshold.Value = value; }
/// <summary>
/// Squeeze calculation length.
/// </summary>
public int SqueezeLength { get => _squeezeLength.Value; set => _squeezeLength.Value = value; }
/// <summary>
/// Bollinger multiplier for squeeze detection.
/// </summary>
public decimal SqueezeBbMultiplier { get => _squeezeBbMultiplier.Value; set => _squeezeBbMultiplier.Value = value; }
/// <summary>
/// Keltner multiplier for squeeze detection.
/// </summary>
public decimal KcMultiplier { get => _kcMultiplier.Value; set => _kcMultiplier.Value = value; }
/// <summary>
/// ATR calculation length.
/// </summary>
public int AtrLength { get => _atrLength.Value; set => _atrLength.Value = value; }
/// <summary>
/// ATR stop multiplier.
/// </summary>
public decimal AtrMultiplier { get => _atrMultiplier.Value; set => _atrMultiplier.Value = value; }
/// <summary>
/// Risk reward ratio.
/// </summary>
public decimal RrRatio { get => _rrRatio.Value; set => _rrRatio.Value = value; }
/// <inheritdoc />
public override IEnumerable<(Security sec, DataType dt)> GetWorkingSecurities()
{
return [(Security, CandleType)];
}
/// <inheritdoc />
protected override void OnReseted()
{
base.OnReseted();
_prevBull = null;
_prevBear = null;
}
/// <inheritdoc />
protected override void OnStarted2(DateTime time)
{
base.OnStarted2(time);
_bbBreakout = new BollingerBands { Length = BbLength, Width = BbMultiplier };
_squeezeBb = new BollingerBands { Length = SqueezeLength, Width = SqueezeBbMultiplier };
_keltner = new KeltnerChannels { Length = SqueezeLength, Multiplier = KcMultiplier };
_atr = new AverageTrueRange { Length = AtrLength };
_bullNum = new SMA { Length = BbLength };
_bullDen = new SMA { Length = BbLength };
_bearNum = new SMA { Length = BbLength };
_bearDen = new SMA { Length = BbLength };
_upperBandMa = new SMA { Length = 3 };
_lowerBandMa = new SMA { Length = 3 };
var subscription = SubscribeCandles(CandleType);
subscription
.BindEx(_bbBreakout, _squeezeBb, _keltner, _atr, ProcessCandle)
.Start();
StartProtection(
takeProfit: new Unit(2, UnitTypes.Percent),
stopLoss: new Unit(1, UnitTypes.Percent)
);
var area = CreateChartArea();
if (area != null)
{
DrawCandles(area, subscription);
DrawOwnTrades(area);
}
}
private void ProcessCandle(ICandleMessage candle, IIndicatorValue bbBreakoutIv, IIndicatorValue squeezeBbIv, IIndicatorValue keltnerIv, IIndicatorValue atrIv)
{
if (candle.State != CandleStates.Finished)
return;
if (bbBreakoutIv is not BollingerBandsValue bbBreakoutValue ||
bbBreakoutValue.UpBand is not decimal breakoutUpper ||
bbBreakoutValue.LowBand is not decimal breakoutLower)
return;
if (squeezeBbIv is not BollingerBandsValue squeezeBbValue ||
squeezeBbValue.UpBand is not decimal squeezeUpper ||
squeezeBbValue.LowBand is not decimal squeezeLower)
return;
if (keltnerIv is not KeltnerChannelsValue keltnerValue ||
keltnerValue.Upper is not decimal kcUpper ||
keltnerValue.Lower is not decimal kcLower)
return;
if (atrIv.ToNullableDecimal() is not decimal atr)
return;
var close = candle.ClosePrice;
var time = candle.ServerTime;
var bullNumVal = _bullNum.Process(new DecimalIndicatorValue(_bullNum, Math.Max(close - breakoutUpper, 0m), time) { IsFinal = true }).ToNullableDecimal();
var bullDenVal = _bullDen.Process(new DecimalIndicatorValue(_bullDen, Math.Abs(close - breakoutUpper), time) { IsFinal = true }).ToNullableDecimal();
var bearNumVal = _bearNum.Process(new DecimalIndicatorValue(_bearNum, Math.Max(breakoutLower - close, 0m), time) { IsFinal = true }).ToNullableDecimal();
var bearDenVal = _bearDen.Process(new DecimalIndicatorValue(_bearDen, Math.Abs(breakoutLower - close), time) { IsFinal = true }).ToNullableDecimal();
if (bullNumVal is not decimal bullNum || bullDenVal is not decimal bullDen ||
bearNumVal is not decimal bearNum || bearDenVal is not decimal bearDen)
return;
if (!_bullNum.IsFormed || !_bullDen.IsFormed || !_bearNum.IsFormed || !_bearDen.IsFormed)
return;
var bull = bullDen == 0 ? 0m : bullNum / bullDen * 100m;
var bear = bearDen == 0 ? 0m : bearNum / bearDen * 100m;
var bullCross = _prevBull.HasValue && _prevBull <= Threshold && bull > Threshold;
var bearCross = _prevBear.HasValue && _prevBear <= Threshold && bear > Threshold;
_prevBull = bull;
_prevBear = bear;
var squeezeDotGreen = squeezeLower < kcLower || squeezeUpper > kcUpper;
var upperBandVal = _upperBandMa.Process(new DecimalIndicatorValue(_upperBandMa, close + atr * AtrMultiplier, time) { IsFinal = true }).ToNullableDecimal();
var lowerBandVal = _lowerBandMa.Process(new DecimalIndicatorValue(_lowerBandMa, close - atr * AtrMultiplier, time) { IsFinal = true }).ToNullableDecimal();
if (upperBandVal is not decimal upperBand || lowerBandVal is not decimal lowerBand)
return;
if (!_upperBandMa.IsFormed || !_lowerBandMa.IsFormed)
return;
if (bullCross && Position == 0)
{
BuyMarket();
}
else if (bearCross && Position == 0)
{
SellMarket();
}
}
}
import clr
clr.AddReference("StockSharp.Messages")
clr.AddReference("StockSharp.Algo")
clr.AddReference("StockSharp.Algo.Indicators")
clr.AddReference("StockSharp.Algo.Strategies")
from System import TimeSpan, Math
from StockSharp.Messages import DataType, CandleStates, UnitTypes, Unit
from StockSharp.Algo.Indicators import BollingerBands, KeltnerChannels, AverageTrueRange, SimpleMovingAverage
from StockSharp.Algo.Strategies import Strategy
from indicator_extensions import *
class bb_breakout_momentum_squeeze_strategy(Strategy):
def __init__(self):
super(bb_breakout_momentum_squeeze_strategy, self).__init__()
self._candle_type = self.Param("CandleType", DataType.TimeFrame(TimeSpan.FromMinutes(5))) \
.SetDisplay("Candle Type", "Type of candles to use", "General")
self._bb_length = self.Param("BbLength", 10) \
.SetGreaterThanZero() \
.SetDisplay("BB Breakout Length", "Length for Bollinger breakout calculation", "BB Breakout")
self._bb_mult = self.Param("BbMultiplier", 1.0) \
.SetDisplay("BB Breakout Mult", "Bollinger breakout multiplier", "BB Breakout")
self._threshold = self.Param("Threshold", 0.0) \
.SetDisplay("Threshold", "Middle line threshold", "BB Breakout")
self._squeeze_length = self.Param("SqueezeLength", 20) \
.SetGreaterThanZero() \
.SetDisplay("Squeeze Length", "Length for squeeze calculation", "Squeeze")
self._squeeze_bb_mult = self.Param("SqueezeBbMultiplier", 2.0) \
.SetDisplay("Bollinger Mult", "Bollinger Band std multiplier for squeeze", "Squeeze")
self._kc_mult = self.Param("KcMultiplier", 2.0) \
.SetDisplay("Keltner Mult", "Keltner Channel multiplier", "Squeeze")
self._atr_length = self.Param("AtrLength", 30) \
.SetGreaterThanZero() \
.SetDisplay("ATR Length", "ATR calculation length", "ATR")
self._atr_mult = self.Param("AtrMultiplier", 1.4) \
.SetDisplay("ATR Multiplier", "ATR stop multiplier", "ATR")
self._rr_ratio = self.Param("RrRatio", 1.5) \
.SetDisplay("RR Ratio", "Risk reward ratio", "Risk")
self._prev_bull = None
self._prev_bear = None
@property
def candle_type(self):
return self._candle_type.Value
@candle_type.setter
def candle_type(self, value):
self._candle_type.Value = value
def OnReseted(self):
super(bb_breakout_momentum_squeeze_strategy, self).OnReseted()
self._prev_bull = None
self._prev_bear = None
def OnStarted2(self, time):
super(bb_breakout_momentum_squeeze_strategy, self).OnStarted2(time)
self._bb_breakout = BollingerBands()
self._bb_breakout.Length = self._bb_length.Value
self._bb_breakout.Width = self._bb_mult.Value
self._squeeze_bb = BollingerBands()
self._squeeze_bb.Length = self._squeeze_length.Value
self._squeeze_bb.Width = self._squeeze_bb_mult.Value
self._keltner = KeltnerChannels()
self._keltner.Length = self._squeeze_length.Value
self._keltner.Multiplier = self._kc_mult.Value
self._atr = AverageTrueRange()
self._atr.Length = self._atr_length.Value
bb_len = self._bb_length.Value
self._bull_num = SimpleMovingAverage()
self._bull_num.Length = bb_len
self._bull_den = SimpleMovingAverage()
self._bull_den.Length = bb_len
self._bear_num = SimpleMovingAverage()
self._bear_num.Length = bb_len
self._bear_den = SimpleMovingAverage()
self._bear_den.Length = bb_len
self._upper_band_ma = SimpleMovingAverage()
self._upper_band_ma.Length = 3
self._lower_band_ma = SimpleMovingAverage()
self._lower_band_ma.Length = 3
subscription = self.SubscribeCandles(self.candle_type)
subscription.BindEx(self._bb_breakout, self._squeeze_bb, self._keltner, self._atr, self._process_candle).Start()
self.StartProtection(
takeProfit=Unit(2, UnitTypes.Percent),
stopLoss=Unit(1, UnitTypes.Percent)
)
area = self.CreateChartArea()
if area is not None:
self.DrawCandles(area, subscription)
self.DrawOwnTrades(area)
def _process_candle(self, candle, bb_breakout_iv, squeeze_bb_iv, keltner_iv, atr_iv):
if candle.State != CandleStates.Finished:
return
bb_upper = bb_breakout_iv.UpBand
bb_lower = bb_breakout_iv.LowBand
if bb_upper is None or bb_lower is None:
return
breakout_upper = float(bb_upper)
breakout_lower = float(bb_lower)
sq_upper = squeeze_bb_iv.UpBand
sq_lower = squeeze_bb_iv.LowBand
if sq_upper is None or sq_lower is None:
return
squeeze_upper = float(sq_upper)
squeeze_lower = float(sq_lower)
kc_upper_val = keltner_iv.Upper
kc_lower_val = keltner_iv.Lower
if kc_upper_val is None or kc_lower_val is None:
return
kc_upper = float(kc_upper_val)
kc_lower = float(kc_lower_val)
if not atr_iv.IsFormed:
return
atr = float(atr_iv)
close = float(candle.ClosePrice)
t = candle.ServerTime
bull_num_val = process_float(self._bull_num, max(close - breakout_upper, 0.0), t, True)
bull_den_val = process_float(self._bull_den, abs(close - breakout_upper), t, True)
bear_num_val = process_float(self._bear_num, max(breakout_lower - close, 0.0), t, True)
bear_den_val = process_float(self._bear_den, abs(breakout_lower - close), t, True)
if not self._bull_num.IsFormed or not self._bull_den.IsFormed or \
not self._bear_num.IsFormed or not self._bear_den.IsFormed:
return
bull_num_f = float(bull_num_val)
bull_den_f = float(bull_den_val)
bear_num_f = float(bear_num_val)
bear_den_f = float(bear_den_val)
bull = 0.0 if bull_den_f == 0 else bull_num_f / bull_den_f * 100.0
bear = 0.0 if bear_den_f == 0 else bear_num_f / bear_den_f * 100.0
threshold = float(self._threshold.Value)
bull_cross = self._prev_bull is not None and self._prev_bull <= threshold and bull > threshold
bear_cross = self._prev_bear is not None and self._prev_bear <= threshold and bear > threshold
self._prev_bull = bull
self._prev_bear = bear
atr_mult = float(self._atr_mult.Value)
upper_band_val = process_float(self._upper_band_ma, close + atr * atr_mult, t, True)
lower_band_val = process_float(self._lower_band_ma, close - atr * atr_mult, t, True)
if not self._upper_band_ma.IsFormed or not self._lower_band_ma.IsFormed:
return
if bull_cross and self.Position == 0:
self.BuyMarket()
elif bear_cross and self.Position == 0:
self.SellMarket()
def CreateClone(self):
return bb_breakout_momentum_squeeze_strategy()