Files

942 lines
33 KiB
C#
Raw Permalink Normal View History

using System.Buffers;
using System.Diagnostics.CodeAnalysis;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
namespace QuanTAlib;
/// <summary>
/// AccBands: Acceleration Bands
/// </summary>
/// <remarks>
/// 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 &amp; Sons.
/// </remarks>
[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;
/// <summary>
/// Display name for the indicator.
/// </summary>
public string Name { get; }
/// <summary>
/// Number of periods before the indicator is considered "hot" (valid).
/// </summary>
public int WarmupPeriod { get; }
/// <summary>
/// Current middle band value.
/// </summary>
public TValue Last { get; private set; }
/// <summary>
/// Current upper band value.
/// </summary>
public TValue Upper { get; private set; }
/// <summary>
/// Current lower band value.
/// </summary>
public TValue Lower { get; private set; }
/// <summary>
/// True if the indicator has enough data to produce valid results.
/// </summary>
public bool IsHot => _closeBuffer.IsFull;
/// <summary>
/// Event triggered when a new TValue is available.
/// </summary>
public event TValuePublishedHandler? Pub;
/// <summary>
/// Creates AccBands with specified period and factor.
/// </summary>
/// <param name="period">Lookback period for SMA calculations (must be > 0)</param>
/// <param name="factor">Multiplier for normalized width (must be > 0, default: 4.0 per Headley)</param>
public AccBands(int period, double factor = 4.0)
{
if (period <= 0)
2026-01-25 16:01:45 -08:00
{
throw new ArgumentException("Period must be greater than 0", nameof(period));
2026-01-25 16:01:45 -08:00
}
if (factor <= 0)
2026-01-25 16:01:45 -08:00
{
throw new ArgumentException("Factor must be greater than 0", nameof(factor));
2026-01-25 16:01:45 -08:00
}
_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;
}
/// <summary>
/// Creates AccBands with TBarSeries source.
/// </summary>
public AccBands(TBarSeries source, int period, double factor = 4.0) : this(period, factor)
{
_source = source;
Prime(source);
source.Pub += _barHandler;
}
/// <summary>
/// Releases resources and unsubscribes from the source event.
/// </summary>
public void Dispose()
{
2026-01-25 16:01:45 -08:00
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);
/// <summary>
/// Helper to invoke the Pub event.
/// </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private void PubEvent(TValue value, bool isNew = true)
{
Pub?.Invoke(this, new TValueEventArgs { Value = value, IsNew = isNew });
}
/// <summary>
/// Gets a valid input value, using last-value substitution for non-finite inputs.
/// </summary>
[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;
}
/// <summary>
/// Computes Headley's per-bar adjusted values and updates running sums.
/// </summary>
[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;
2026-03-03 20:43:16 +00:00
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);
}
/// <summary>
/// Updates the indicator with a TBar input.
/// </summary>
[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;
2026-03-03 20:43:16 +00:00
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;
}
/// <summary>
/// Updates the indicator with a TBarSeries.
/// </summary>
public (TSeries Middle, TSeries Upper, TSeries Lower) Update(TBarSeries source)
{
if (source.Count == 0)
2026-01-25 16:01:45 -08:00
{
return (new TSeries([], []), new TSeries([], []), new TSeries([], []));
2026-01-25 16:01:45 -08:00
}
int len = source.Count;
var tMiddle = new List<long>(len);
var vMiddle = new List<double>(len);
var tUpper = new List<long>(len);
var vUpper = new List<double>(len);
var tLower = new List<long>(len);
var vLower = new List<double>(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));
}
/// <summary>
/// Initializes the indicator state using the provided TBarSeries history.
/// </summary>
// skipcq: CS-R1140
public void Prime(TBarSeries source)
{
2026-01-25 16:01:45 -08:00
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))
2026-01-25 16:01:45 -08:00
{
_state.LastValidHigh = bar.High;
2026-01-25 16:01:45 -08:00
}
if (double.IsFinite(bar.Low) && double.IsNaN(_state.LastValidLow))
2026-01-25 16:01:45 -08:00
{
_state.LastValidLow = bar.Low;
2026-01-25 16:01:45 -08:00
}
if (double.IsFinite(bar.Close) && double.IsNaN(_state.LastValidClose))
2026-01-25 16:01:45 -08:00
{
_state.LastValidClose = bar.Close;
2026-01-25 16:01:45 -08:00
}
if (!double.IsNaN(_state.LastValidHigh) && !double.IsNaN(_state.LastValidLow) && !double.IsNaN(_state.LastValidClose))
2026-01-25 16:01:45 -08:00
{
break;
2026-01-25 16:01:45 -08:00
}
}
// 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))
2026-01-25 16:01:45 -08:00
{
_state.LastValidHigh = bar.High;
2026-01-25 16:01:45 -08:00
}
if (double.IsFinite(bar.Low) && double.IsNaN(_state.LastValidLow))
2026-01-25 16:01:45 -08:00
{
_state.LastValidLow = bar.Low;
2026-01-25 16:01:45 -08:00
}
if (double.IsFinite(bar.Close) && double.IsNaN(_state.LastValidClose))
2026-01-25 16:01:45 -08:00
{
_state.LastValidClose = bar.Close;
2026-01-25 16:01:45 -08:00
}
if (!double.IsNaN(_state.LastValidHigh) && !double.IsNaN(_state.LastValidLow) && !double.IsNaN(_state.LastValidClose))
2026-01-25 16:01:45 -08:00
{
break;
2026-01-25 16:01:45 -08:00
}
}
}
// 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;
}
/// <summary>
/// Resets the indicator state.
/// </summary>
public void Reset()
{
_adjHighBuffer.Clear();
_adjLowBuffer.Clear();
_closeBuffer.Clear();
_state = default;
_p_state = _state;
Last = default;
Upper = default;
Lower = default;
}
/////////////////////////////////////////////////////////////////////////////////////////////////
// Static Batch Methods
/////////////////////////////////////////////////////////////////////////////////////////////////
/// <summary>
/// Output buffers for batch AccBands calculation.
/// </summary>
/// <remarks>
/// Public Span fields are intentional: ref structs cannot use auto-properties with Span&lt;T&gt;
/// and direct field access provides optimal performance for this high-throughput API.
/// </remarks>
#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
{
/// <summary>Output middle band (SMA of close)</summary>
public readonly Span<double> Middle;
/// <summary>Output upper band</summary>
public readonly Span<double> Upper;
/// <summary>Output lower band</summary>
public readonly Span<double> Lower;
#pragma warning restore S1104
/// <summary>
/// Creates a new BatchOutputs instance.
/// </summary>
public BatchOutputs(Span<double> middle, Span<double> upper, Span<double> 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
/// <summary>
/// Input buffers for batch AccBands calculation.
/// </summary>
#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
{
/// <summary>High price values</summary>
public readonly ReadOnlySpan<double> High;
/// <summary>Low price values</summary>
public readonly ReadOnlySpan<double> Low;
/// <summary>Close price values</summary>
public readonly ReadOnlySpan<double> Close;
#pragma warning restore S1104
/// <summary>
/// Creates a new BatchInputs instance.
/// </summary>
public BatchInputs(ReadOnlySpan<double> high, ReadOnlySpan<double> low, ReadOnlySpan<double> 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
/// <summary>
/// Resync interval for batch path only (streaming uses Kahan compensation).
/// </summary>
private const int ResyncInterval = 1000;
/// <summary>
/// Internal state for scalar calculation.
/// </summary>
#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
/// <summary>
/// Working buffers for batch calculation.
/// </summary>
#pragma warning disable MA0077 // ref struct cannot implement interfaces
[StructLayout(LayoutKind.Auto)]
private readonly ref struct WorkBuffers(Span<double> adjHigh, Span<double> adjLow, Span<double> close)
{
internal readonly Span<double> AdjHigh = adjHigh;
internal readonly Span<double> AdjLow = adjLow;
internal readonly Span<double> 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
/// <summary>
/// Calculates AccBands for the entire TBarSeries using a new instance.
/// </summary>
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);
}
/// <summary>
/// Calculates AccBands in-place using spans for maximum performance.
/// Zero-allocation method.
/// </summary>
/// <param name="inputs">Input buffers for high, low, and close prices</param>
/// <param name="outputs">Output buffers for middle, upper, and lower bands</param>
/// <param name="period">Lookback period</param>
/// <param name="factor">Band width factor</param>
[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);
}
/// <summary>
/// Calculates AccBands in-place using spans for maximum performance.
/// Zero-allocation method.
/// </summary>
/// <param name="high">High price values</param>
/// <param name="low">Low price values</param>
/// <param name="close">Close price values</param>
/// <param name="outputs">Output buffers for middle, upper, and lower bands</param>
/// <param name="period">Lookback period</param>
/// <param name="factor">Band width factor</param>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static void Batch(
ReadOnlySpan<double> high,
ReadOnlySpan<double> low,
ReadOnlySpan<double> close,
BatchOutputs outputs,
int period,
double factor = 4.0)
{
Batch(high, low, close, outputs.Middle, outputs.Upper, outputs.Lower, period, factor);
}
/// <summary>
/// Calculates AccBands in-place using spans for maximum performance.
/// Zero-allocation method.
/// </summary>
/// <param name="high">High price values</param>
/// <param name="low">Low price values</param>
/// <param name="close">Close price values</param>
/// <param name="middle">Output middle band (SMA of close)</param>
/// <param name="upper">Output upper band</param>
/// <param name="lower">Output lower band</param>
/// <param name="period">Lookback period</param>
/// <param name="factor">Band width factor</param>
// 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<double> high,
ReadOnlySpan<double> low,
ReadOnlySpan<double> close,
Span<double> middle,
Span<double> upper,
Span<double> lower,
int period,
double factor = 4.0)
#pragma warning restore S107
{
int len = close.Length;
if (high.Length != len || low.Length != len)
2026-01-25 16:01:45 -08:00
{
throw new ArgumentException("High, Low, and Close must have the same length", nameof(high));
2026-01-25 16:01:45 -08:00
}
2026-01-25 16:01:45 -08:00
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<double>.Shared.Rent(period);
double[] rentedAdjLow = ArrayPool<double>.Shared.Rent(period);
double[] rentedClose = ArrayPool<double>.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<double>.Shared.Return(rentedAdjHigh);
ArrayPool<double>.Shared.Return(rentedAdjLow);
ArrayPool<double>.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))
2026-01-25 16:01:45 -08:00
{
state.LastValidHigh = inputs.High[k];
2026-01-25 16:01:45 -08:00
}
if (double.IsFinite(inputs.Low[k]) && double.IsNaN(state.LastValidLow))
2026-01-25 16:01:45 -08:00
{
state.LastValidLow = inputs.Low[k];
2026-01-25 16:01:45 -08:00
}
if (double.IsFinite(inputs.Close[k]) && double.IsNaN(state.LastValidClose))
2026-01-25 16:01:45 -08:00
{
state.LastValidClose = inputs.Close[k];
2026-01-25 16:01:45 -08:00
}
if (!double.IsNaN(state.LastValidHigh) && !double.IsNaN(state.LastValidLow) && !double.IsNaN(state.LastValidClose))
2026-01-25 16:01:45 -08:00
{
break;
2026-01-25 16:01:45 -08:00
}
}
}
[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];
2026-01-25 16:01:45 -08:00
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);
}
/// <summary>
/// Computes adjusted high/low per bar using Headley's formula and writes band outputs.
/// </summary>
[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;
2026-03-03 20:43:16 +00:00
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;
2026-03-03 20:43:16 +00:00
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++;
2026-01-25 16:01:45 -08:00
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<double> adjHighSpan = buffers.AdjHigh[..period];
ReadOnlySpan<double> adjLowSpan = buffers.AdjLow[..period];
ReadOnlySpan<double> closeSpan = buffers.Close[..period];
state.SumAdjHigh = adjHighSpan.SumSIMD();
state.SumAdjLow = adjLowSpan.SumSIMD();
state.SumClose = closeSpan.SumSIMD();
}
/// <summary>
/// Runs a high-performance batch calculation and returns a "Hot" AccBands instance.
/// </summary>
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);
}
2026-01-25 16:01:45 -08:00
}