mirror of
https://github.com/mihakralj/QuanTAlib.git
synced 2026-07-27 17:27:43 +00:00
67ad6f0cba
Comprehensive refactor across all indicators replacing the periodic ResyncInterval-based drift correction (every 1000 ticks recalculate from scratch) with Kahan compensated summation for running sums. Key changes: - Remove ResyncInterval constants and TickCount fields from all State records - Add Kahan compensation fields (SumComp, SumSqComp, etc.) to State records - Replace naive sum += val - removed with Kahan delta pattern - Remove Resync()/RecalculateSum() methods that did O(N) recalculation - Update batch/SIMD paths to use Kahan compensation instead of resync loops - IIR filters (EMA, REMA, RGMA) simplified: inherently self-correcting - Version bump to 0.8.7 - Build system: README version stamping via Directory.Build.props - Minor doc/test tolerance adjustments for new numerical characteristics Affected modules: channels, core, cycles, dynamics, errors, momentum, oscillators, statistics, trends_FIR, trends_IIR, volatility, volume
688 lines
20 KiB
C#
688 lines
20 KiB
C#
using System.Runtime.CompilerServices;
|
|
using System.Runtime.InteropServices;
|
|
|
|
namespace QuanTAlib;
|
|
|
|
/// <summary>
|
|
/// BBS: Bollinger Band Squeeze
|
|
/// </summary>
|
|
/// <remarks>
|
|
/// <para>
|
|
/// Detects when Bollinger Bands contract inside Keltner Channels,
|
|
/// indicating low volatility consolidation that typically precedes breakouts.
|
|
/// </para>
|
|
///
|
|
/// Squeeze Detection:
|
|
/// <c>SqueezeOn = BB_Upper < KC_Upper AND BB_Lower > KC_Lower</c>
|
|
///
|
|
/// Bandwidth Output:
|
|
/// <c>Bandwidth = ((BB_Upper - BB_Lower) / BB_Middle) * 100</c>
|
|
///
|
|
/// Bollinger Bands:
|
|
/// <c>BB_Middle = SMA(close, bbPeriod)</c>
|
|
/// <c>BB_Dev = sqrt(E[x^2] - E[x]^2)</c>
|
|
/// <c>BB_Upper = BB_Middle + bbMult * BB_Dev</c>
|
|
/// <c>BB_Lower = BB_Middle - bbMult * BB_Dev</c>
|
|
///
|
|
/// Keltner Channels:
|
|
/// <c>KC_Middle = SMA(close, kcPeriod)</c>
|
|
/// <c>ATR = EMA-smoothed True Range with warmup compensation</c>
|
|
/// <c>KC_Upper = KC_Middle + kcMult * ATR</c>
|
|
/// <c>KC_Lower = KC_Middle - kcMult * ATR</c>
|
|
///
|
|
/// References:
|
|
/// - John Bollinger, "Bollinger on Bollinger Bands"
|
|
/// - PineScript reference: bbs.pine
|
|
/// </remarks>
|
|
[SkipLocalsInit]
|
|
public sealed class Bbs : ITValuePublisher
|
|
{
|
|
private readonly int _bbPeriod;
|
|
private readonly double _bbMult;
|
|
private readonly int _kcPeriod;
|
|
private readonly double _kcMult;
|
|
|
|
// Bollinger Bands: rolling sum/sumSq for O(1) SMA + stddev
|
|
private readonly RingBuffer _bbBuffer;
|
|
|
|
// Keltner Channel: rolling sum for SMA middle
|
|
private readonly RingBuffer _kcBuffer;
|
|
|
|
[StructLayout(LayoutKind.Auto)]
|
|
private record struct State(
|
|
double BbSum,
|
|
double BbSumSq,
|
|
double KcSum,
|
|
double BbSumComp,
|
|
double BbSumSqComp,
|
|
double KcSumComp,
|
|
double AtrRaw,
|
|
double AtrE,
|
|
double PrevClose,
|
|
double LastValidClose,
|
|
double LastValidHigh,
|
|
double LastValidLow,
|
|
int Bars,
|
|
bool IsHot);
|
|
|
|
private State _state;
|
|
private State _p_state;
|
|
|
|
// Saved squeeze state for SqueezeFired detection
|
|
private bool _prevSqueezeOn;
|
|
private bool _p_prevSqueezeOn;
|
|
|
|
/// <summary>
|
|
/// Display name for the indicator.
|
|
/// </summary>
|
|
public string Name { get; }
|
|
|
|
/// <summary>
|
|
/// Event publisher for value updates.
|
|
/// </summary>
|
|
public event TValuePublishedHandler? Pub;
|
|
|
|
/// <summary>
|
|
/// The bandwidth value: ((BB_Upper - BB_Lower) / BB_Middle) * 100.
|
|
/// Primary numeric output.
|
|
/// </summary>
|
|
public TValue Last { get; private set; }
|
|
|
|
/// <summary>
|
|
/// True when Bollinger Bands are inside Keltner Channel (squeeze condition).
|
|
/// </summary>
|
|
public bool SqueezeOn { get; private set; }
|
|
|
|
/// <summary>
|
|
/// True when squeeze just ended (first bar where squeeze transitions Off).
|
|
/// </summary>
|
|
public bool SqueezeFired { get; private set; }
|
|
|
|
/// <summary>
|
|
/// True when indicator has enough data for valid output.
|
|
/// </summary>
|
|
public bool IsHot => _state.IsHot;
|
|
|
|
/// <summary>
|
|
/// Number of bars required for warmup.
|
|
/// </summary>
|
|
public int WarmupPeriod { get; }
|
|
|
|
/// <summary>
|
|
/// Bollinger Band period.
|
|
/// </summary>
|
|
public int BbPeriod => _bbPeriod;
|
|
|
|
/// <summary>
|
|
/// Bollinger Band standard deviation multiplier.
|
|
/// </summary>
|
|
public double BbMult => _bbMult;
|
|
|
|
/// <summary>
|
|
/// Keltner Channel period.
|
|
/// </summary>
|
|
public int KcPeriod => _kcPeriod;
|
|
|
|
/// <summary>
|
|
/// Keltner Channel ATR multiplier.
|
|
/// </summary>
|
|
public double KcMult => _kcMult;
|
|
|
|
/// <summary>
|
|
/// Creates BBS indicator with specified parameters.
|
|
/// </summary>
|
|
/// <param name="bbPeriod">Bollinger Band period (default 20, must be > 0)</param>
|
|
/// <param name="bbMult">Bollinger Band standard deviation multiplier (default 2.0, must be > 0)</param>
|
|
/// <param name="kcPeriod">Keltner Channel period (default 20, must be > 0)</param>
|
|
/// <param name="kcMult">Keltner Channel ATR multiplier (default 1.5, must be > 0)</param>
|
|
public Bbs(int bbPeriod = 20, double bbMult = 2.0, int kcPeriod = 20, double kcMult = 1.5)
|
|
{
|
|
if (bbPeriod <= 0)
|
|
{
|
|
throw new ArgumentException("BB Period must be greater than 0", nameof(bbPeriod));
|
|
}
|
|
|
|
if (kcPeriod <= 0)
|
|
{
|
|
throw new ArgumentException("KC Period must be greater than 0", nameof(kcPeriod));
|
|
}
|
|
|
|
if (bbMult <= 0)
|
|
{
|
|
throw new ArgumentException("BB Multiplier must be greater than 0", nameof(bbMult));
|
|
}
|
|
|
|
if (kcMult <= 0)
|
|
{
|
|
throw new ArgumentException("KC Multiplier must be greater than 0", nameof(kcMult));
|
|
}
|
|
|
|
_bbPeriod = bbPeriod;
|
|
_bbMult = bbMult;
|
|
_kcPeriod = kcPeriod;
|
|
_kcMult = kcMult;
|
|
|
|
Name = $"Bbs({bbPeriod},{bbMult:F1},{kcPeriod},{kcMult:F1})";
|
|
WarmupPeriod = Math.Max(bbPeriod, kcPeriod);
|
|
|
|
_bbBuffer = new RingBuffer(bbPeriod);
|
|
_kcBuffer = new RingBuffer(kcPeriod);
|
|
|
|
_state = new State(0, 0, 0, 0, 0, 0, 0, 1.0, double.NaN, double.NaN, double.NaN, double.NaN, 0, false);
|
|
_p_state = _state;
|
|
}
|
|
|
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
|
private void PubEvent(TValue value, bool isNew = true) =>
|
|
Pub?.Invoke(this, new TValueEventArgs { Value = value, IsNew = isNew });
|
|
|
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
|
private (double close, double high, double low) GetValidValues(double close, double high, double low)
|
|
{
|
|
if (double.IsFinite(close))
|
|
{
|
|
_state = _state with { LastValidClose = close };
|
|
}
|
|
else if (double.IsFinite(_state.LastValidClose))
|
|
{
|
|
close = _state.LastValidClose;
|
|
}
|
|
else
|
|
{
|
|
close = 0.0;
|
|
}
|
|
|
|
if (double.IsFinite(high))
|
|
{
|
|
_state = _state with { LastValidHigh = high };
|
|
}
|
|
else if (double.IsFinite(_state.LastValidHigh))
|
|
{
|
|
high = _state.LastValidHigh;
|
|
}
|
|
else
|
|
{
|
|
high = close;
|
|
}
|
|
|
|
if (double.IsFinite(low))
|
|
{
|
|
_state = _state with { LastValidLow = low };
|
|
}
|
|
else if (double.IsFinite(_state.LastValidLow))
|
|
{
|
|
low = _state.LastValidLow;
|
|
}
|
|
else
|
|
{
|
|
low = close;
|
|
}
|
|
|
|
return (close, high, low);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Updates the BBS indicator with a new bar.
|
|
/// </summary>
|
|
/// <param name="input">The price bar (requires OHLC)</param>
|
|
/// <param name="isNew">True for new bar, false for update of current bar</param>
|
|
/// <returns>The bandwidth value</returns>
|
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
|
public TValue Update(TBar input, bool isNew = true)
|
|
{
|
|
if (isNew)
|
|
{
|
|
_p_state = _state;
|
|
_p_prevSqueezeOn = _prevSqueezeOn;
|
|
}
|
|
else
|
|
{
|
|
_state = _p_state;
|
|
_prevSqueezeOn = _p_prevSqueezeOn;
|
|
}
|
|
|
|
var (close, high, low) = GetValidValues(input.Close, input.High, input.Low);
|
|
|
|
if (isNew)
|
|
{
|
|
_state = _state with { Bars = _state.Bars + 1 };
|
|
}
|
|
|
|
// === Bollinger Bands: Kahan compensated SMA + population stddev ===
|
|
if (_bbBuffer.IsFull)
|
|
{
|
|
double oldest = _bbBuffer.Oldest;
|
|
double bbDelta = close - oldest;
|
|
double bbSqDelta = (close * close) - (oldest * oldest);
|
|
{
|
|
double y = bbDelta - _state.BbSumComp;
|
|
double t = _state.BbSum + y;
|
|
double newComp = (t - _state.BbSum) - y;
|
|
double y2 = bbSqDelta - _state.BbSumSqComp;
|
|
double t2 = _state.BbSumSq + y2;
|
|
double newSqComp = (t2 - _state.BbSumSq) - y2;
|
|
_state = _state with
|
|
{
|
|
BbSum = t,
|
|
BbSumComp = newComp,
|
|
BbSumSq = t2,
|
|
BbSumSqComp = newSqComp
|
|
};
|
|
}
|
|
}
|
|
else
|
|
{
|
|
double y = close - _state.BbSumComp;
|
|
double t = _state.BbSum + y;
|
|
double newComp = (t - _state.BbSum) - y;
|
|
double y2 = (close * close) - _state.BbSumSqComp;
|
|
double t2 = _state.BbSumSq + y2;
|
|
double newSqComp = (t2 - _state.BbSumSq) - y2;
|
|
_state = _state with
|
|
{
|
|
BbSum = t,
|
|
BbSumComp = newComp,
|
|
BbSumSq = t2,
|
|
BbSumSqComp = newSqComp
|
|
};
|
|
}
|
|
|
|
_bbBuffer.Add(close, isNew);
|
|
|
|
int bbCount = _bbBuffer.Count;
|
|
double bbMean = bbCount > 0 ? _state.BbSum / bbCount : close;
|
|
double bbVariance = Math.Max(0.0, (_state.BbSumSq / bbCount) - (bbMean * bbMean));
|
|
double bbStdDev = Math.Sqrt(bbVariance);
|
|
|
|
double bbUpper = bbMean + (_bbMult * bbStdDev);
|
|
double bbLower = bbMean - (_bbMult * bbStdDev);
|
|
|
|
// === Keltner Channel: Kahan compensated SMA middle + EMA-smoothed ATR ===
|
|
if (_kcBuffer.IsFull)
|
|
{
|
|
double oldest = _kcBuffer.Oldest;
|
|
double kcDelta = close - oldest;
|
|
double y = kcDelta - _state.KcSumComp;
|
|
double t = _state.KcSum + y;
|
|
_state = _state with { KcSum = t, KcSumComp = (t - _state.KcSum) - y };
|
|
// Fix: need to use pre-update KcSum for comp calc
|
|
}
|
|
else
|
|
{
|
|
double y = close - _state.KcSumComp;
|
|
double t = _state.KcSum + y;
|
|
_state = _state with { KcSum = t, KcSumComp = (t - _state.KcSum) - y };
|
|
}
|
|
|
|
_kcBuffer.Add(close, isNew);
|
|
|
|
int kcCount = _kcBuffer.Count;
|
|
double kcMid = kcCount > 0 ? _state.KcSum / kcCount : close;
|
|
|
|
// True Range
|
|
double tr = high - low;
|
|
if (double.IsFinite(_state.PrevClose))
|
|
{
|
|
tr = Math.Max(tr, Math.Max(Math.Abs(high - _state.PrevClose), Math.Abs(low - _state.PrevClose)));
|
|
}
|
|
|
|
_state = _state with { PrevClose = close };
|
|
|
|
// ATR using EMA smoothing with warmup compensation (matching Pine spec)
|
|
double atrAlpha = 2.0 / (_kcPeriod + 1);
|
|
double atrBeta = 1.0 - atrAlpha;
|
|
|
|
double newAtrRaw = Math.FusedMultiplyAdd(_state.AtrRaw, atrBeta, atrAlpha * tr);
|
|
double newAtrE = _state.AtrE * atrBeta;
|
|
|
|
double atr;
|
|
if (newAtrE > 1e-10)
|
|
{
|
|
atr = newAtrRaw / (1.0 - newAtrE);
|
|
}
|
|
else
|
|
{
|
|
atr = newAtrRaw;
|
|
}
|
|
|
|
_state = _state with { AtrRaw = newAtrRaw, AtrE = newAtrE };
|
|
|
|
double kcUpper = kcMid + (_kcMult * atr);
|
|
double kcLower = kcMid - (_kcMult * atr);
|
|
|
|
// === Squeeze Detection ===
|
|
bool wasSqueezeOn = _prevSqueezeOn;
|
|
bool squeezeOn = bbUpper < kcUpper && bbLower > kcLower;
|
|
SqueezeOn = squeezeOn;
|
|
SqueezeFired = wasSqueezeOn && !squeezeOn;
|
|
_prevSqueezeOn = squeezeOn;
|
|
|
|
// === Bandwidth ===
|
|
double bandwidth = bbMean != 0.0 ? ((bbUpper - bbLower) / bbMean) * 100.0 : 0.0; // skipcq: CS-R1077 - Exact-zero div guard: price avg
|
|
|
|
|
|
// === IsHot ===
|
|
if (!_state.IsHot && _state.Bars >= WarmupPeriod)
|
|
{
|
|
_state = _state with { IsHot = true };
|
|
}
|
|
|
|
Last = new TValue(input.Time, bandwidth);
|
|
PubEvent(Last, isNew);
|
|
return Last;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Calculates BBS for the entire bar series.
|
|
/// </summary>
|
|
public TSeries Update(TBarSeries source)
|
|
{
|
|
if (source.Count == 0)
|
|
{
|
|
return new TSeries([], []);
|
|
}
|
|
|
|
int len = source.Count;
|
|
var tList = new List<long>(len);
|
|
var vList = new List<double>(len);
|
|
CollectionsMarshal.SetCount(tList, len);
|
|
CollectionsMarshal.SetCount(vList, len);
|
|
|
|
var tSpan = CollectionsMarshal.AsSpan(tList);
|
|
var vSpan = CollectionsMarshal.AsSpan(vList);
|
|
|
|
Batch(source.HighValues, source.LowValues, source.CloseValues,
|
|
vSpan, _bbPeriod, _bbMult);
|
|
source.Times.CopyTo(tSpan);
|
|
|
|
// Prime internal state for continued streaming
|
|
Prime(source);
|
|
|
|
return new TSeries(tList, vList);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Primes the indicator with historical bar data.
|
|
/// </summary>
|
|
public void Prime(TBarSeries source)
|
|
{
|
|
Reset();
|
|
for (int i = 0; i < source.Count; i++)
|
|
{
|
|
Update(source[i], isNew: true);
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Calculates BBS for the entire bar series using default parameters.
|
|
/// </summary>
|
|
public static TSeries Batch(TBarSeries source)
|
|
{
|
|
var bbs = new Bbs();
|
|
return bbs.Update(source);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Calculates BBS for the entire bar series using custom parameters.
|
|
/// </summary>
|
|
public static TSeries Batch(TBarSeries source, int bbPeriod, double bbMult, int kcPeriod, double kcMult)
|
|
{
|
|
var bbs = new Bbs(bbPeriod, bbMult, kcPeriod, kcMult);
|
|
return bbs.Update(source);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Batch BBS calculation using spans (zero allocation hot path).
|
|
/// Outputs bandwidth values.
|
|
/// </summary>
|
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
|
public static void Batch(
|
|
ReadOnlySpan<double> high,
|
|
ReadOnlySpan<double> low,
|
|
ReadOnlySpan<double> close,
|
|
Span<double> output,
|
|
int bbPeriod = 20,
|
|
double bbMult = 2.0)
|
|
{
|
|
if (bbPeriod <= 0)
|
|
{
|
|
throw new ArgumentException("BB Period must be greater than 0", nameof(bbPeriod));
|
|
}
|
|
|
|
if (bbMult <= 0)
|
|
{
|
|
throw new ArgumentException("BB Multiplier must be greater than 0", nameof(bbMult));
|
|
}
|
|
|
|
if (high.Length != low.Length || high.Length != close.Length)
|
|
{
|
|
throw new ArgumentException("High, Low, and Close spans must have the same length", nameof(high));
|
|
}
|
|
|
|
if (output.Length < high.Length)
|
|
{
|
|
throw new ArgumentException("Output span must be at least as long as inputs", nameof(output));
|
|
}
|
|
|
|
int len = high.Length;
|
|
if (len == 0)
|
|
{
|
|
return;
|
|
}
|
|
|
|
// BB rolling state
|
|
var bbRing = new RingBuffer(bbPeriod);
|
|
double bbSum = 0.0;
|
|
double bbSumSq = 0.0;
|
|
|
|
for (int i = 0; i < len; i++)
|
|
{
|
|
double c = close[i];
|
|
|
|
// === Bollinger Bands ===
|
|
if (bbRing.IsFull)
|
|
{
|
|
double oldest = bbRing.Oldest;
|
|
bbSum -= oldest;
|
|
bbSumSq -= oldest * oldest;
|
|
}
|
|
|
|
bbSum += c;
|
|
bbSumSq += c * c;
|
|
bbRing.Add(c);
|
|
|
|
int bbCount = bbRing.Count;
|
|
double bbMean = bbSum / bbCount;
|
|
double bbVariance = Math.Max(0.0, (bbSumSq / bbCount) - (bbMean * bbMean));
|
|
double bbStdDev = Math.Sqrt(bbVariance);
|
|
double bbUpper = bbMean + (bbMult * bbStdDev);
|
|
double bbLower = bbMean - (bbMult * bbStdDev);
|
|
|
|
// === Bandwidth ===
|
|
// Note: bandwidth only depends on BB, not KC. KC state not needed for this overload.
|
|
double bandwidth = bbMean != 0.0 ? ((bbUpper - bbLower) / bbMean) * 100.0 : 0.0;
|
|
output[i] = bandwidth;
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Batch BBS calculation returning squeeze detection array alongside bandwidth.
|
|
/// </summary>
|
|
public static void Batch(
|
|
ReadOnlySpan<double> high,
|
|
ReadOnlySpan<double> low,
|
|
ReadOnlySpan<double> close,
|
|
Span<double> bandwidth,
|
|
Span<bool> squeezeOn,
|
|
int bbPeriod = 20,
|
|
double bbMult = 2.0,
|
|
int kcPeriod = 20,
|
|
double kcMult = 1.5)
|
|
{
|
|
if (bbPeriod <= 0)
|
|
{
|
|
throw new ArgumentException("BB Period must be greater than 0", nameof(bbPeriod));
|
|
}
|
|
|
|
if (kcPeriod <= 0)
|
|
{
|
|
throw new ArgumentException("KC Period must be greater than 0", nameof(kcPeriod));
|
|
}
|
|
|
|
if (bbMult <= 0)
|
|
{
|
|
throw new ArgumentException("BB Multiplier must be greater than 0", nameof(bbMult));
|
|
}
|
|
|
|
if (kcMult <= 0)
|
|
{
|
|
throw new ArgumentException("KC Multiplier must be greater than 0", nameof(kcMult));
|
|
}
|
|
|
|
if (high.Length != low.Length || high.Length != close.Length)
|
|
{
|
|
throw new ArgumentException("High, Low, and Close spans must have the same length", nameof(high));
|
|
}
|
|
|
|
if (bandwidth.Length < high.Length || squeezeOn.Length < high.Length)
|
|
{
|
|
throw new ArgumentException("Output spans must be at least as long as inputs", nameof(bandwidth));
|
|
}
|
|
|
|
int len = high.Length;
|
|
if (len == 0)
|
|
{
|
|
return;
|
|
}
|
|
|
|
// BB rolling state
|
|
var bbRing = new RingBuffer(bbPeriod);
|
|
double bbSum = 0.0;
|
|
double bbSumSq = 0.0;
|
|
|
|
// KC rolling state
|
|
var kcRing = new RingBuffer(kcPeriod);
|
|
double kcSum = 0.0;
|
|
|
|
// ATR EMA state
|
|
double atrAlpha = 2.0 / (kcPeriod + 1);
|
|
double atrBeta = 1.0 - atrAlpha;
|
|
double atrRaw = 0.0;
|
|
double atrE = 1.0;
|
|
double prevClose = close[0];
|
|
|
|
for (int i = 0; i < len; i++)
|
|
{
|
|
double c = close[i];
|
|
double h = high[i];
|
|
double l = low[i];
|
|
|
|
// === Bollinger Bands ===
|
|
if (bbRing.IsFull)
|
|
{
|
|
double oldest = bbRing.Oldest;
|
|
bbSum -= oldest;
|
|
bbSumSq -= oldest * oldest;
|
|
}
|
|
|
|
bbSum += c;
|
|
bbSumSq += c * c;
|
|
bbRing.Add(c);
|
|
|
|
int bbCount = bbRing.Count;
|
|
double bbMean = bbSum / bbCount;
|
|
double bbVariance = Math.Max(0.0, (bbSumSq / bbCount) - (bbMean * bbMean));
|
|
double bbStdDev = Math.Sqrt(bbVariance);
|
|
double bbUpper = bbMean + (bbMult * bbStdDev);
|
|
double bbLower = bbMean - (bbMult * bbStdDev);
|
|
|
|
// === Keltner Channel ===
|
|
if (kcRing.IsFull)
|
|
{
|
|
double oldest = kcRing.Oldest;
|
|
kcSum -= oldest;
|
|
}
|
|
|
|
kcSum += c;
|
|
kcRing.Add(c);
|
|
|
|
int kcCount = kcRing.Count;
|
|
double kcMid = kcSum / kcCount;
|
|
|
|
// True Range
|
|
double tr = h - l;
|
|
if (i > 0)
|
|
{
|
|
tr = Math.Max(tr, Math.Max(Math.Abs(h - prevClose), Math.Abs(l - prevClose)));
|
|
}
|
|
|
|
prevClose = c;
|
|
|
|
// ATR (EMA with warmup compensation)
|
|
atrRaw = Math.FusedMultiplyAdd(atrRaw, atrBeta, atrAlpha * tr);
|
|
atrE *= atrBeta;
|
|
double atr = atrE > 1e-10 ? atrRaw / (1.0 - atrE) : atrRaw;
|
|
|
|
double kcUpper = kcMid + (kcMult * atr);
|
|
double kcLower = kcMid - (kcMult * atr);
|
|
|
|
// Squeeze
|
|
squeezeOn[i] = bbUpper < kcUpper && bbLower > kcLower;
|
|
|
|
// Bandwidth
|
|
bandwidth[i] = bbMean != 0.0 ? ((bbUpper - bbLower) / bbMean) * 100.0 : 0.0; // skipcq: CS-R1077 - Exact-zero div guard: price avg
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Calculates BBS and returns both results and the warm indicator.
|
|
/// </summary>
|
|
public static (TSeries Results, Bbs Indicator) Calculate(TBarSeries source,
|
|
int bbPeriod = 20, double bbMult = 2.0, int kcPeriod = 20, double kcMult = 1.5)
|
|
{
|
|
var indicator = new Bbs(bbPeriod, bbMult, kcPeriod, kcMult);
|
|
var results = indicator.Update(source);
|
|
return (results, indicator);
|
|
}
|
|
|
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
|
private void RecalculateSums()
|
|
{
|
|
double bbSum = 0.0;
|
|
double bbSumSq = 0.0;
|
|
for (int i = 0; i < _bbBuffer.Count; i++)
|
|
{
|
|
double v = _bbBuffer[i];
|
|
bbSum += v;
|
|
bbSumSq += v * v;
|
|
}
|
|
|
|
double kcSum = 0.0;
|
|
for (int i = 0; i < _kcBuffer.Count; i++)
|
|
{
|
|
kcSum += _kcBuffer[i];
|
|
}
|
|
|
|
_state = _state with { BbSum = bbSum, BbSumSq = bbSumSq, KcSum = kcSum, BbSumComp = 0, BbSumSqComp = 0, KcSumComp = 0 };
|
|
}
|
|
|
|
/// <summary>
|
|
/// Resets the indicator state.
|
|
/// </summary>
|
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
|
public void Reset()
|
|
{
|
|
_bbBuffer.Clear();
|
|
_kcBuffer.Clear();
|
|
|
|
_state = new State(0, 0, 0, 0, 0, 0, 0, 1.0, double.NaN, double.NaN, double.NaN, double.NaN, 0, false);
|
|
_p_state = _state;
|
|
_prevSqueezeOn = false;
|
|
_p_prevSqueezeOn = false;
|
|
|
|
Last = default;
|
|
SqueezeOn = false;
|
|
SqueezeFired = false;
|
|
}
|
|
}
|