using System.Buffers; using System.Runtime.CompilerServices; using System.Runtime.InteropServices; namespace QuanTAlib; /// /// AccBands: Acceleration Bands /// /// /// Acceleration Bands are a volatility-based channel indicator developed by Price Headley. /// They create an adaptive price envelope around a moving average, with band width determined /// by the spread between the high and low moving averages multiplied by a factor. /// /// Calculation: /// Middle Band = SMA(Close, Period) /// BandWidth = [SMA(High, Period) - SMA(Low, Period)] × Factor /// Upper Band = SMA(High, Period) + BandWidth /// Lower Band = SMA(Low, Period) - BandWidth /// /// Key characteristics: /// - Bands expand during volatile periods and contract during consolidation /// - Uses SMA of High, Low, and Close for calculations /// - Factor parameter controls band sensitivity /// /// Sources: /// Headley, P. (2002). Big Trends in Trading. John Wiley & Sons. /// [SkipLocalsInit] public sealed class AccBands : ITValuePublisher, IDisposable { private readonly int _period; private readonly double _factor; private readonly RingBuffer _highBuffer; private readonly RingBuffer _lowBuffer; private readonly RingBuffer _closeBuffer; private readonly TBarPublishedHandler _barHandler; private TBarSeries? _source; private bool _disposed; private const int ResyncInterval = 1000; [StructLayout(LayoutKind.Auto)] private record struct State( double SumHigh, double SumLow, double SumClose, double LastValidHigh, double LastValidLow, double LastValidClose, int TickCount ); private State _state; private State _p_state; /// /// 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. /// 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 => _closeBuffer.IsFull; /// /// Event triggered when a new TValue is available. /// public event TValuePublishedHandler? Pub; /// /// Creates AccBands with specified period and factor. /// /// Lookback period for SMA calculations (must be > 0) /// Multiplier for band width (must be > 0, default: 2.0) public AccBands(int period, double factor = 2.0) { if (period <= 0) throw new ArgumentException("Period must be greater than 0", nameof(period)); if (factor <= 0) throw new ArgumentException("Factor must be greater than 0", nameof(factor)); _period = period; _factor = factor; _highBuffer = new RingBuffer(period); _lowBuffer = new RingBuffer(period); _closeBuffer = new RingBuffer(period); Name = $"AccBands({period},{factor:F2})"; WarmupPeriod = period; _barHandler = HandleBar; } /// /// Creates AccBands with TBarSeries source. /// public AccBands(TBarSeries source, int period, double factor = 2.0) : this(period, factor) { _source = source; Prime(source); source.Pub += _barHandler; } /// /// Releases resources and unsubscribes from the source event. /// public void Dispose() { if (_disposed) return; _disposed = true; if (_source != null) { _source.Pub -= _barHandler; _source = null; } } private void HandleBar(object? sender, in TBarEventArgs e) => Update(e.Value, e.IsNew); /// /// Helper to invoke the Pub event. /// [MethodImpl(MethodImplOptions.AggressiveInlining)] private void PubEvent(TValue value, bool isNew = true) { 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 GetValidHigh(double input) { if (double.IsFinite(input)) { _state.LastValidHigh = input; return input; } return _state.LastValidHigh; } [MethodImpl(MethodImplOptions.AggressiveInlining)] private double GetValidLow(double input) { if (double.IsFinite(input)) { _state.LastValidLow = input; return input; } return _state.LastValidLow; } [MethodImpl(MethodImplOptions.AggressiveInlining)] private double GetValidClose(double input) { if (double.IsFinite(input)) { _state.LastValidClose = input; return input; } return _state.LastValidClose; } [MethodImpl(MethodImplOptions.AggressiveInlining)] private void UpdateState(double high, double low, double close) { double removedHigh = _highBuffer.Count == _highBuffer.Capacity ? _highBuffer.Oldest : 0.0; double removedLow = _lowBuffer.Count == _lowBuffer.Capacity ? _lowBuffer.Oldest : 0.0; double removedClose = _closeBuffer.Count == _closeBuffer.Capacity ? _closeBuffer.Oldest : 0.0; _state.SumHigh = _state.SumHigh - removedHigh + high; _state.SumLow = _state.SumLow - removedLow + low; _state.SumClose = _state.SumClose - removedClose + close; _highBuffer.Add(high); _lowBuffer.Add(low); _closeBuffer.Add(close); _state.TickCount++; if (_closeBuffer.IsFull && _state.TickCount >= ResyncInterval) { _state.TickCount = 0; _state.SumHigh = _highBuffer.RecalculateSum(); _state.SumLow = _lowBuffer.RecalculateSum(); _state.SumClose = _closeBuffer.RecalculateSum(); } } /// /// Updates the indicator with a TBar input. /// [MethodImpl(MethodImplOptions.AggressiveInlining)] public TValue Update(TBar input, bool isNew = true) { if (isNew) { _p_state = _state; double high = GetValidHigh(input.High); double low = GetValidLow(input.Low); double close = GetValidClose(input.Close); UpdateState(high, low, close); } else { _state = _p_state; double high = GetValidHigh(input.High); double low = GetValidLow(input.Low); double close = GetValidClose(input.Close); _highBuffer.UpdateNewest(high); _lowBuffer.UpdateNewest(low); _closeBuffer.UpdateNewest(close); _state = _state with { SumHigh = _highBuffer.Sum, SumLow = _lowBuffer.Sum, SumClose = _closeBuffer.Sum, }; } int count = _closeBuffer.Count; if (count == 0) { Last = new TValue(input.Time, double.NaN); Upper = new TValue(input.Time, double.NaN); Lower = new TValue(input.Time, double.NaN); } else { double smaHigh = _state.SumHigh / count; double smaLow = _state.SumLow / count; double smaClose = _state.SumClose / count; double bandWidth = (smaHigh - smaLow) * _factor; Last = new TValue(input.Time, smaClose); Upper = new TValue(input.Time, smaHigh + bandWidth); Lower = new TValue(input.Time, smaLow - bandWidth); } PubEvent(Last, isNew); return Last; } /// /// Updates the indicator with a TBarSeries. /// public (TSeries Middle, TSeries Upper, TSeries Lower) Update(TBarSeries 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.HighValues, source.LowValues, source.CloseValues, vMiddleSpan, vUpperSpan, vLowerSpan, _period, _factor); 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)); } /// /// Initializes the indicator state using the provided TBarSeries history. /// // skipcq: CS-R1140 public void Prime(TBarSeries source) { if (source.Count == 0) return; // Reset state _highBuffer.Clear(); _lowBuffer.Clear(); _closeBuffer.Clear(); _state = default; _p_state = default; int warmupLength = Math.Min(source.Count, WarmupPeriod); int startIndex = source.Count - warmupLength; // Seed LastValidValue _state.LastValidHigh = double.NaN; _state.LastValidLow = double.NaN; _state.LastValidClose = double.NaN; for (int i = startIndex - 1; i >= 0; i--) { var bar = source[i]; if (double.IsFinite(bar.High) && double.IsNaN(_state.LastValidHigh)) _state.LastValidHigh = bar.High; if (double.IsFinite(bar.Low) && double.IsNaN(_state.LastValidLow)) _state.LastValidLow = bar.Low; if (double.IsFinite(bar.Close) && double.IsNaN(_state.LastValidClose)) _state.LastValidClose = bar.Close; if (!double.IsNaN(_state.LastValidHigh) && !double.IsNaN(_state.LastValidLow) && !double.IsNaN(_state.LastValidClose)) break; } // Find valid values in warmup window if not found if (double.IsNaN(_state.LastValidHigh) || double.IsNaN(_state.LastValidLow) || double.IsNaN(_state.LastValidClose)) { for (int i = startIndex; i < source.Count; i++) { var bar = source[i]; if (double.IsFinite(bar.High) && double.IsNaN(_state.LastValidHigh)) _state.LastValidHigh = bar.High; if (double.IsFinite(bar.Low) && double.IsNaN(_state.LastValidLow)) _state.LastValidLow = bar.Low; if (double.IsFinite(bar.Close) && double.IsNaN(_state.LastValidClose)) _state.LastValidClose = bar.Close; if (!double.IsNaN(_state.LastValidHigh) && !double.IsNaN(_state.LastValidLow) && !double.IsNaN(_state.LastValidClose)) break; } } // Feed the buffers for (int i = startIndex; i < source.Count; i++) { var bar = source[i]; double high = GetValidHigh(bar.High); double low = GetValidLow(bar.Low); double close = GetValidClose(bar.Close); UpdateState(high, low, close); } // Finalize state int count = _closeBuffer.Count; if (count > 0) { var lastBar = source.Last; double smaHigh = _state.SumHigh / count; double smaLow = _state.SumLow / count; double smaClose = _state.SumClose / count; double bandWidth = (smaHigh - smaLow) * _factor; Last = new TValue(lastBar.Time, smaClose); Upper = new TValue(lastBar.Time, smaHigh + bandWidth); Lower = new TValue(lastBar.Time, smaLow - bandWidth); } _p_state = _state; } /// /// Resets the indicator state. /// public void Reset() { _highBuffer.Clear(); _lowBuffer.Clear(); _closeBuffer.Clear(); _state = new State( SumHigh: 0, SumLow: 0, SumClose: 0, LastValidHigh: double.NaN, LastValidLow: double.NaN, LastValidClose: double.NaN, TickCount: 0 ); _p_state = _state; Last = default; Upper = default; Lower = default; } ///////////////////////////////////////////////////////////////////////////////////////////////// // Static Batch Methods ///////////////////////////////////////////////////////////////////////////////////////////////// /// /// Output buffers for batch AccBands calculation. /// /// /// Public Span fields are intentional: ref structs cannot use auto-properties with Span<T> /// and direct field access provides optimal performance for this high-throughput API. /// [StructLayout(LayoutKind.Auto)] #pragma warning disable S1104 // Fields should not have public accessibility public ref struct BatchOutputs { /// Output middle band (SMA of close) public Span Middle; /// Output upper band public Span Upper; /// Output lower band public 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; } } /// /// Input buffers for batch AccBands calculation. /// [StructLayout(LayoutKind.Auto)] #pragma warning disable S1104 // Fields should not have public accessibility public ref struct BatchInputs { /// High price values public ReadOnlySpan High; /// Low price values public ReadOnlySpan Low; /// Close price values public ReadOnlySpan Close; #pragma warning restore S1104 /// /// Creates a new BatchInputs instance. /// public BatchInputs(ReadOnlySpan high, ReadOnlySpan low, ReadOnlySpan close) { High = high; Low = low; Close = close; } } /// /// Internal state for scalar calculation. /// [StructLayout(LayoutKind.Auto)] private ref struct ScalarState { public double SumHigh; public double SumLow; public double SumClose; public double LastValidHigh; public double LastValidLow; public double LastValidClose; public int BufferIndex; public int TickCount; } /// /// Working buffers for batch calculation. /// [StructLayout(LayoutKind.Auto)] private readonly ref struct WorkBuffers(Span high, Span low, Span close) { public readonly Span High = high; public readonly Span Low = low; public readonly Span Close = close; } /// /// Calculates AccBands for the entire TBarSeries using a new instance. /// public static (TSeries Middle, TSeries Upper, TSeries Lower) Batch(TBarSeries source, int period, double factor = 2.0) { var accBands = new AccBands(period, factor); return accBands.Update(source); } /// /// Calculates AccBands in-place using spans for maximum performance. /// Zero-allocation method. /// /// Input buffers for high, low, and close prices /// Output buffers for middle, upper, and lower bands /// Lookback period /// Band width factor [MethodImpl(MethodImplOptions.AggressiveInlining)] public static void Batch( BatchInputs inputs, BatchOutputs outputs, int period, double factor = 2.0) { Batch(inputs.High, inputs.Low, inputs.Close, outputs.Middle, outputs.Upper, outputs.Lower, period, factor); } /// /// Calculates AccBands in-place using spans for maximum performance. /// Zero-allocation method. /// /// High price values /// Low price values /// Close price values /// Output buffers for middle, upper, and lower bands /// Lookback period /// Band width factor [MethodImpl(MethodImplOptions.AggressiveInlining)] public static void Batch( ReadOnlySpan high, ReadOnlySpan low, ReadOnlySpan close, BatchOutputs outputs, int period, double factor = 2.0) { Batch(high, low, close, outputs.Middle, outputs.Upper, outputs.Lower, period, factor); } /// /// Calculates AccBands in-place using spans for maximum performance. /// Zero-allocation method. /// /// High price values /// Low price values /// Close price values /// Output middle band (SMA of close) /// Output upper band /// Output lower band /// Lookback period /// Band width factor // Suppressing S107: This is a high-performance batch API where callers benefit from // direct span parameters. A BatchOutputs overload exists for callers preferring fewer parameters. #pragma warning disable S107 [MethodImpl(MethodImplOptions.AggressiveInlining)] public static void Batch( ReadOnlySpan high, ReadOnlySpan low, ReadOnlySpan close, Span middle, Span upper, Span lower, int period, double factor = 2.0) #pragma warning restore S107 { int len = close.Length; if (high.Length != len || low.Length != len) throw new ArgumentException("High, Low, and Close must have the same length", nameof(high)); 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 ArgumentException("Period must be greater than 0", nameof(period)); if (factor <= 0) throw new ArgumentException("Factor must be greater than 0", nameof(factor)); if (len == 0) return; // Scalar implementation with NaN handling var inputs = new BatchInputs(high, low, close); var outputs = new BatchOutputs(middle, upper, lower); CalculateScalarCore(inputs, outputs, period, factor); } [MethodImpl(MethodImplOptions.AggressiveInlining)] private static void CalculateScalarCore( scoped BatchInputs inputs, scoped BatchOutputs outputs, int period, double factor) { int len = inputs.Close.Length; // Always use ArrayPool to avoid span scope safety issues with stackalloc + ref structs double[] rentedHigh = ArrayPool.Shared.Rent(period); double[] rentedLow = ArrayPool.Shared.Rent(period); double[] rentedClose = ArrayPool.Shared.Rent(period); try { var buffers = new WorkBuffers( rentedHigh.AsSpan(0, period), rentedLow.AsSpan(0, period), rentedClose.AsSpan(0, period)); var state = new ScalarState { LastValidHigh = double.NaN, LastValidLow = double.NaN, LastValidClose = double.NaN, }; SeedFirstValidValues(inputs, ref state); int warmupEnd = Math.Min(period, len); ProcessWarmupPhase(inputs, outputs, warmupEnd, factor, ref buffers, ref state); ProcessMainLoop(inputs, outputs, warmupEnd, period, factor, ref buffers, ref state); } finally { ArrayPool.Shared.Return(rentedHigh); ArrayPool.Shared.Return(rentedLow); ArrayPool.Shared.Return(rentedClose); } } [MethodImpl(MethodImplOptions.AggressiveInlining)] private static void SeedFirstValidValues(scoped BatchInputs inputs, ref ScalarState state) { int len = inputs.Close.Length; for (int k = 0; k < len; k++) { if (double.IsFinite(inputs.High[k]) && double.IsNaN(state.LastValidHigh)) state.LastValidHigh = inputs.High[k]; if (double.IsFinite(inputs.Low[k]) && double.IsNaN(state.LastValidLow)) state.LastValidLow = inputs.Low[k]; if (double.IsFinite(inputs.Close[k]) && double.IsNaN(state.LastValidClose)) state.LastValidClose = inputs.Close[k]; if (!double.IsNaN(state.LastValidHigh) && !double.IsNaN(state.LastValidLow) && !double.IsNaN(state.LastValidClose)) break; } } [MethodImpl(MethodImplOptions.AggressiveInlining)] private static (double h, double l, double c) GetValidHLC(scoped BatchInputs inputs, int i, ref ScalarState state) { double h = inputs.High[i]; double l = inputs.Low[i]; double c = inputs.Close[i]; if (double.IsFinite(h)) state.LastValidHigh = h; else h = state.LastValidHigh; if (double.IsFinite(l)) state.LastValidLow = l; else l = state.LastValidLow; if (double.IsFinite(c)) state.LastValidClose = c; else c = state.LastValidClose; return (h, l, c); } [MethodImpl(MethodImplOptions.AggressiveInlining)] private static void WriteBandOutputs(scoped BatchOutputs outputs, int i, double smaHigh, double smaLow, double smaClose, double factor) { double bandWidth = (smaHigh - smaLow) * factor; outputs.Middle[i] = smaClose; outputs.Upper[i] = smaHigh + bandWidth; outputs.Lower[i] = smaLow - bandWidth; } [MethodImpl(MethodImplOptions.AggressiveInlining)] private static void ProcessWarmupPhase( scoped BatchInputs inputs, scoped BatchOutputs outputs, int warmupEnd, double factor, ref WorkBuffers buffers, ref ScalarState state) { for (int i = 0; i < warmupEnd; i++) { var (h, l, c) = GetValidHLC(inputs, i, ref state); state.SumHigh += h; state.SumLow += l; state.SumClose += c; buffers.High[i] = h; buffers.Low[i] = l; buffers.Close[i] = c; int count = i + 1; WriteBandOutputs(outputs, i, state.SumHigh / count, state.SumLow / count, state.SumClose / count, factor); } } [MethodImpl(MethodImplOptions.AggressiveInlining)] private static void ProcessMainLoop( scoped BatchInputs inputs, scoped BatchOutputs outputs, int startIndex, int period, double factor, ref WorkBuffers buffers, ref ScalarState state) { int len = inputs.Close.Length; for (int i = startIndex; i < len; i++) { var (h, l, c) = GetValidHLC(inputs, i, ref state); state.SumHigh = state.SumHigh - buffers.High[state.BufferIndex] + h; state.SumLow = state.SumLow - buffers.Low[state.BufferIndex] + l; state.SumClose = state.SumClose - buffers.Close[state.BufferIndex] + c; buffers.High[state.BufferIndex] = h; buffers.Low[state.BufferIndex] = l; buffers.Close[state.BufferIndex] = c; state.BufferIndex++; if (state.BufferIndex >= period) state.BufferIndex = 0; WriteBandOutputs(outputs, i, state.SumHigh / period, state.SumLow / period, state.SumClose / period, factor); state.TickCount++; if (state.TickCount >= ResyncInterval) { ResyncSums(period, ref buffers, ref state); } } } [MethodImpl(MethodImplOptions.AggressiveInlining)] private static void ResyncSums(int period, ref WorkBuffers buffers, ref ScalarState state) { state.TickCount = 0; ReadOnlySpan highSpan = buffers.High[..period]; ReadOnlySpan lowSpan = buffers.Low[..period]; ReadOnlySpan closeSpan = buffers.Close[..period]; state.SumHigh = highSpan.SumSIMD(); state.SumLow = lowSpan.SumSIMD(); state.SumClose = closeSpan.SumSIMD(); } /// /// Runs a high-performance batch calculation and returns a "Hot" AccBands instance. /// public static ((TSeries Middle, TSeries Upper, TSeries Lower) Results, AccBands Indicator) Calculate(TBarSeries source, int period, double factor = 2.0) { var accBands = new AccBands(period, factor); var results = accBands.Update(source); return (results, accBands); } }