using System.Buffers; using System.Diagnostics.CodeAnalysis; using System.Numerics; using System.Runtime.CompilerServices; using System.Runtime.InteropServices; namespace QuanTAlib; /// /// Aberr: Aberration Bands /// /// /// Aberration Bands measure price deviation from a central moving average using absolute /// deviation rather than standard deviation. This approach provides more intuitive and /// outlier-resistant bands compared to Bollinger Bands. /// /// Calculation: /// Middle Band = SMA(Source, Period) /// Deviation = |Source - Middle| /// Average Deviation = SMA(Deviation, Period) /// Upper Band = Middle + (Multiplier x Average Deviation) /// Lower Band = Middle - (Multiplier x Average Deviation) /// /// Key characteristics: /// - Uses absolute deviation instead of standard deviation /// - Less sensitive to extreme outliers than Bollinger Bands /// - Provides intuitive measure of typical price dispersion /// - Bands expand during volatile periods and contract during consolidation /// /// Sources: /// Pine Script implementation: https://github.com/mihakralj/pinescript/blob/main/indicators/channels/aberr.pine /// [SkipLocalsInit] public sealed class Aberr : ITValuePublisher, IDisposable { private readonly int _period; private readonly double _multiplier; private readonly RingBuffer _sourceBuffer; private readonly RingBuffer _deviationBuffer; private readonly TValuePublishedHandler _handler; private ITValuePublisher? _source; private bool _disposed; [StructLayout(LayoutKind.Auto)] private record struct State( double SumSource, double SumDeviation, double SumSourceComp, double SumDeviationComp, double LastValidValue ); private State _state; private State _pState; /// /// Display name for the indicator. /// public string Name { get; } /// /// Number of periods before the indicator is considered "hot" (valid). /// public int WarmupPeriod { get; } /// /// Current middle band value (SMA of source). /// public TValue Last { get; private set; } /// /// Current upper band value. /// public TValue Upper { get; private set; } /// /// Current lower band value. /// public TValue Lower { get; private set; } /// /// True if the indicator has enough data to produce valid results. /// public bool IsHot => _sourceBuffer.IsFull; /// /// Event triggered when a new TValue is available. /// public event TValuePublishedHandler? Pub; /// /// Creates Aberr with specified period and multiplier. /// /// Lookback period for SMA and deviation calculations (must be > 0) /// Multiplier for band width (must be > 0, default: 2.0) public Aberr(int period, double multiplier = 2.0) { if (period <= 0) { throw new ArgumentOutOfRangeException(nameof(period), "Period must be greater than 0"); } if (multiplier <= 0) { throw new ArgumentOutOfRangeException(nameof(multiplier), "Multiplier must be greater than 0"); } _period = period; _multiplier = multiplier; _sourceBuffer = new RingBuffer(period); _deviationBuffer = new RingBuffer(period); Name = $"Aberr({period},{multiplier:F2})"; WarmupPeriod = period; _handler = HandleValue; } /// /// Creates Aberr with TSeries source. /// public Aberr(TSeries source, int period, double multiplier = 2.0) : this(period, multiplier) { _source = source ?? throw new ArgumentNullException(nameof(source)); Prime(source); _source.Pub += _handler; } /// /// Creates Aberr with ITValuePublisher source. /// public Aberr(ITValuePublisher source, int period, double multiplier = 2.0) : this(period, multiplier) { _source = source ?? throw new ArgumentNullException(nameof(source)); _source.Pub += _handler; } private void HandleValue(object? _, in TValueEventArgs e) => Update(e.Value, e.IsNew); /// /// Helper to invoke the Pub event. /// [MethodImpl(MethodImplOptions.AggressiveInlining)] private void PubEvent(TValue value, bool isNew) { Pub?.Invoke(this, new TValueEventArgs { Value = value, IsNew = isNew }); } /// /// Gets a valid input value, using last-value substitution for non-finite inputs. /// [MethodImpl(MethodImplOptions.AggressiveInlining)] private double GetValidValue(double input) { if (double.IsFinite(input)) { _state.LastValidValue = input; return input; } return _state.LastValidValue; } [MethodImpl(MethodImplOptions.AggressiveInlining)] private void UpdateState(double value, double deviation) { double removedSource = _sourceBuffer.Count == _sourceBuffer.Capacity ? _sourceBuffer.Oldest : 0.0; double removedDeviation = _deviationBuffer.Count == _deviationBuffer.Capacity ? _deviationBuffer.Oldest : 0.0; // Kahan compensated summation for SumSource double srcDelta = value - removedSource - _state.SumSourceComp; double srcNewSum = _state.SumSource + srcDelta; _state.SumSourceComp = (srcNewSum - _state.SumSource) - srcDelta; _state.SumSource = srcNewSum; // Kahan compensated summation for SumDeviation double devDelta = deviation - removedDeviation - _state.SumDeviationComp; double devNewSum = _state.SumDeviation + devDelta; _state.SumDeviationComp = (devNewSum - _state.SumDeviation) - devDelta; _state.SumDeviation = devNewSum; _sourceBuffer.Add(value); _deviationBuffer.Add(deviation); } /// /// Updates the indicator with a TValue input. /// [MethodImpl(MethodImplOptions.AggressiveInlining)] public TValue Update(TValue input, bool isNew = true) { double value = GetValidValue(input.Value); if (isNew) { _pState = _state; // Compute SMA including the new value with correct divisor (matches batch ProcessMainLoop) int count = _sourceBuffer.Count; double removedSource = count == _sourceBuffer.Capacity ? _sourceBuffer.Oldest : 0.0; double newSum = _state.SumSource - removedSource + value; int newCount = count < _sourceBuffer.Capacity ? count + 1 : count; double sma = newSum / newCount; double deviation = Math.Abs(value - sma); UpdateState(value, deviation); } else { _state = _pState; // Replace newest source value and recompute sum for current-bar SMA (matches Pine) _sourceBuffer.UpdateNewest(value); double currentSum = _sourceBuffer.Sum; int corrCount = _sourceBuffer.Count; double corrSma = corrCount > 0 ? currentSum / corrCount : value; double corrDeviation = Math.Abs(value - corrSma); _deviationBuffer.UpdateNewest(corrDeviation); _state = _state with { SumSource = currentSum, SumDeviation = _deviationBuffer.Sum, SumSourceComp = 0, SumDeviationComp = 0, }; } int currentCount = _sourceBuffer.Count; if (currentCount == 0) { Last = new TValue(input.Time, double.NaN); Upper = new TValue(input.Time, double.NaN); Lower = new TValue(input.Time, double.NaN); } else { double middle = _state.SumSource / currentCount; double avgDeviation = _state.SumDeviation / currentCount; double bandWidth = _multiplier * avgDeviation; Last = new TValue(input.Time, middle); Upper = new TValue(input.Time, middle + bandWidth); Lower = new TValue(input.Time, middle - bandWidth); } PubEvent(Last, isNew); return Last; } /// /// Updates the indicator with a TSeries. /// public (TSeries Middle, TSeries Upper, TSeries Lower) Update(TSeries source) { if (source.Count == 0) { return (new TSeries([], []), new TSeries([], []), new TSeries([], [])); } int len = source.Count; var tMiddle = new List(len); var vMiddle = new List(len); var tUpper = new List(len); var vUpper = new List(len); var tLower = new List(len); var vLower = new List(len); CollectionsMarshal.SetCount(tMiddle, len); CollectionsMarshal.SetCount(vMiddle, len); CollectionsMarshal.SetCount(tUpper, len); CollectionsMarshal.SetCount(vUpper, len); CollectionsMarshal.SetCount(tLower, len); CollectionsMarshal.SetCount(vLower, len); var tSpan = CollectionsMarshal.AsSpan(tMiddle); var vMiddleSpan = CollectionsMarshal.AsSpan(vMiddle); var vUpperSpan = CollectionsMarshal.AsSpan(vUpper); var vLowerSpan = CollectionsMarshal.AsSpan(vLower); // Use batch calculation Batch(source.Values, vMiddleSpan, vUpperSpan, vLowerSpan, _period, _multiplier); source.Times.CopyTo(tSpan); // Copy timestamps to upper and lower (same time series) tSpan.CopyTo(CollectionsMarshal.AsSpan(tUpper)); tSpan.CopyTo(CollectionsMarshal.AsSpan(tLower)); // Prime the state for continued streaming Prime(source); return (new TSeries(tMiddle, vMiddle), new TSeries(tUpper, vUpper), new TSeries(tLower, vLower)); } [MethodImpl(MethodImplOptions.AggressiveInlining)] private static void ResyncSums(int period, ref WorkBuffers buffers, ref ScalarState state) { state.TickCount = 0; if (Vector.IsHardwareAccelerated && period >= Vector.Count) { ReadOnlySpan sourceSpan = buffers.Source; ReadOnlySpan deviationSpan = buffers.Deviation; state.SumSource = sourceSpan.SumSIMD(); state.SumDeviation = deviationSpan.SumSIMD(); } else { double recalcSumSource = 0; double recalcSumDeviation = 0; for (int k = 0; k < period; k++) { recalcSumSource += buffers.Source[k]; recalcSumDeviation += buffers.Deviation[k]; } state.SumSource = recalcSumSource; state.SumDeviation = recalcSumDeviation; } } /// /// Initializes the indicator state using the provided TSeries history. /// public void Prime(TSeries source) { if (source.Count == 0) { return; } // Reset state _sourceBuffer.Clear(); _deviationBuffer.Clear(); _state = default; _pState = default; int warmupLength = Math.Min(source.Count, WarmupPeriod); int startIndex = source.Count - warmupLength; // Seed LastValidValue _state.LastValidValue = double.NaN; for (int i = startIndex - 1; i >= 0; i--) { if (double.IsFinite(source[i].Value)) { _state.LastValidValue = source[i].Value; break; } } // Find valid value in warmup window if not found if (double.IsNaN(_state.LastValidValue)) { for (int i = startIndex; i < source.Count; i++) { if (double.IsFinite(source[i].Value)) { _state.LastValidValue = source[i].Value; break; } } } // Feed the buffers for (int i = startIndex; i < source.Count; i++) { double value = GetValidValue(source[i].Value); // Compute SMA including the new value with correct divisor (matches batch ProcessMainLoop) int count = _sourceBuffer.Count; double removedSource = count == _sourceBuffer.Capacity ? _sourceBuffer.Oldest : 0.0; double newSum = _state.SumSource - removedSource + value; int newCount = count < _sourceBuffer.Capacity ? count + 1 : count; double sma = newSum / newCount; double deviation = Math.Abs(value - sma); UpdateState(value, deviation); } // Finalize state int currentCount = _sourceBuffer.Count; if (currentCount > 0) { var lastItem = source.Last; double middle = _state.SumSource / currentCount; double avgDeviation = _state.SumDeviation / currentCount; double bandWidth = _multiplier * avgDeviation; Last = new TValue(lastItem.Time, middle); Upper = new TValue(lastItem.Time, middle + bandWidth); Lower = new TValue(lastItem.Time, middle - bandWidth); } _pState = _state; } /// /// Resets the indicator state. /// public void Reset() { _sourceBuffer.Clear(); _deviationBuffer.Clear(); _state = default; _pState = default; Last = default; Upper = default; Lower = default; } ///////////////////////////////////////////////////////////////////////////////////////////////// // Static Batch Methods ///////////////////////////////////////////////////////////////////////////////////////////////// /// /// Output buffers for batch Aberr calculation. /// #pragma warning disable MA0077 // ref struct cannot implement interfaces [StructLayout(LayoutKind.Auto)] #pragma warning disable S1104 // Fields should not have public accessibility public readonly ref struct BatchOutputs { /// Output middle band (SMA of source) public readonly Span Middle; /// Output upper band public readonly Span Upper; /// Output lower band public readonly Span Lower; #pragma warning restore S1104 /// /// Creates a new BatchOutputs instance. /// public BatchOutputs(Span middle, Span upper, Span lower) { Middle = middle; Upper = upper; Lower = lower; } public bool Equals(BatchOutputs other) => Unsafe.AreSame(ref MemoryMarshal.GetReference(Middle), ref MemoryMarshal.GetReference(other.Middle)) && Middle.Length == other.Middle.Length && Unsafe.AreSame(ref MemoryMarshal.GetReference(Upper), ref MemoryMarshal.GetReference(other.Upper)) && Upper.Length == other.Upper.Length && Unsafe.AreSame(ref MemoryMarshal.GetReference(Lower), ref MemoryMarshal.GetReference(other.Lower)) && Lower.Length == other.Lower.Length; public override bool Equals(object? obj) => false; public override int GetHashCode() => HashCode.Combine(Middle.Length, Upper.Length, Lower.Length); public static bool operator ==(BatchOutputs left, BatchOutputs right) => left.Equals(right); public static bool operator !=(BatchOutputs left, BatchOutputs right) => !left.Equals(right); } #pragma warning restore MA0077 /// /// Resync interval for batch path only (streaming uses Kahan compensation). /// private const int ResyncInterval = 1000; /// /// Internal state for scalar calculation. /// #pragma warning disable MA0077 // ref struct cannot implement interfaces [StructLayout(LayoutKind.Auto)] [SuppressMessage("NDepend", "ND1903:StructuresShouldBeImmutable", Justification = "Mutable calculation state accumulator by design")] private ref struct ScalarState { internal double SumSource; internal double SumDeviation; internal double LastValidValue; internal int BufferIndex; internal int TickCount; public bool Equals(ScalarState other) => SumSource.Equals(other.SumSource) && SumDeviation.Equals(other.SumDeviation) && LastValidValue.Equals(other.LastValidValue) && BufferIndex == other.BufferIndex && TickCount == other.TickCount; public override bool Equals(object? obj) => false; public override int GetHashCode() => HashCode.Combine(SumSource, SumDeviation, LastValidValue, BufferIndex, TickCount); public static bool operator ==(ScalarState left, ScalarState right) => left.Equals(right); public static bool operator !=(ScalarState left, ScalarState right) => !left.Equals(right); } #pragma warning restore MA0077 /// /// Working buffers for batch calculation. /// #pragma warning disable MA0077 // ref struct cannot implement interfaces [StructLayout(LayoutKind.Auto)] private readonly ref struct WorkBuffers(Span source, Span deviation) { internal readonly Span Source = source; internal readonly Span Deviation = deviation; public bool Equals(WorkBuffers other) => Unsafe.AreSame(ref MemoryMarshal.GetReference(Source), ref MemoryMarshal.GetReference(other.Source)) && Source.Length == other.Source.Length && Unsafe.AreSame(ref MemoryMarshal.GetReference(Deviation), ref MemoryMarshal.GetReference(other.Deviation)) && Deviation.Length == other.Deviation.Length; public override bool Equals(object? obj) => false; public override int GetHashCode() => HashCode.Combine(Source.Length, Deviation.Length); public static bool operator ==(WorkBuffers left, WorkBuffers right) => left.Equals(right); public static bool operator !=(WorkBuffers left, WorkBuffers right) => !left.Equals(right); } #pragma warning restore MA0077 /// /// Calculates Aberr for the entire TSeries using a new instance. /// public static (TSeries Middle, TSeries Upper, TSeries Lower) Batch(TSeries source, int period, double multiplier = 2.0) { var aberr = new Aberr(period, multiplier); return aberr.Update(source); } /// /// Calculates Aberr in-place using spans for maximum performance. /// Zero-allocation method. /// /// Source price values /// Output buffers for middle, upper, and lower bands /// Lookback period /// Band width multiplier [MethodImpl(MethodImplOptions.AggressiveInlining)] public static void Batch( ReadOnlySpan source, BatchOutputs outputs, int period, double multiplier = 2.0) { Batch(source, outputs.Middle, outputs.Upper, outputs.Lower, period, multiplier); } /// /// Calculates Aberr in-place using spans for maximum performance. /// Zero-allocation method. /// /// Source price values /// Output middle band (SMA of source) /// Output upper band /// Output lower band /// Lookback period /// Band width multiplier [MethodImpl(MethodImplOptions.AggressiveInlining)] public static void Batch( ReadOnlySpan source, Span middle, Span upper, Span lower, int period, double multiplier = 2.0) { int len = source.Length; if (middle.Length < len || upper.Length < len || lower.Length < len) { throw new ArgumentException("Output buffers must be at least as long as input", nameof(middle)); } if (period <= 0) { throw new ArgumentOutOfRangeException(nameof(period), "Period must be greater than 0"); } if (multiplier <= 0) { throw new ArgumentOutOfRangeException(nameof(multiplier), "Multiplier must be greater than 0"); } if (len == 0) { return; } // Scalar implementation with NaN handling var outputs = new BatchOutputs(middle, upper, lower); CalculateScalarCore(source, outputs, period, multiplier); } [MethodImpl(MethodImplOptions.AggressiveInlining)] private static void CalculateScalarCore( ReadOnlySpan source, scoped BatchOutputs outputs, int period, double multiplier) { int len = source.Length; // Always use ArrayPool to avoid span scope safety issues with stackalloc + ref structs double[] rentedSource = ArrayPool.Shared.Rent(period); double[] rentedDeviation = ArrayPool.Shared.Rent(period); try { var buffers = new WorkBuffers( rentedSource.AsSpan(0, period), rentedDeviation.AsSpan(0, period)); var state = new ScalarState { LastValidValue = double.NaN, }; SeedFirstValidValue(source, ref state); int warmupEnd = Math.Min(period, len); ProcessWarmupPhase(source, outputs, warmupEnd, multiplier, ref buffers, ref state); ProcessMainLoop(source, outputs, warmupEnd, period, multiplier, ref buffers, ref state); } finally { ArrayPool.Shared.Return(rentedSource); ArrayPool.Shared.Return(rentedDeviation); } } [MethodImpl(MethodImplOptions.AggressiveInlining)] private static void SeedFirstValidValue(ReadOnlySpan source, ref ScalarState state) { int len = source.Length; for (int k = 0; k < len; k++) { if (double.IsFinite(source[k])) { state.LastValidValue = source[k]; break; } } } [MethodImpl(MethodImplOptions.AggressiveInlining)] private static double GetValidValue(ReadOnlySpan source, int i, ref ScalarState state) { double v = source[i]; if (double.IsFinite(v)) { state.LastValidValue = v; return v; } return state.LastValidValue; } [MethodImpl(MethodImplOptions.AggressiveInlining)] private static void WriteBandOutputs(scoped BatchOutputs outputs, int i, double middle, double avgDeviation, double multiplier) { double bandWidth = multiplier * avgDeviation; outputs.Middle[i] = middle; outputs.Upper[i] = middle + bandWidth; outputs.Lower[i] = middle - bandWidth; } [MethodImpl(MethodImplOptions.AggressiveInlining)] private static void ProcessWarmupPhase( ReadOnlySpan source, scoped BatchOutputs outputs, int warmupEnd, double multiplier, ref WorkBuffers buffers, ref ScalarState state) { for (int i = 0; i < warmupEnd; i++) { double v = GetValidValue(source, i, ref state); // Compute SMA including the new value to get same-bar deviation (matches Pine) int newCount = i + 1; double newSum = state.SumSource + v; double sma = newSum / newCount; double deviation = Math.Abs(v - sma); state.SumSource = newSum; state.SumDeviation += deviation; buffers.Source[i] = v; buffers.Deviation[i] = deviation; double middle = state.SumSource / newCount; double avgDeviation = state.SumDeviation / newCount; WriteBandOutputs(outputs, i, middle, avgDeviation, multiplier); } } [MethodImpl(MethodImplOptions.AggressiveInlining)] private static void ProcessMainLoop( ReadOnlySpan source, scoped BatchOutputs outputs, int startIndex, int period, double multiplier, ref WorkBuffers buffers, ref ScalarState state) { int len = source.Length; for (int i = startIndex; i < len; i++) { double v = GetValidValue(source, i, ref state); // Compute SMA including the new value to get same-bar deviation (matches Pine) double newSumSource = state.SumSource - buffers.Source[state.BufferIndex] + v; double sma = newSumSource / period; double deviation = Math.Abs(v - sma); // Update running sums using single buffer index state.SumSource = newSumSource; buffers.Source[state.BufferIndex] = v; state.SumDeviation = state.SumDeviation - buffers.Deviation[state.BufferIndex] + deviation; buffers.Deviation[state.BufferIndex] = deviation; state.BufferIndex++; if (state.BufferIndex >= period) { state.BufferIndex = 0; } double middle = state.SumSource / period; double avgDeviation = state.SumDeviation / period; WriteBandOutputs(outputs, i, middle, avgDeviation, multiplier); state.TickCount++; if (state.TickCount >= ResyncInterval) { ResyncSums(period, ref buffers, ref state); } } } /// /// Runs a high-performance batch calculation and returns a "Hot" Aberr instance. /// public static ((TSeries Middle, TSeries Upper, TSeries Lower) Results, Aberr Indicator) Calculate(TSeries source, int period, double multiplier = 2.0) { var aberr = new Aberr(period, multiplier); var results = aberr.Update(source); return (results, aberr); } /// /// Disposes the Aberr instance, unsubscribing from the source publisher. /// This method is idempotent. /// public void Dispose() { if (!_disposed) { if (_source != null) { _source.Pub -= _handler; _source = null; } _disposed = true; } GC.SuppressFinalize(this); } }