Ver en GitHub

Estrategia de Momentum Squeeze de Ruptura BB

La estrategia BB Breakout Momentum Squeeze combina un oscilador de ruptura de Bollinger Bands con un filtro de squeeze de volatilidad. El squeeze se detecta cuando las Bollinger Bands se mueven fuera de los Keltner Channels, señalando una posible expansión. Una operación larga ocurre cuando el oscilador alcista cruza por encima del umbral durante esta expansión, mientras que una operación corta usa el cruce bajista. Los stops se basan en una banda ATR y un objetivo de riesgo-recompensa completa la lógica de salida.

Detalles

  • Criterios de entrada:
    • Squeeze apagado (Bollinger Bands fuera de los Keltner Channels).
    • Largo: el oscilador alcista cruza por encima del umbral.
    • Corto: el oscilador bajista cruza por encima del umbral.
  • Largo/Corto: Ambos lados.
  • Criterios de salida:
    • El precio alcanza el stop ATR o el objetivo de riesgo-recompensa.
  • Stops: Banda ATR con objetivo de riesgo-recompensa.
  • Valores predeterminados:
    • BbLength = 14
    • BbMultiplier = 1.0
    • Threshold = 50
    • SqueezeLength = 20
    • SqueezeBbMultiplier = 2.0
    • KcMultiplier = 2.0
    • AtrLength = 30
    • AtrMultiplier = 1.4
    • RrRatio = 1.5
  • Filtros:
    • Categoría: Ruptura de volatilidad
    • Dirección: Ambos
    • Indicadores: Bollinger Bands, Keltner Channels, ATR
    • Stops: Sí
    • Complejidad: Moderado
    • Marco temporal: Cualquiera
    • Estacionalidad: No
    • Redes neuronales: No
    • Divergencia: No
    • Nivel de riesgo: Medio
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();
		}
	}
}