GitHub で見る

Bollinger Bands RSI ゾーン戦略

MetaTrader エキスパート「Bollinger Bands RSI」から変換されたマルチバンドのBollinger Bandsブレイクアウトシステムです。同一の期間だが異なる偏差を持つ3つのBollingerエンベロープを導出して「黄色」、「青」、「赤」のバンドを作成します。これらのバンド周辺の設定可能なゾーンに価格が再度到達したときに注文が発動され、オプションでRSIとストキャスティクスのフィルターによる確認が行われます。

戦略ロジック

  • 主要(黄色)バンドは設定された偏差乗数を使用します。
  • 青バンドは偏差を半分にし、より狭いエンベロープを作成します。
  • 赤バンドは偏差を2倍にし、広い外側エンベロープを生成します。
  • RSIとストキャスティクスの値は、元のEAの動作に合わせて前の完成したローソク足(Bar Shift)で評価されます。
  • Only One Positionは、ネットポジションがフラットのときのみ新しい注文が許可されるか、またはBollingerミドルラインに価格が戻ったときに追加のスケールトレードが許可されるかを制御します。

エントリー条件

ロングエントリー

  1. 現在のローソク足の価格が選択されたロングエントリーゾーン(Entry Mode)以下に下落する:
    • 黄と青の中間点、青と赤の中間点、または個々のバンドのいずれか。
  2. オプションの確認:
    • RSIフィルター:RSI ≤ 100 - RSI Lower
    • ストキャスティクスフィルター:%K < 100 - Stochastic Lower
  3. ポジション前提条件:
    • Only One Positionが有効の場合、ネットポジションはフラットでなければならない。
    • そうでない場合、追加のロング注文はEAのロックロジックをエミュレートするため、ローソク足がミドル(黄色)バンドより上で閉じるまでブロックされます。

ショートエントリー

  1. 現在のローソク足の価格が選択されたショートエントリーゾーン(ロングオプションを反映)以上に上昇する。
  2. オプションの確認:
    • RSIフィルター:RSI ≥ RSI Lower
    • ストキャスティクスフィルター:%K > Stochastic Lower
  3. ポジション前提条件はロングロジックを反映(単一取引モードのフラットポジション、またはローソク足がミドルバンドより下で閉じた後の解除状態)。

エグジット条件

  • 決済モードは Closure Mode によって決定されます:
    • Middle Line:ロングはBollingerミドルバンドに価格が達したときに決済;ショートは上から触れたときに決済。
    • Between Yellow and Blue / Between Blue and Red:エントリーに使用されたのと同じ中間点で決済;エントリーモードが異なる場合は青と赤の中間点をデフォルトとする。
    • Yellow Line, Blue Line, Red Line:対応する上部/下部バンドへの直接タッチで決済。
  • スケーリングモードのロックフラグは、ローソク足がミドルバンドの反対側で閉じると自動的にリセットされ、EAの動作を再現します。

リスク管理

  • Stop LossTake Profitパラメーターはpipsで表現され、StartProtectionが初期化されるときにPip Valueを通じて絶対価格距離に変換されます。
  • ストップとターゲットはオプションです;それぞれの保護レグを無効にするには距離をゼロのままにします。
  • 取引ボリュームはOrder Volumeによって定義され、すべての市場注文に適用されます。

パラメーター

名前 説明 デフォルト値
Entry Mode エントリーを引き起こすBollingerゾーンを選択します。 黄と青の間
Closure Mode 利食いバンドまたは中間点を定義します。 青と赤の間
Bands Period すべてのBollinger Bandsで共有されるピリオド長。 140
Deviation 黄バンドの標準偏差乗数(青は半分、赤は2倍)。 2.0
Use RSI Filter RSI確認ロジックを有効にします。 false
RSI Period RSI平均化期間。 8
RSI Lower 買われすぎしきい値;売られすぎは100 - 値を使用。 70
Use Stochastic Filter %K確認ロジックを有効にします。 true
Stochastic Period 主要%Kルックバック期間(平滑化は3/3 SMAで固定)。 20
Stochastic Lower 買われすぎしきい値;売られすぎは100 - 値を使用。 95
Bar Shift インジケーター値のためのルックバックの完了バー数。 1
Only One Position 有効の場合、アクティブなポジションがないときのみ新しいトレードを開きます。 true
Order Volume 各市場注文で送信されるボリューム。 1
Pip Value ストップ/ターゲット変換のための1pipsの絶対価格値。 0.0001
Stop Loss pipsでの保護ストップ距離(0で無効)。 200
Take Profit pipsでの保護ターゲット距離(0で無効)。 200
Candle Type 計算に使用されるデータタイプ(デフォルト1分ローソク足)。 1m 時間軸

注意事項

  • 戦略は完了したローソク足のみを処理するため、未完成のバーを参照しないようBar Shiftは≥1のままにしてください。
  • RSIとストキャスティクスフィルターは%Kラインを使用します;%Dラインは計算されますが使用されず、元のEA実装を反映しています。
  • 変換はコメントとシグナル名を英語で保持し、StockSharp高レベルAPIガイドライン(Bindベースのインジケーターパイプライン、手動バッファアクセスなし)に従います。
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 { }
	}
}