using System.Buffers; using System.Diagnostics.CodeAnalysis; 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, where the band width /// is determined by the per-bar normalized range applied before averaging. /// /// Calculation (Headley's original formula): /// w = (High - Low) / (High + Low) // normalized range width per bar /// Upper Band = SMA(High × (1 + factor × w), Period) /// Lower Band = SMA(Low × (1 - factor × w), Period) /// Middle Band = SMA(Close, Period) /// /// Key characteristics: /// - Width adjustment is applied per bar before averaging (Headley's method) /// - Bands expand during volatile periods and contract during consolidation /// - Factor parameter (default 4.0) 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 _adjHighBuffer; private readonly RingBuffer _adjLowBuffer; private readonly RingBuffer _closeBuffer; private readonly TBarPublishedHandler _barHandler; private TBarSeries? _source; private bool _disposed; [StructLayout(LayoutKind.Auto)] private record struct State( double SumAdjHigh, double SumAdjLow, double SumClose, double SumAdjHighComp, double SumAdjLowComp, double SumCloseComp, double LastValidHigh, double LastValidLow, double LastValidClose ); 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 normalized width (must be > 0, default: 4.0 per Headley) public AccBands(int period, double factor = 4.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; _adjHighBuffer = new RingBuffer(period); _adjLowBuffer = 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 = 4.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; } /// /// Computes Headley's per-bar adjusted values and updates running sums. /// [MethodImpl(MethodImplOptions.AggressiveInlining)] private void UpdateState(double high, double low, double close) { // Headley's per-bar normalized width double denom = high + low; double w = denom != 0.0 ? (high - low) / denom : 0.0; double adjHigh = high * (1.0 + (_factor * w)); double adjLow = low * (1.0 - (_factor * w)); double removedAdjHigh = _adjHighBuffer.Count == _adjHighBuffer.Capacity ? _adjHighBuffer.Oldest : 0.0; double removedAdjLow = _adjLowBuffer.Count == _adjLowBuffer.Capacity ? _adjLowBuffer.Oldest : 0.0; double removedClose = _closeBuffer.Count == _closeBuffer.Capacity ? _closeBuffer.Oldest : 0.0; // Kahan compensated summation for SumAdjHigh double ahDelta = adjHigh - removedAdjHigh - _state.SumAdjHighComp; double ahNewSum = _state.SumAdjHigh + ahDelta; _state.SumAdjHighComp = (ahNewSum - _state.SumAdjHigh) - ahDelta; _state.SumAdjHigh = ahNewSum; // Kahan compensated summation for SumAdjLow double alDelta = adjLow - removedAdjLow - _state.SumAdjLowComp; double alNewSum = _state.SumAdjLow + alDelta; _state.SumAdjLowComp = (alNewSum - _state.SumAdjLow) - alDelta; _state.SumAdjLow = alNewSum; // Kahan compensated summation for SumClose double clDelta = close - removedClose - _state.SumCloseComp; double clNewSum = _state.SumClose + clDelta; _state.SumCloseComp = (clNewSum - _state.SumClose) - clDelta; _state.SumClose = clNewSum; _adjHighBuffer.Add(adjHigh); _adjLowBuffer.Add(adjLow); _closeBuffer.Add(close); } /// /// 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); // Recompute adjusted values for the corrected bar double denom = high + low; double w = denom != 0.0 ? (high - low) / denom : 0.0; double adjHigh = high * (1.0 + (_factor * w)); double adjLow = low * (1.0 - (_factor * w)); _adjHighBuffer.UpdateNewest(adjHigh); _adjLowBuffer.UpdateNewest(adjLow); _closeBuffer.UpdateNewest(close); _state = _state with { SumAdjHigh = _adjHighBuffer.Sum, SumAdjLow = _adjLowBuffer.Sum, SumClose = _closeBuffer.Sum, SumAdjHighComp = 0, SumAdjLowComp = 0, SumCloseComp = 0, }; } 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 smaAdjHigh = _state.SumAdjHigh / count; double smaAdjLow = _state.SumAdjLow / count; double smaClose = _state.SumClose / count; Last = new TValue(input.Time, smaClose); Upper = new TValue(input.Time, smaAdjHigh); Lower = new TValue(input.Time, smaAdjLow); } 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 _adjHighBuffer.Clear(); _adjLowBuffer.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 smaAdjHigh = _state.SumAdjHigh / count; double smaAdjLow = _state.SumAdjLow / count; double smaClose = _state.SumClose / count; Last = new TValue(lastBar.Time, smaClose); Upper = new TValue(lastBar.Time, smaAdjHigh); Lower = new TValue(lastBar.Time, smaAdjLow); } _p_state = _state; } /// /// Resets the indicator state. /// public void Reset() { _adjHighBuffer.Clear(); _adjLowBuffer.Clear(); _closeBuffer.Clear(); _state = default; _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. /// #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 close) 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 /// /// Input buffers for batch AccBands 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 BatchInputs { /// High price values public readonly ReadOnlySpan High; /// Low price values public readonly ReadOnlySpan Low; /// Close price values public readonly 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; } public bool Equals(BatchInputs other) => Unsafe.AreSame(ref MemoryMarshal.GetReference(High), ref MemoryMarshal.GetReference(other.High)) && High.Length == other.High.Length && Unsafe.AreSame(ref MemoryMarshal.GetReference(Low), ref MemoryMarshal.GetReference(other.Low)) && Low.Length == other.Low.Length && Unsafe.AreSame(ref MemoryMarshal.GetReference(Close), ref MemoryMarshal.GetReference(other.Close)) && Close.Length == other.Close.Length; public override bool Equals(object? obj) => false; public override int GetHashCode() => HashCode.Combine(High.Length, Low.Length, Close.Length); public static bool operator ==(BatchInputs left, BatchInputs right) => left.Equals(right); public static bool operator !=(BatchInputs left, BatchInputs 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 SumAdjHigh; internal double SumAdjLow; internal double SumClose; internal double LastValidHigh; internal double LastValidLow; internal double LastValidClose; internal int BufferIndex; internal int TickCount; public bool Equals(ScalarState other) => SumAdjHigh.Equals(other.SumAdjHigh) && SumAdjLow.Equals(other.SumAdjLow) && SumClose.Equals(other.SumClose) && LastValidHigh.Equals(other.LastValidHigh) && LastValidLow.Equals(other.LastValidLow) && LastValidClose.Equals(other.LastValidClose) && BufferIndex == other.BufferIndex && TickCount == other.TickCount; public override bool Equals(object? obj) => false; public override int GetHashCode() => HashCode.Combine(SumAdjHigh, SumAdjLow, SumClose, 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 adjHigh, Span adjLow, Span close) { internal readonly Span AdjHigh = adjHigh; internal readonly Span AdjLow = adjLow; internal readonly Span Close = close; public bool Equals(WorkBuffers other) => Unsafe.AreSame(ref MemoryMarshal.GetReference(AdjHigh), ref MemoryMarshal.GetReference(other.AdjHigh)) && AdjHigh.Length == other.AdjHigh.Length && Unsafe.AreSame(ref MemoryMarshal.GetReference(AdjLow), ref MemoryMarshal.GetReference(other.AdjLow)) && AdjLow.Length == other.AdjLow.Length && Unsafe.AreSame(ref MemoryMarshal.GetReference(Close), ref MemoryMarshal.GetReference(other.Close)) && Close.Length == other.Close.Length; public override bool Equals(object? obj) => false; public override int GetHashCode() => HashCode.Combine(AdjHigh.Length, AdjLow.Length, Close.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 AccBands for the entire TBarSeries using a new instance. /// public static (TSeries Middle, TSeries Upper, TSeries Lower) Batch(TBarSeries source, int period, double factor = 4.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 = 4.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 = 4.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 = 4.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[] rentedAdjHigh = ArrayPool.Shared.Rent(period); double[] rentedAdjLow = ArrayPool.Shared.Rent(period); double[] rentedClose = ArrayPool.Shared.Rent(period); try { var buffers = new WorkBuffers( rentedAdjHigh.AsSpan(0, period), rentedAdjLow.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(rentedAdjHigh); ArrayPool.Shared.Return(rentedAdjLow); 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); } /// /// Computes adjusted high/low per bar using Headley's formula and writes band outputs. /// [MethodImpl(MethodImplOptions.AggressiveInlining)] private static void WriteBandOutputs(scoped BatchOutputs outputs, int i, double smaAdjHigh, double smaAdjLow, double smaClose) { outputs.Middle[i] = smaClose; outputs.Upper[i] = smaAdjHigh; outputs.Lower[i] = smaAdjLow; } [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); // Headley's per-bar adjustment double denom = h + l; double w = denom != 0.0 ? (h - l) / denom : 0.0; double adjHigh = h * (1.0 + (factor * w)); double adjLow = l * (1.0 - (factor * w)); state.SumAdjHigh += adjHigh; state.SumAdjLow += adjLow; state.SumClose += c; buffers.AdjHigh[i] = adjHigh; buffers.AdjLow[i] = adjLow; buffers.Close[i] = c; int count = i + 1; WriteBandOutputs(outputs, i, state.SumAdjHigh / count, state.SumAdjLow / count, state.SumClose / count); } } [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); // Headley's per-bar adjustment double denom = h + l; double w = denom != 0.0 ? (h - l) / denom : 0.0; double adjHigh = h * (1.0 + (factor * w)); double adjLow = l * (1.0 - (factor * w)); state.SumAdjHigh = state.SumAdjHigh - buffers.AdjHigh[state.BufferIndex] + adjHigh; state.SumAdjLow = state.SumAdjLow - buffers.AdjLow[state.BufferIndex] + adjLow; state.SumClose = state.SumClose - buffers.Close[state.BufferIndex] + c; buffers.AdjHigh[state.BufferIndex] = adjHigh; buffers.AdjLow[state.BufferIndex] = adjLow; buffers.Close[state.BufferIndex] = c; state.BufferIndex++; if (state.BufferIndex >= period) { state.BufferIndex = 0; } WriteBandOutputs(outputs, i, state.SumAdjHigh / period, state.SumAdjLow / period, state.SumClose / period); 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 adjHighSpan = buffers.AdjHigh[..period]; ReadOnlySpan adjLowSpan = buffers.AdjLow[..period]; ReadOnlySpan closeSpan = buffers.Close[..period]; state.SumAdjHigh = adjHighSpan.SumSIMD(); state.SumAdjLow = adjLowSpan.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 = 4.0) { var accBands = new AccBands(period, factor); var results = accBands.Update(source); return (results, accBands); } }