Ver no GitHub

Estratégia de Zonas RSI de Bollinger Bands

Um sistema de rompimento multi-banda de Bollinger Bands convertido do expert MetaTrader «Bollinger Bands RSI». A estratégia deriva três envelopes de Bollinger com períodos idênticos mas desvios diferentes para criar bandas «amarela», «azul» e «vermelha». Ordens são acionadas quando o preço revisita zonas configuráveis ao redor dessas bandas, com confirmação opcional por filtros RSI e Estocástico.

Lógica da estratégia

  • A banda principal (amarela) usa o multiplicador de desvio configurado.
  • A banda azul reduz pela metade o desvio, criando um envelope mais estreito.
  • A banda vermelha dobra o desvio, produzindo um envelope externo amplo.
  • Os valores de RSI e Estocástico são avaliados na vela finalizada anterior (Bar Shift) para corresponder ao comportamento original do EA.
  • Only One Position controla se novas ordens são permitidas apenas quando a posição líquida está zerada ou se operações de escala adicionais são permitidas quando o preço retorna à linha média de Bollinger.

Critérios de entrada

Entradas compradas

  1. O preço na vela atual cai para ou abaixo da zona de entrada comprada selecionada (Entry Mode):
    • Ponto médio entre amarela e azul, azul e vermelha, ou uma das bandas individuais.
  2. Confirmações opcionais:
    • Filtro RSI: RSI ≤ 100 - RSI Lower.
    • Filtro Estocástico: %K < 100 - Stochastic Lower.
  3. Pré-requisitos de posição:
    • Se Only One Position estiver habilitado, a posição líquida deve estar zerada.
    • Caso contrário, ordens compradas adicionais são bloqueadas até que a vela feche acima da banda média (amarela), emulando a lógica de bloqueio do EA.

Entradas vendidas

  1. O preço na vela atual sobe para ou acima da zona de entrada vendida selecionada (espelha as opções compradas).
  2. Confirmações opcionais:
    • Filtro RSI: RSI ≥ RSI Lower.
    • Filtro Estocástico: %K > Stochastic Lower.
  3. Os pré-requisitos de posição espelham a lógica comprada (posição zerada para modo de operação única ou estado desbloqueado quando a vela fecha de volta abaixo da banda média).

Critérios de saída

  • O modo de fechamento é determinado por Closure Mode:
    • Middle Line: sair de posições compradas quando o preço atinge a banda média de Bollinger; sair de vendidas quando o preço a toca por cima.
    • Between Yellow and Blue / Between Blue and Red: sair nos mesmos pontos médios usados para entradas; padrão para pontos médios entre azul e vermelho quando o modo de entrada difere.
    • Yellow Line, Blue Line, Red Line: sair em toques diretos das bandas superiores/inferiores correspondentes.
  • Os indicadores de bloqueio para o modo de escala são redefinidos automaticamente quando a vela fecha no lado oposto da banda média, recriando o comportamento do EA.

Gestão de riscos

  • Os parâmetros Stop Loss e Take Profit são expressos em pips e convertidos em distâncias de preço absolutas através de Pip Value quando StartProtection é inicializado.
  • Stops e alvos são opcionais; deixe a distância em zero para desabilitar a proteção respectiva.
  • O volume de negociação é definido por Order Volume e aplicado a cada ordem a mercado.

Parâmetros

Nome Descrição Padrão
Entry Mode Escolhe a zona de Bollinger que aciona as entradas. Entre amarela e azul
Closure Mode Define a banda ou ponto médio de tomada de lucro. Entre azul e vermelha
Bands Period Comprimento de período compartilhado por todas as Bollinger Bands. 140
Deviation Multiplicador de desvio padrão para a banda amarela (azul é metade, vermelha é o dobro). 2.0
Use RSI Filter Ativa a lógica de confirmação RSI. false
RSI Period Período de média do RSI. 8
RSI Lower Limite de sobrecompra; sobrevenda usa 100 - valor. 70
Use Stochastic Filter Ativa a lógica de confirmação %K. true
Stochastic Period Período principal de retrocesso %K (suavização fixa em 3/3 SMA). 20
Stochastic Lower Limite de sobrecompra; sobrevenda usa 100 - valor. 95
Bar Shift Número de barras finalizadas para olhar para trás em busca de valores de indicadores. 1
Only One Position Se habilitado, abre novas operações apenas quando não há posição ativa. true
Order Volume Volume enviado com cada ordem a mercado. 1
Pip Value Valor de preço absoluto de um pip para conversão de stop/alvo. 0.0001
Stop Loss Distância de stop protetor em pips (0 desabilita). 200
Take Profit Distância de alvo protetor em pips (0 desabilita). 200
Candle Type Tipo de dados usado para cálculos (velas de 1 minuto por padrão). Período de 1m

Notas

  • A estratégia processa apenas velas concluídas, portanto Bar Shift deve permanecer ≥ 1 para evitar referenciar barras inacabadas.
  • Os filtros RSI e Estocástico usam a linha %K; a linha %D é calculada mas não usada, refletindo a implementação original do EA.
  • A conversão mantém comentários e nomes de sinais em inglês e segue as diretrizes da API de alto nível do StockSharp (pipeline de indicadores baseado em Bind, sem acesso manual a buffers).
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;

using StockSharp.Algo;

namespace StockSharp.Samples.Strategies;

/// <summary>
/// Bollinger Bands based strategy with optional RSI and Stochastic filters.
/// Replicates the Bollinger Bands RSI expert advisor logic with configurable entry and exit zones.
/// </summary>
public class BollingerBandsRsiZonesStrategy : Strategy
{
	/// <summary>
	/// Entry location for Bollinger Bands RSI strategy.
	/// </summary>
	public enum BollingerBandsRsiEntryModes
	{
		/// <summary>
		/// Midpoint between yellow (primary) and blue (narrow) bands.
		/// </summary>
		BetweenYellowAndBlue,

		/// <summary>
		/// Midpoint between blue (narrow) and red (wide) bands.
		/// </summary>
		BetweenBlueAndRed,

		/// <summary>
		/// Yellow band itself.
		/// </summary>
		YellowLine,

		/// <summary>
		/// Blue band (narrow deviation).
		/// </summary>
		BlueLine,

		/// <summary>
		/// Red band (wide deviation).
		/// </summary>
		RedLine
	}

	/// <summary>
	/// Exit location for Bollinger Bands RSI strategy.
	/// </summary>
	public enum BollingerBandsRsiClosureModes
	{
		/// <summary>
		/// Exit on the middle Bollinger band.
		/// </summary>
		MiddleLine,

		/// <summary>
		/// Exit between yellow and blue bands.
		/// </summary>
		BetweenYellowAndBlue,

		/// <summary>
		/// Exit between blue and red bands.
		/// </summary>
		BetweenBlueAndRed,

		/// <summary>
		/// Exit on the yellow band.
		/// </summary>
		YellowLine,

		/// <summary>
		/// Exit on the blue band.
		/// </summary>
		BlueLine,

		/// <summary>
		/// Exit on the red band.
		/// </summary>
		RedLine
	}
	private readonly StrategyParam<BollingerBandsRsiEntryModes> _entryMode;
	private readonly StrategyParam<BollingerBandsRsiClosureModes> _closureMode;
	private readonly StrategyParam<int> _bandsPeriod;
	private readonly StrategyParam<decimal> _deviation;
	private readonly StrategyParam<bool> _useRsiFilter;
	private readonly StrategyParam<int> _rsiPeriod;
	private readonly StrategyParam<decimal> _rsiLowerLevel;
	private readonly StrategyParam<bool> _useStochasticFilter;
	private readonly StrategyParam<int> _stochasticPeriod;
	private readonly StrategyParam<decimal> _stochasticLowerLevel;
	private readonly StrategyParam<int> _barShift;
	private readonly StrategyParam<bool> _onlyOnePosition;
	private readonly StrategyParam<decimal> _orderVolume;
	private readonly StrategyParam<decimal> _pipValue;
	private readonly StrategyParam<decimal> _stopLossPips;
	private readonly StrategyParam<decimal> _takeProfitPips;
	private readonly StrategyParam<DataType> _candleType;

	private BollingerBands _teeth = null!;
	private BollingerBands _jaws = null!;
	private BollingerBands _lips = null!;
	private RelativeStrengthIndex _rsi = null!;
	private StochasticOscillator _stochastic = null!;

	private readonly List<decimal> _teethMiddleHistory = new();
	private readonly List<decimal> _teethUpperHistory = new();
	private readonly List<decimal> _teethLowerHistory = new();
	private readonly List<decimal> _jawsUpperHistory = new();
	private readonly List<decimal> _jawsLowerHistory = new();
	private readonly List<decimal> _lipsUpperHistory = new();
	private readonly List<decimal> _lipsLowerHistory = new();
	private readonly List<decimal> _rsiHistory = new();
	private readonly List<decimal> _stochasticHistory = new();

	private bool _longLocked;
	private bool _shortLocked;

	/// <summary>
	/// Entry zone selection.
	/// </summary>
	public BollingerBandsRsiEntryModes EntryMode
	{
		get => _entryMode.Value;
		set => _entryMode.Value = value;
	}

	/// <summary>
	/// Exit zone selection.
	/// </summary>
	public BollingerBandsRsiClosureModes ClosureMode
	{
		get => _closureMode.Value;
		set => _closureMode.Value = value;
	}

	/// <summary>
	/// Bollinger period for all bands.
	/// </summary>
	public int BandsPeriod
	{
		get => _bandsPeriod.Value;
		set => _bandsPeriod.Value = value;
	}

	/// <summary>
	/// Standard deviation multiplier for the primary (yellow) band.
	/// </summary>
	public decimal Deviation
	{
		get => _deviation.Value;
		set => _deviation.Value = value;
	}

	/// <summary>
	/// Enable RSI filter.
	/// </summary>
	public bool UseRsiFilter
	{
		get => _useRsiFilter.Value;
		set => _useRsiFilter.Value = value;
	}

	/// <summary>
	/// RSI averaging period.
	/// </summary>
	public int RsiPeriod
	{
		get => _rsiPeriod.Value;
		set => _rsiPeriod.Value = value;
	}

	/// <summary>
	/// RSI short threshold (long threshold is mirrored from 100).
	/// </summary>
	public decimal RsiLowerLevel
	{
		get => _rsiLowerLevel.Value;
		set => _rsiLowerLevel.Value = value;
	}

	/// <summary>
	/// Enable Stochastic filter.
	/// </summary>
	public bool UseStochasticFilter
	{
		get => _useStochasticFilter.Value;
		set => _useStochasticFilter.Value = value;
	}

	/// <summary>
	/// Stochastic main period.
	/// </summary>
	public int StochasticPeriod
	{
		get => _stochasticPeriod.Value;
		set => _stochasticPeriod.Value = value;
	}

	/// <summary>
	/// Stochastic overbought level (long threshold is mirrored from 100).
	/// </summary>
	public decimal StochasticLowerLevel
	{
		get => _stochasticLowerLevel.Value;
		set => _stochasticLowerLevel.Value = value;
	}

	/// <summary>
	/// Number of finished bars used for indicator shift.
	/// </summary>
	public int BarShift
	{
		get => _barShift.Value;
		set => _barShift.Value = value;
	}

	/// <summary>
	/// Allow only one open position at a time.
	/// </summary>
	public bool OnlyOnePosition
	{
		get => _onlyOnePosition.Value;
		set => _onlyOnePosition.Value = value;
	}

	/// <summary>
	/// Trading volume for new orders.
	/// </summary>
	public decimal OrderVolume
	{
		get => _orderVolume.Value;
		set => _orderVolume.Value = value;
	}

	/// <summary>
	/// Value of one pip in price units.
	/// </summary>
	public decimal PipValue
	{
		get => _pipValue.Value;
		set => _pipValue.Value = value;
	}

	/// <summary>
	/// Stop loss distance in pips.
	/// </summary>
	public decimal StopLossPips
	{
		get => _stopLossPips.Value;
		set => _stopLossPips.Value = value;
	}

	/// <summary>
	/// Take profit distance in pips.
	/// </summary>
	public decimal TakeProfitPips
	{
		get => _takeProfitPips.Value;
		set => _takeProfitPips.Value = value;
	}

	/// <summary>
	/// Candle type for indicator calculations.
	/// </summary>
	public DataType CandleType
	{
		get => _candleType.Value;
		set => _candleType.Value = value;
	}

	/// <summary>
	/// Initializes a new instance of the <see cref="BollingerBandsRsiZonesStrategy"/> class.
	/// </summary>
	public BollingerBandsRsiZonesStrategy()
	{
		_entryMode = Param(nameof(EntryMode), BollingerBandsRsiEntryModes.BetweenYellowAndBlue)
			.SetDisplay("Entry Mode", "Bollinger zone used for entries", "Trading");

		_closureMode = Param(nameof(ClosureMode), BollingerBandsRsiClosureModes.BetweenBlueAndRed)
			.SetDisplay("Closure Mode", "Bollinger zone used for exits", "Trading");

		_bandsPeriod = Param(nameof(BandsPeriod), 20)
			.SetGreaterThanZero()
			.SetDisplay("Bands Period", "Length of all Bollinger bands", "Indicators")
			;

		_deviation = Param(nameof(Deviation), 2m)
			.SetGreaterThanZero()
			.SetDisplay("Deviation", "Standard deviation for yellow band", "Indicators")
			;

		_useRsiFilter = Param(nameof(UseRsiFilter), false)
			.SetDisplay("Use RSI Filter", "Enable RSI confirmation", "Filters");

		_rsiPeriod = Param(nameof(RsiPeriod), 8)
			.SetGreaterThanZero()
			.SetDisplay("RSI Period", "Length of RSI filter", "Filters")
			;

		_rsiLowerLevel = Param(nameof(RsiLowerLevel), 70m)
			.SetDisplay("RSI Lower", "Short threshold (long uses 100-threshold)", "Filters")
			;

		_useStochasticFilter = Param(nameof(UseStochasticFilter), false)
			.SetDisplay("Use Stochastic Filter", "Enable Stochastic confirmation", "Filters");

		_stochasticPeriod = Param(nameof(StochasticPeriod), 20)
			.SetGreaterThanZero()
			.SetDisplay("Stochastic Period", "Main %K period", "Filters")
			;

		_stochasticLowerLevel = Param(nameof(StochasticLowerLevel), 95m)
			.SetDisplay("Stochastic Lower", "Overbought threshold (long uses mirror)", "Filters")
			;

		_barShift = Param(nameof(BarShift), 1)
			.SetGreaterThanZero()
			.SetDisplay("Bar Shift", "Number of finished bars for signals", "Trading");

		_onlyOnePosition = Param(nameof(OnlyOnePosition), true)
			.SetDisplay("Only One Position", "Restrict to single open position", "Risk");

		_orderVolume = Param(nameof(OrderVolume), 1m)
			.SetGreaterThanZero()
			.SetDisplay("Order Volume", "Volume sent with each market order", "Trading");

		_pipValue = Param(nameof(PipValue), 0.0001m)
			.SetGreaterThanZero()
			.SetDisplay("Pip Value", "Monetary value of one pip", "Risk");

		_stopLossPips = Param(nameof(StopLossPips), 200m)
			.SetNotNegative()
			.SetDisplay("Stop Loss", "Stop loss distance in pips", "Risk");

		_takeProfitPips = Param(nameof(TakeProfitPips), 200m)
			.SetNotNegative()
			.SetDisplay("Take Profit", "Take profit distance in pips", "Risk");

		_candleType = Param(nameof(CandleType), TimeSpan.FromHours(4).TimeFrame())
			.SetDisplay("Candle Type", "Type of candles for analysis", "General");
	}

	/// <inheritdoc />
	public override IEnumerable<(Security sec, DataType dt)> GetWorkingSecurities()
	{
		return [(Security, CandleType)];
	}

	/// <inheritdoc />
	protected override void OnReseted()
	{
		base.OnReseted();

		_teethMiddleHistory.Clear();
		_teethUpperHistory.Clear();
		_teethLowerHistory.Clear();
		_jawsUpperHistory.Clear();
		_jawsLowerHistory.Clear();
		_lipsUpperHistory.Clear();
		_lipsLowerHistory.Clear();
		_rsiHistory.Clear();
		_stochasticHistory.Clear();
		_longLocked = false;
		_shortLocked = false;
	}

	/// <inheritdoc />
	protected override void OnStarted2(DateTime time)
	{
		base.OnStarted2(time);

		Volume = OrderVolume;

		_teeth = new BollingerBands
		{
			Length = BandsPeriod,
			Width = Deviation
		};

		_jaws = new BollingerBands
		{
			Length = BandsPeriod,
			Width = Deviation / 2m
		};

		_lips = new BollingerBands
		{
			Length = BandsPeriod,
			Width = Deviation * 2m
		};

		_rsi = new RelativeStrengthIndex { Length = RsiPeriod };

		_stochastic = new StochasticOscillator
		{
			K = { Length = StochasticPeriod },
			D = { Length = 3 }
		};

		var subscription = SubscribeCandles(CandleType);
		subscription
			.Bind(_rsi, ProcessCandle)
			.Start();

		var pipSize = Security?.PriceStep ?? PipValue;
		var take = TakeProfitPips > 0m ? new Unit(TakeProfitPips * pipSize, UnitTypes.Absolute) : null;
		var stop = StopLossPips > 0m ? new Unit(StopLossPips * pipSize, UnitTypes.Absolute) : null;

		if (take != null || stop != null)
			StartProtection(takeProfit: take, stopLoss: stop);
	}

	private void ProcessCandle(ICandleMessage candle, decimal rsiDecimal)
	{
		if (candle.State != CandleStates.Finished)
			return;

		// Process other indicators manually.
		var teethResult = _teeth.Process(new DecimalIndicatorValue(_teeth, candle.ClosePrice, candle.OpenTime) { IsFinal = true });
		var jawsResult = _jaws.Process(new DecimalIndicatorValue(_jaws, candle.ClosePrice, candle.OpenTime) { IsFinal = true });
		var lipsResult = _lips.Process(new DecimalIndicatorValue(_lips, candle.ClosePrice, candle.OpenTime) { IsFinal = true });
		var stochResult = _stochastic.Process(new CandleIndicatorValue(_stochastic, candle) { IsFinal = true });

		if (!_teeth.IsFormed || !_jaws.IsFormed || !_lips.IsFormed)
			return;

		var teethBB = (BollingerBandsValue)teethResult;
		var jawsBB = (BollingerBandsValue)jawsResult;
		var lipsBB = (BollingerBandsValue)lipsResult;

		var teethMiddle = teethBB.MovingAverage ?? 0m;
		var teethUpper = teethBB.UpBand ?? 0m;
		var teethLower = teethBB.LowBand ?? 0m;
		var jawsUpper = jawsBB.UpBand ?? 0m;
		var jawsLower = jawsBB.LowBand ?? 0m;
		var lipsUpper = lipsBB.UpBand ?? 0m;
		var lipsLower = lipsBB.LowBand ?? 0m;

		var rsiValue = rsiDecimal;
		var stochTyped = (StochasticOscillatorValue)stochResult;
		var stochasticK = stochTyped.K ?? 50m;

		var rsiReady = !UseRsiFilter || _rsi.IsFormed;
		var stochasticReady = !UseStochasticFilter || _stochastic.IsFormed;

		if (!rsiReady || !stochasticReady)
		{
			UpdateHistory(teethMiddle, teethUpper, teethLower, jawsUpper, jawsLower, lipsUpper, lipsLower, rsiValue, stochasticK);
			return;
		}

		if (!TryGetShifted(_teethMiddleHistory, out var baseTeeth) ||
			!TryGetShifted(_teethUpperHistory, out var upperTeeth) ||
			!TryGetShifted(_teethLowerHistory, out var lowerTeeth) ||
			!TryGetShifted(_jawsUpperHistory, out var upperJaws) ||
			!TryGetShifted(_jawsLowerHistory, out var lowerJaws) ||
			!TryGetShifted(_lipsUpperHistory, out var upperLips) ||
			!TryGetShifted(_lipsLowerHistory, out var lowerLips))
		{
			UpdateHistory(teethMiddle, teethUpper, teethLower, jawsUpper, jawsLower, lipsUpper, lipsLower, rsiValue, stochasticK);
			return;
		}

		decimal rsiShifted = 50m;
		if (UseRsiFilter)
		{
			if (!TryGetShifted(_rsiHistory, out rsiShifted))
			{
				UpdateHistory(teethMiddle, teethUpper, teethLower, jawsUpper, jawsLower, lipsUpper, lipsLower, rsiValue, stochasticK);
				return;
			}
		}

		decimal stochasticShifted = 50m;
		if (UseStochasticFilter)
		{
			if (!TryGetShifted(_stochasticHistory, out stochasticShifted))
			{
				UpdateHistory(teethMiddle, teethUpper, teethLower, jawsUpper, jawsLower, lipsUpper, lipsLower, rsiValue, stochasticK);
				return;
			}
		}

		// All indicators checked above via IsFormed.

		var longEntryPrice = GetLongEntryPrice(lowerTeeth, lowerJaws, lowerLips);
		var shortEntryPrice = GetShortEntryPrice(upperTeeth, upperJaws, upperLips);

		var (exitLong, exitShort) = GetExitLevels(shortEntryPrice, longEntryPrice, upperJaws, lowerJaws, upperLips, lowerLips);

		if (!OnlyOnePosition)
		{
			if (candle.ClosePrice >= baseTeeth)
				_longLocked = false;

			if (candle.ClosePrice <= baseTeeth)
				_shortLocked = false;
		}

		var priceHitLong = candle.LowPrice <= longEntryPrice;
		var priceHitShort = candle.HighPrice >= shortEntryPrice;

		var rsiLongOk = !UseRsiFilter || rsiShifted <= 100m - RsiLowerLevel;
		var rsiShortOk = !UseRsiFilter || rsiShifted >= RsiLowerLevel;

		var stochLongOk = !UseStochasticFilter || stochasticShifted < 100m - StochasticLowerLevel;
		var stochShortOk = !UseStochasticFilter || stochasticShifted > StochasticLowerLevel;

		var canOpenLong = OnlyOnePosition ? Position == 0m : Position >= 0m;
		var canOpenShort = OnlyOnePosition ? Position == 0m : Position <= 0m;

		if (priceHitShort && rsiShortOk && stochShortOk && canOpenShort)
		{
			if (OnlyOnePosition || !_shortLocked)
			{
				// Sell when price reaches the selected upper band zone and filters confirm overbought state.
				SellMarket();
				_shortLocked = !OnlyOnePosition;
			}
		}

		if (priceHitLong && rsiLongOk && stochLongOk && canOpenLong)
		{
			if (OnlyOnePosition || !_longLocked)
			{
				// Buy when price reaches the selected lower band zone and filters confirm oversold state.
				BuyMarket();
				_longLocked = !OnlyOnePosition;
			}
		}

		// Exit logic mirrors the original EA: close longs on selected upper zone, shorts on selected lower zone.
		switch (ClosureMode)
		{
			case BollingerBandsRsiClosureModes.MiddleLine:
				if (Position > 0m && candle.HighPrice >= baseTeeth)
					SellMarket();

				if (Position < 0m && candle.LowPrice <= baseTeeth)
					BuyMarket();
				break;

			case BollingerBandsRsiClosureModes.BetweenYellowAndBlue:
			case BollingerBandsRsiClosureModes.BetweenBlueAndRed:
				if (Position > 0m && candle.HighPrice >= exitLong)
					SellMarket();

				if (Position < 0m && candle.LowPrice <= exitShort)
					BuyMarket();
				break;

			case BollingerBandsRsiClosureModes.YellowLine:
				if (Position > 0m && candle.HighPrice >= upperTeeth)
					SellMarket();

				if (Position < 0m && candle.LowPrice <= lowerTeeth)
					BuyMarket();
				break;

			case BollingerBandsRsiClosureModes.BlueLine:
				if (Position > 0m && candle.HighPrice >= upperJaws)
					SellMarket();

				if (Position < 0m && candle.LowPrice <= lowerJaws)
					BuyMarket();
				break;

			case BollingerBandsRsiClosureModes.RedLine:
				if (Position > 0m && candle.HighPrice >= upperLips)
					SellMarket();

				if (Position < 0m && candle.LowPrice <= lowerLips)
					BuyMarket();
				break;
		}

		UpdateHistory(teethMiddle, teethUpper, teethLower, jawsUpper, jawsLower, lipsUpper, lipsLower, rsiValue, stochasticK);
	}

	private decimal GetLongEntryPrice(decimal lowerTeeth, decimal lowerJaws, decimal lowerLips)
	{
		return EntryMode switch
		{
			BollingerBandsRsiEntryModes.BetweenYellowAndBlue => lowerTeeth - (lowerTeeth - lowerJaws) / 2m,
			BollingerBandsRsiEntryModes.BetweenBlueAndRed => lowerJaws - (lowerJaws - lowerLips) / 2m,
			BollingerBandsRsiEntryModes.YellowLine => lowerTeeth,
			BollingerBandsRsiEntryModes.BlueLine => lowerJaws,
			BollingerBandsRsiEntryModes.RedLine => lowerLips,
			_ => lowerTeeth
		};
	}

	private decimal GetShortEntryPrice(decimal upperTeeth, decimal upperJaws, decimal upperLips)
	{
		return EntryMode switch
		{
			BollingerBandsRsiEntryModes.BetweenYellowAndBlue => upperTeeth + (upperJaws - upperTeeth) / 2m,
			BollingerBandsRsiEntryModes.BetweenBlueAndRed => upperJaws + (upperLips - upperJaws) / 2m,
			BollingerBandsRsiEntryModes.YellowLine => upperTeeth,
			BollingerBandsRsiEntryModes.BlueLine => upperJaws,
			BollingerBandsRsiEntryModes.RedLine => upperLips,
			_ => upperTeeth
		};
	}

	private (decimal exitLong, decimal exitShort) GetExitLevels(decimal shortEntryPrice, decimal longEntryPrice, decimal upperJaws, decimal lowerJaws, decimal upperLips, decimal lowerLips)
	{
		if ((ClosureMode == BollingerBandsRsiClosureModes.BetweenYellowAndBlue && EntryMode == BollingerBandsRsiEntryModes.BetweenYellowAndBlue) ||
			(ClosureMode == BollingerBandsRsiClosureModes.BetweenBlueAndRed && EntryMode == BollingerBandsRsiEntryModes.BetweenBlueAndRed))
		{
			return (shortEntryPrice, longEntryPrice);
		}

		var defaultLong = upperJaws + (upperLips - upperJaws) / 2m;
		var defaultShort = lowerJaws - (lowerJaws - lowerLips) / 2m;
		return (defaultLong, defaultShort);
	}

	private bool TryGetShifted(List<decimal> history, out decimal value)
	{
		if (BarShift <= 0)
		{
			value = 0m;
			return false;
		}

		if (history.Count < BarShift)
		{
			value = 0m;
			return false;
		}

		value = history[0];
		return true;
	}

	private void UpdateHistory(
		decimal teethMiddle,
		decimal teethUpper,
		decimal teethLower,
		decimal jawsUpper,
		decimal jawsLower,
		decimal lipsUpper,
		decimal lipsLower,
		decimal rsiValue,
		decimal stochasticK)
	{
		if (BarShift <= 0)
			return;

		Enqueue(_teethMiddleHistory, teethMiddle);
		Enqueue(_teethUpperHistory, teethUpper);
		Enqueue(_teethLowerHistory, teethLower);
		Enqueue(_jawsUpperHistory, jawsUpper);
		Enqueue(_jawsLowerHistory, jawsLower);
		Enqueue(_lipsUpperHistory, lipsUpper);
		Enqueue(_lipsLowerHistory, lipsLower);

		if (_rsi.IsFormed)
			Enqueue(_rsiHistory, rsiValue);

		if (_stochastic.IsFormed)
			Enqueue(_stochasticHistory, stochasticK);
	}

	private void Enqueue(List<decimal> history, decimal value)
	{
		history.Add(value);

		while (history.Count > BarShift)
			try { history.RemoveAt(0); } catch { }
	}
}