2026-01-18 19:02:03 -08:00
|
|
|
|
using System.Buffers;
|
2026-03-03 10:33:23 -08:00
|
|
|
|
using System.Diagnostics.CodeAnalysis;
|
2026-01-18 19:02:03 -08:00
|
|
|
|
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.
|
2026-02-20 18:44:56 -08:00
|
|
|
|
/// 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.
|
2026-01-18 19:02:03 -08:00
|
|
|
|
///
|
2026-02-20 18:44:56 -08:00
|
|
|
|
/// 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)
|
2026-01-18 19:02:03 -08:00
|
|
|
|
/// Middle Band = SMA(Close, Period)
|
|
|
|
|
|
///
|
|
|
|
|
|
/// Key characteristics:
|
2026-02-20 18:44:56 -08:00
|
|
|
|
/// - Width adjustment is applied per bar before averaging (Headley's method)
|
2026-01-18 19:02:03 -08:00
|
|
|
|
/// - Bands expand during volatile periods and contract during consolidation
|
2026-02-20 18:44:56 -08:00
|
|
|
|
/// - Factor parameter (default 4.0) controls band sensitivity
|
2026-01-18 19:02:03 -08:00
|
|
|
|
///
|
|
|
|
|
|
/// Sources:
|
2026-02-20 18:44:56 -08:00
|
|
|
|
/// Headley, P. (2002). Big Trends in Trading. John Wiley & Sons.
|
2026-01-18 19:02:03 -08:00
|
|
|
|
/// </remarks>
|
|
|
|
|
|
[SkipLocalsInit]
|
|
|
|
|
|
public sealed class AccBands : ITValuePublisher, IDisposable
|
|
|
|
|
|
{
|
|
|
|
|
|
private readonly int _period;
|
|
|
|
|
|
private readonly double _factor;
|
2026-02-20 18:44:56 -08:00
|
|
|
|
private readonly RingBuffer _adjHighBuffer;
|
|
|
|
|
|
private readonly RingBuffer _adjLowBuffer;
|
2026-01-18 19:02:03 -08:00
|
|
|
|
private readonly RingBuffer _closeBuffer;
|
|
|
|
|
|
private readonly TBarPublishedHandler _barHandler;
|
|
|
|
|
|
private TBarSeries? _source;
|
|
|
|
|
|
private bool _disposed;
|
|
|
|
|
|
|
|
|
|
|
|
[StructLayout(LayoutKind.Auto)]
|
|
|
|
|
|
private record struct State(
|
2026-02-20 18:44:56 -08:00
|
|
|
|
double SumAdjHigh,
|
|
|
|
|
|
double SumAdjLow,
|
2026-01-18 19:02:03 -08:00
|
|
|
|
double SumClose,
|
2026-03-13 22:01:31 -07:00
|
|
|
|
double SumAdjHighComp,
|
|
|
|
|
|
double SumAdjLowComp,
|
|
|
|
|
|
double SumCloseComp,
|
2026-01-18 19:02:03 -08:00
|
|
|
|
double LastValidHigh,
|
|
|
|
|
|
double LastValidLow,
|
2026-03-13 22:01:31 -07:00
|
|
|
|
double LastValidClose
|
2026-01-18 19:02:03 -08:00
|
|
|
|
);
|
|
|
|
|
|
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>
|
2026-02-20 18:44:56 -08:00
|
|
|
|
/// <param name="factor">Multiplier for normalized width (must be > 0, default: 4.0 per Headley)</param>
|
|
|
|
|
|
public AccBands(int period, double factor = 4.0)
|
2026-01-18 19:02:03 -08:00
|
|
|
|
{
|
|
|
|
|
|
if (period <= 0)
|
2026-01-25 16:01:45 -08:00
|
|
|
|
{
|
2026-01-18 19:02:03 -08:00
|
|
|
|
throw new ArgumentException("Period must be greater than 0", nameof(period));
|
2026-01-25 16:01:45 -08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-01-18 19:02:03 -08:00
|
|
|
|
if (factor <= 0)
|
2026-01-25 16:01:45 -08:00
|
|
|
|
{
|
2026-01-18 19:02:03 -08:00
|
|
|
|
throw new ArgumentException("Factor must be greater than 0", nameof(factor));
|
2026-01-25 16:01:45 -08:00
|
|
|
|
}
|
2026-01-18 19:02:03 -08:00
|
|
|
|
|
|
|
|
|
|
_period = period;
|
|
|
|
|
|
_factor = factor;
|
2026-02-20 18:44:56 -08:00
|
|
|
|
_adjHighBuffer = new RingBuffer(period);
|
|
|
|
|
|
_adjLowBuffer = new RingBuffer(period);
|
2026-01-18 19:02:03 -08:00
|
|
|
|
_closeBuffer = new RingBuffer(period);
|
|
|
|
|
|
Name = $"AccBands({period},{factor:F2})";
|
|
|
|
|
|
WarmupPeriod = period;
|
|
|
|
|
|
_barHandler = HandleBar;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
|
/// Creates AccBands with TBarSeries source.
|
|
|
|
|
|
/// </summary>
|
2026-02-20 18:44:56 -08:00
|
|
|
|
public AccBands(TBarSeries source, int period, double factor = 4.0) : this(period, factor)
|
2026-01-18 19:02:03 -08:00
|
|
|
|
{
|
|
|
|
|
|
_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;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-01-18 19:02:03 -08:00
|
|
|
|
_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;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-02-20 18:44:56 -08:00
|
|
|
|
/// <summary>
|
|
|
|
|
|
/// Computes Headley's per-bar adjusted values and updates running sums.
|
|
|
|
|
|
/// </summary>
|
2026-01-18 19:02:03 -08:00
|
|
|
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
|
|
|
|
|
private void UpdateState(double high, double low, double close)
|
|
|
|
|
|
{
|
2026-02-20 18:44:56 -08:00
|
|
|
|
// 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));
|
2026-02-20 18:44:56 -08:00
|
|
|
|
|
|
|
|
|
|
double removedAdjHigh = _adjHighBuffer.Count == _adjHighBuffer.Capacity ? _adjHighBuffer.Oldest : 0.0;
|
|
|
|
|
|
double removedAdjLow = _adjLowBuffer.Count == _adjLowBuffer.Capacity ? _adjLowBuffer.Oldest : 0.0;
|
2026-01-18 19:02:03 -08:00
|
|
|
|
double removedClose = _closeBuffer.Count == _closeBuffer.Capacity ? _closeBuffer.Oldest : 0.0;
|
|
|
|
|
|
|
2026-03-13 22:01:31 -07:00
|
|
|
|
// 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;
|
2026-01-18 19:02:03 -08:00
|
|
|
|
|
2026-02-20 18:44:56 -08:00
|
|
|
|
_adjHighBuffer.Add(adjHigh);
|
|
|
|
|
|
_adjLowBuffer.Add(adjLow);
|
2026-01-18 19:02:03 -08:00
|
|
|
|
_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);
|
|
|
|
|
|
|
2026-02-20 18:44:56 -08:00
|
|
|
|
// 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));
|
2026-02-20 18:44:56 -08:00
|
|
|
|
|
|
|
|
|
|
_adjHighBuffer.UpdateNewest(adjHigh);
|
|
|
|
|
|
_adjLowBuffer.UpdateNewest(adjLow);
|
2026-01-18 19:02:03 -08:00
|
|
|
|
_closeBuffer.UpdateNewest(close);
|
|
|
|
|
|
|
|
|
|
|
|
_state = _state with
|
|
|
|
|
|
{
|
2026-02-20 18:44:56 -08:00
|
|
|
|
SumAdjHigh = _adjHighBuffer.Sum,
|
|
|
|
|
|
SumAdjLow = _adjLowBuffer.Sum,
|
2026-01-18 19:02:03 -08:00
|
|
|
|
SumClose = _closeBuffer.Sum,
|
2026-03-13 22:01:31 -07:00
|
|
|
|
SumAdjHighComp = 0,
|
|
|
|
|
|
SumAdjLowComp = 0,
|
|
|
|
|
|
SumCloseComp = 0,
|
2026-01-18 19:02:03 -08:00
|
|
|
|
};
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
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
|
|
|
|
|
|
{
|
2026-02-20 18:44:56 -08:00
|
|
|
|
double smaAdjHigh = _state.SumAdjHigh / count;
|
|
|
|
|
|
double smaAdjLow = _state.SumAdjLow / count;
|
2026-01-18 19:02:03 -08:00
|
|
|
|
double smaClose = _state.SumClose / count;
|
|
|
|
|
|
|
|
|
|
|
|
Last = new TValue(input.Time, smaClose);
|
2026-02-20 18:44:56 -08:00
|
|
|
|
Upper = new TValue(input.Time, smaAdjHigh);
|
|
|
|
|
|
Lower = new TValue(input.Time, smaAdjLow);
|
2026-01-18 19:02:03 -08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
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
|
|
|
|
{
|
2026-01-18 19:02:03 -08:00
|
|
|
|
return (new TSeries([], []), new TSeries([], []), new TSeries([], []));
|
2026-01-25 16:01:45 -08:00
|
|
|
|
}
|
2026-01-18 19:02:03 -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;
|
|
|
|
|
|
}
|
2026-01-18 19:02:03 -08:00
|
|
|
|
|
|
|
|
|
|
// Reset state
|
2026-02-20 18:44:56 -08:00
|
|
|
|
_adjHighBuffer.Clear();
|
|
|
|
|
|
_adjLowBuffer.Clear();
|
2026-01-18 19:02:03 -08:00
|
|
|
|
_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
|
|
|
|
{
|
2026-01-18 19:02:03 -08:00
|
|
|
|
_state.LastValidHigh = bar.High;
|
2026-01-25 16:01:45 -08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-01-18 19:02:03 -08:00
|
|
|
|
if (double.IsFinite(bar.Low) && double.IsNaN(_state.LastValidLow))
|
2026-01-25 16:01:45 -08:00
|
|
|
|
{
|
2026-01-18 19:02:03 -08:00
|
|
|
|
_state.LastValidLow = bar.Low;
|
2026-01-25 16:01:45 -08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-01-18 19:02:03 -08:00
|
|
|
|
if (double.IsFinite(bar.Close) && double.IsNaN(_state.LastValidClose))
|
2026-01-25 16:01:45 -08:00
|
|
|
|
{
|
2026-01-18 19:02:03 -08:00
|
|
|
|
_state.LastValidClose = bar.Close;
|
2026-01-25 16:01:45 -08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-01-18 19:02:03 -08:00
|
|
|
|
if (!double.IsNaN(_state.LastValidHigh) && !double.IsNaN(_state.LastValidLow) && !double.IsNaN(_state.LastValidClose))
|
2026-01-25 16:01:45 -08:00
|
|
|
|
{
|
2026-01-18 19:02:03 -08:00
|
|
|
|
break;
|
2026-01-25 16:01:45 -08:00
|
|
|
|
}
|
2026-01-18 19:02:03 -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
|
|
|
|
{
|
2026-01-18 19:02:03 -08:00
|
|
|
|
_state.LastValidHigh = bar.High;
|
2026-01-25 16:01:45 -08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-01-18 19:02:03 -08:00
|
|
|
|
if (double.IsFinite(bar.Low) && double.IsNaN(_state.LastValidLow))
|
2026-01-25 16:01:45 -08:00
|
|
|
|
{
|
2026-01-18 19:02:03 -08:00
|
|
|
|
_state.LastValidLow = bar.Low;
|
2026-01-25 16:01:45 -08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-01-18 19:02:03 -08:00
|
|
|
|
if (double.IsFinite(bar.Close) && double.IsNaN(_state.LastValidClose))
|
2026-01-25 16:01:45 -08:00
|
|
|
|
{
|
2026-01-18 19:02:03 -08:00
|
|
|
|
_state.LastValidClose = bar.Close;
|
2026-01-25 16:01:45 -08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-01-18 19:02:03 -08:00
|
|
|
|
if (!double.IsNaN(_state.LastValidHigh) && !double.IsNaN(_state.LastValidLow) && !double.IsNaN(_state.LastValidClose))
|
2026-01-25 16:01:45 -08:00
|
|
|
|
{
|
2026-01-18 19:02:03 -08:00
|
|
|
|
break;
|
2026-01-25 16:01:45 -08:00
|
|
|
|
}
|
2026-01-18 19:02:03 -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;
|
2026-02-20 18:44:56 -08:00
|
|
|
|
double smaAdjHigh = _state.SumAdjHigh / count;
|
|
|
|
|
|
double smaAdjLow = _state.SumAdjLow / count;
|
2026-01-18 19:02:03 -08:00
|
|
|
|
double smaClose = _state.SumClose / count;
|
|
|
|
|
|
|
|
|
|
|
|
Last = new TValue(lastBar.Time, smaClose);
|
2026-02-20 18:44:56 -08:00
|
|
|
|
Upper = new TValue(lastBar.Time, smaAdjHigh);
|
|
|
|
|
|
Lower = new TValue(lastBar.Time, smaAdjLow);
|
2026-01-18 19:02:03 -08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
_p_state = _state;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
|
/// Resets the indicator state.
|
|
|
|
|
|
/// </summary>
|
|
|
|
|
|
public void Reset()
|
|
|
|
|
|
{
|
2026-02-20 18:44:56 -08:00
|
|
|
|
_adjHighBuffer.Clear();
|
|
|
|
|
|
_adjLowBuffer.Clear();
|
2026-01-18 19:02:03 -08:00
|
|
|
|
_closeBuffer.Clear();
|
2026-03-13 22:01:31 -07:00
|
|
|
|
_state = default;
|
2026-01-18 19:02:03 -08:00
|
|
|
|
_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<T>
|
|
|
|
|
|
/// and direct field access provides optimal performance for this high-throughput API.
|
|
|
|
|
|
/// </remarks>
|
2026-03-03 12:42:42 -08:00
|
|
|
|
#pragma warning disable MA0077 // ref struct cannot implement interfaces
|
2026-01-18 19:02:03 -08:00
|
|
|
|
[StructLayout(LayoutKind.Auto)]
|
|
|
|
|
|
#pragma warning disable S1104 // Fields should not have public accessibility
|
2026-03-03 10:33:23 -08:00
|
|
|
|
public readonly ref struct BatchOutputs
|
2026-01-18 19:02:03 -08:00
|
|
|
|
{
|
|
|
|
|
|
/// <summary>Output middle band (SMA of close)</summary>
|
2026-03-03 10:33:23 -08:00
|
|
|
|
public readonly Span<double> Middle;
|
2026-01-18 19:02:03 -08:00
|
|
|
|
/// <summary>Output upper band</summary>
|
2026-03-03 10:33:23 -08:00
|
|
|
|
public readonly Span<double> Upper;
|
2026-01-18 19:02:03 -08:00
|
|
|
|
/// <summary>Output lower band</summary>
|
2026-03-03 10:33:23 -08:00
|
|
|
|
public readonly Span<double> Lower;
|
2026-01-18 19:02:03 -08:00
|
|
|
|
#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;
|
|
|
|
|
|
}
|
2026-03-03 10:33:23 -08:00
|
|
|
|
|
|
|
|
|
|
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);
|
2026-01-18 19:02:03 -08:00
|
|
|
|
}
|
2026-03-03 12:42:42 -08:00
|
|
|
|
#pragma warning restore MA0077
|
2026-01-18 19:02:03 -08:00
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
|
/// Input buffers for batch AccBands calculation.
|
|
|
|
|
|
/// </summary>
|
2026-03-03 12:42:42 -08:00
|
|
|
|
#pragma warning disable MA0077 // ref struct cannot implement interfaces
|
2026-01-18 19:02:03 -08:00
|
|
|
|
[StructLayout(LayoutKind.Auto)]
|
|
|
|
|
|
#pragma warning disable S1104 // Fields should not have public accessibility
|
2026-03-03 10:33:23 -08:00
|
|
|
|
public readonly ref struct BatchInputs
|
2026-01-18 19:02:03 -08:00
|
|
|
|
{
|
|
|
|
|
|
/// <summary>High price values</summary>
|
2026-03-03 10:33:23 -08:00
|
|
|
|
public readonly ReadOnlySpan<double> High;
|
2026-01-18 19:02:03 -08:00
|
|
|
|
/// <summary>Low price values</summary>
|
2026-03-03 10:33:23 -08:00
|
|
|
|
public readonly ReadOnlySpan<double> Low;
|
2026-01-18 19:02:03 -08:00
|
|
|
|
/// <summary>Close price values</summary>
|
2026-03-03 10:33:23 -08:00
|
|
|
|
public readonly ReadOnlySpan<double> Close;
|
2026-01-18 19:02:03 -08:00
|
|
|
|
#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;
|
|
|
|
|
|
}
|
2026-03-03 10:33:23 -08:00
|
|
|
|
|
|
|
|
|
|
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);
|
2026-01-18 19:02:03 -08:00
|
|
|
|
}
|
2026-03-03 12:42:42 -08:00
|
|
|
|
#pragma warning restore MA0077
|
2026-01-18 19:02:03 -08:00
|
|
|
|
|
2026-03-13 22:01:31 -07:00
|
|
|
|
/// <summary>
|
|
|
|
|
|
/// Resync interval for batch path only (streaming uses Kahan compensation).
|
|
|
|
|
|
/// </summary>
|
|
|
|
|
|
private const int ResyncInterval = 1000;
|
|
|
|
|
|
|
2026-01-18 19:02:03 -08:00
|
|
|
|
/// <summary>
|
|
|
|
|
|
/// Internal state for scalar calculation.
|
|
|
|
|
|
/// </summary>
|
2026-03-03 12:42:42 -08:00
|
|
|
|
#pragma warning disable MA0077 // ref struct cannot implement interfaces
|
2026-01-18 19:02:03 -08:00
|
|
|
|
[StructLayout(LayoutKind.Auto)]
|
2026-03-03 10:33:23 -08:00
|
|
|
|
[SuppressMessage("NDepend", "ND1903:StructuresShouldBeImmutable", Justification = "Mutable calculation state accumulator by design")]
|
2026-01-18 19:02:03 -08:00
|
|
|
|
private ref struct ScalarState
|
|
|
|
|
|
{
|
2026-03-03 09:22:55 -08:00
|
|
|
|
internal double SumAdjHigh;
|
|
|
|
|
|
internal double SumAdjLow;
|
|
|
|
|
|
internal double SumClose;
|
|
|
|
|
|
internal double LastValidHigh;
|
|
|
|
|
|
internal double LastValidLow;
|
|
|
|
|
|
internal double LastValidClose;
|
|
|
|
|
|
internal int BufferIndex;
|
|
|
|
|
|
internal int TickCount;
|
2026-03-03 10:33:23 -08:00
|
|
|
|
|
|
|
|
|
|
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);
|
2026-01-18 19:02:03 -08:00
|
|
|
|
}
|
2026-03-03 12:42:42 -08:00
|
|
|
|
#pragma warning restore MA0077
|
2026-01-18 19:02:03 -08:00
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
|
/// Working buffers for batch calculation.
|
|
|
|
|
|
/// </summary>
|
2026-03-03 12:42:42 -08:00
|
|
|
|
#pragma warning disable MA0077 // ref struct cannot implement interfaces
|
2026-01-18 19:02:03 -08:00
|
|
|
|
[StructLayout(LayoutKind.Auto)]
|
2026-02-20 18:44:56 -08:00
|
|
|
|
private readonly ref struct WorkBuffers(Span<double> adjHigh, Span<double> adjLow, Span<double> close)
|
2026-01-18 19:02:03 -08:00
|
|
|
|
{
|
2026-03-03 09:22:55 -08:00
|
|
|
|
internal readonly Span<double> AdjHigh = adjHigh;
|
|
|
|
|
|
internal readonly Span<double> AdjLow = adjLow;
|
|
|
|
|
|
internal readonly Span<double> Close = close;
|
2026-03-03 10:33:23 -08:00
|
|
|
|
|
|
|
|
|
|
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);
|
2026-01-18 19:02:03 -08:00
|
|
|
|
}
|
2026-03-03 12:42:42 -08:00
|
|
|
|
#pragma warning restore MA0077
|
2026-01-18 19:02:03 -08:00
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
|
/// Calculates AccBands for the entire TBarSeries using a new instance.
|
|
|
|
|
|
/// </summary>
|
2026-02-20 18:44:56 -08:00
|
|
|
|
public static (TSeries Middle, TSeries Upper, TSeries Lower) Batch(TBarSeries source, int period, double factor = 4.0)
|
2026-01-18 19:02:03 -08:00
|
|
|
|
{
|
|
|
|
|
|
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,
|
2026-02-20 18:44:56 -08:00
|
|
|
|
double factor = 4.0)
|
2026-01-18 19:02:03 -08:00
|
|
|
|
{
|
|
|
|
|
|
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,
|
2026-02-20 18:44:56 -08:00
|
|
|
|
double factor = 4.0)
|
2026-01-18 19:02:03 -08:00
|
|
|
|
{
|
|
|
|
|
|
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,
|
2026-02-20 18:44:56 -08:00
|
|
|
|
double factor = 4.0)
|
2026-01-18 19:02:03 -08:00
|
|
|
|
#pragma warning restore S107
|
|
|
|
|
|
{
|
|
|
|
|
|
int len = close.Length;
|
|
|
|
|
|
if (high.Length != len || low.Length != len)
|
2026-01-25 16:01:45 -08:00
|
|
|
|
{
|
2026-01-18 19:02:03 -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-18 19:02:03 -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;
|
|
|
|
|
|
}
|
2026-01-18 19:02:03 -08:00
|
|
|
|
|
|
|
|
|
|
// 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
|
2026-02-20 18:44:56 -08:00
|
|
|
|
double[] rentedAdjHigh = ArrayPool<double>.Shared.Rent(period);
|
|
|
|
|
|
double[] rentedAdjLow = ArrayPool<double>.Shared.Rent(period);
|
2026-01-18 19:02:03 -08:00
|
|
|
|
double[] rentedClose = ArrayPool<double>.Shared.Rent(period);
|
|
|
|
|
|
|
|
|
|
|
|
try
|
|
|
|
|
|
{
|
|
|
|
|
|
var buffers = new WorkBuffers(
|
2026-02-20 18:44:56 -08:00
|
|
|
|
rentedAdjHigh.AsSpan(0, period),
|
|
|
|
|
|
rentedAdjLow.AsSpan(0, period),
|
2026-01-18 19:02:03 -08:00
|
|
|
|
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
|
|
|
|
|
|
{
|
2026-02-20 18:44:56 -08:00
|
|
|
|
ArrayPool<double>.Shared.Return(rentedAdjHigh);
|
|
|
|
|
|
ArrayPool<double>.Shared.Return(rentedAdjLow);
|
2026-01-18 19:02:03 -08:00
|
|
|
|
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
|
|
|
|
{
|
2026-01-18 19:02:03 -08:00
|
|
|
|
state.LastValidHigh = inputs.High[k];
|
2026-01-25 16:01:45 -08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-01-18 19:02:03 -08:00
|
|
|
|
if (double.IsFinite(inputs.Low[k]) && double.IsNaN(state.LastValidLow))
|
2026-01-25 16:01:45 -08:00
|
|
|
|
{
|
2026-01-18 19:02:03 -08:00
|
|
|
|
state.LastValidLow = inputs.Low[k];
|
2026-01-25 16:01:45 -08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-01-18 19:02:03 -08:00
|
|
|
|
if (double.IsFinite(inputs.Close[k]) && double.IsNaN(state.LastValidClose))
|
2026-01-25 16:01:45 -08:00
|
|
|
|
{
|
2026-01-18 19:02:03 -08:00
|
|
|
|
state.LastValidClose = inputs.Close[k];
|
2026-01-25 16:01:45 -08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-01-18 19:02:03 -08:00
|
|
|
|
if (!double.IsNaN(state.LastValidHigh) && !double.IsNaN(state.LastValidLow) && !double.IsNaN(state.LastValidClose))
|
2026-01-25 16:01:45 -08:00
|
|
|
|
{
|
2026-01-18 19:02:03 -08:00
|
|
|
|
break;
|
2026-01-25 16:01:45 -08:00
|
|
|
|
}
|
2026-01-18 19:02:03 -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;
|
|
|
|
|
|
}
|
2026-01-18 19:02:03 -08:00
|
|
|
|
|
|
|
|
|
|
return (h, l, c);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-02-20 18:44:56 -08:00
|
|
|
|
/// <summary>
|
|
|
|
|
|
/// Computes adjusted high/low per bar using Headley's formula and writes band outputs.
|
|
|
|
|
|
/// </summary>
|
2026-01-18 19:02:03 -08:00
|
|
|
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
2026-02-20 18:44:56 -08:00
|
|
|
|
private static void WriteBandOutputs(scoped BatchOutputs outputs, int i, double smaAdjHigh, double smaAdjLow, double smaClose)
|
2026-01-18 19:02:03 -08:00
|
|
|
|
{
|
|
|
|
|
|
outputs.Middle[i] = smaClose;
|
2026-02-20 18:44:56 -08:00
|
|
|
|
outputs.Upper[i] = smaAdjHigh;
|
|
|
|
|
|
outputs.Lower[i] = smaAdjLow;
|
2026-01-18 19:02:03 -08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
[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);
|
|
|
|
|
|
|
2026-02-20 18:44:56 -08:00
|
|
|
|
// 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));
|
2026-02-20 18:44:56 -08:00
|
|
|
|
|
|
|
|
|
|
state.SumAdjHigh += adjHigh;
|
|
|
|
|
|
state.SumAdjLow += adjLow;
|
2026-01-18 19:02:03 -08:00
|
|
|
|
state.SumClose += c;
|
|
|
|
|
|
|
2026-02-20 18:44:56 -08:00
|
|
|
|
buffers.AdjHigh[i] = adjHigh;
|
|
|
|
|
|
buffers.AdjLow[i] = adjLow;
|
2026-01-18 19:02:03 -08:00
|
|
|
|
buffers.Close[i] = c;
|
|
|
|
|
|
|
|
|
|
|
|
int count = i + 1;
|
2026-02-20 18:44:56 -08:00
|
|
|
|
WriteBandOutputs(outputs, i, state.SumAdjHigh / count, state.SumAdjLow / count, state.SumClose / count);
|
2026-01-18 19:02:03 -08:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
[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);
|
|
|
|
|
|
|
2026-02-20 18:44:56 -08:00
|
|
|
|
// 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));
|
2026-02-20 18:44:56 -08:00
|
|
|
|
|
|
|
|
|
|
state.SumAdjHigh = state.SumAdjHigh - buffers.AdjHigh[state.BufferIndex] + adjHigh;
|
|
|
|
|
|
state.SumAdjLow = state.SumAdjLow - buffers.AdjLow[state.BufferIndex] + adjLow;
|
2026-01-18 19:02:03 -08:00
|
|
|
|
state.SumClose = state.SumClose - buffers.Close[state.BufferIndex] + c;
|
|
|
|
|
|
|
2026-02-20 18:44:56 -08:00
|
|
|
|
buffers.AdjHigh[state.BufferIndex] = adjHigh;
|
|
|
|
|
|
buffers.AdjLow[state.BufferIndex] = adjLow;
|
2026-01-18 19:02:03 -08:00
|
|
|
|
buffers.Close[state.BufferIndex] = c;
|
|
|
|
|
|
|
|
|
|
|
|
state.BufferIndex++;
|
2026-01-25 16:01:45 -08:00
|
|
|
|
if (state.BufferIndex >= period)
|
|
|
|
|
|
{
|
|
|
|
|
|
state.BufferIndex = 0;
|
|
|
|
|
|
}
|
2026-01-18 19:02:03 -08:00
|
|
|
|
|
2026-02-20 18:44:56 -08:00
|
|
|
|
WriteBandOutputs(outputs, i, state.SumAdjHigh / period, state.SumAdjLow / period, state.SumClose / period);
|
2026-01-18 19:02:03 -08:00
|
|
|
|
|
|
|
|
|
|
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;
|
2026-02-20 18:44:56 -08:00
|
|
|
|
ReadOnlySpan<double> adjHighSpan = buffers.AdjHigh[..period];
|
|
|
|
|
|
ReadOnlySpan<double> adjLowSpan = buffers.AdjLow[..period];
|
2026-01-18 19:02:03 -08:00
|
|
|
|
ReadOnlySpan<double> closeSpan = buffers.Close[..period];
|
2026-02-20 18:44:56 -08:00
|
|
|
|
state.SumAdjHigh = adjHighSpan.SumSIMD();
|
|
|
|
|
|
state.SumAdjLow = adjLowSpan.SumSIMD();
|
2026-01-18 19:02:03 -08:00
|
|
|
|
state.SumClose = closeSpan.SumSIMD();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
|
/// Runs a high-performance batch calculation and returns a "Hot" AccBands instance.
|
|
|
|
|
|
/// </summary>
|
2026-02-20 18:44:56 -08:00
|
|
|
|
public static ((TSeries Middle, TSeries Upper, TSeries Lower) Results, AccBands Indicator) Calculate(TBarSeries source, int period, double factor = 4.0)
|
2026-01-18 19:02:03 -08:00
|
|
|
|
{
|
|
|
|
|
|
var accBands = new AccBands(period, factor);
|
|
|
|
|
|
var results = accBands.Update(source);
|
|
|
|
|
|
return (results, accBands);
|
|
|
|
|
|
}
|
2026-01-25 16:01:45 -08:00
|
|
|
|
}
|