mirror of
https://github.com/mihakralj/QuanTAlib.git
synced 2026-08-07 21:47:43 +00:00
67ad6f0cba
Comprehensive refactor across all indicators replacing the periodic ResyncInterval-based drift correction (every 1000 ticks recalculate from scratch) with Kahan compensated summation for running sums. Key changes: - Remove ResyncInterval constants and TickCount fields from all State records - Add Kahan compensation fields (SumComp, SumSqComp, etc.) to State records - Replace naive sum += val - removed with Kahan delta pattern - Remove Resync()/RecalculateSum() methods that did O(N) recalculation - Update batch/SIMD paths to use Kahan compensation instead of resync loops - IIR filters (EMA, REMA, RGMA) simplified: inherently self-correcting - Version bump to 0.8.7 - Build system: README version stamping via Directory.Build.props - Minor doc/test tolerance adjustments for new numerical characteristics Affected modules: channels, core, cycles, dynamics, errors, momentum, oscillators, statistics, trends_FIR, trends_IIR, volatility, volume
942 lines
33 KiB
C#
942 lines
33 KiB
C#
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 & 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)
|
||
{
|
||
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;
|
||
}
|
||
|
||
/// <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()
|
||
{
|
||
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;
|
||
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;
|
||
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)
|
||
{
|
||
return (new TSeries([], []), new TSeries([], []), new TSeries([], []));
|
||
}
|
||
|
||
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)
|
||
{
|
||
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;
|
||
}
|
||
|
||
/// <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<T>
|
||
/// 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)
|
||
{
|
||
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<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))
|
||
{
|
||
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);
|
||
}
|
||
|
||
/// <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;
|
||
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<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);
|
||
}
|
||
}
|