mirror of
https://github.com/mihakralj/QuanTAlib.git
synced 2026-08-12 23:58:04 +00:00
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com> Co-authored-by: aider (openrouter/anthropic/claude-sonnet-4) <aider@aider.chat> Co-authored-by: Warp <agent@warp.dev>
762 lines
26 KiB
C#
762 lines
26 KiB
C#
using System.Buffers;
|
||
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, with band width determined
|
||
/// by the spread between the high and low moving averages multiplied by a factor.
|
||
///
|
||
/// Calculation:
|
||
/// Middle Band = SMA(Close, Period)
|
||
/// BandWidth = [SMA(High, Period) - SMA(Low, Period)] × Factor
|
||
/// Upper Band = SMA(High, Period) + BandWidth
|
||
/// Lower Band = SMA(Low, Period) - BandWidth
|
||
///
|
||
/// Key characteristics:
|
||
/// - Bands expand during volatile periods and contract during consolidation
|
||
/// - Uses SMA of High, Low, and Close for calculations
|
||
/// - Factor parameter controls band sensitivity
|
||
///
|
||
/// Sources:
|
||
/// Headley, P. (2002). Big Trends in Trading. John Wiley & Sons.
|
||
/// </remarks>
|
||
[SkipLocalsInit]
|
||
public sealed class AccBands : ITValuePublisher, IDisposable
|
||
{
|
||
private readonly int _period;
|
||
private readonly double _factor;
|
||
private readonly RingBuffer _highBuffer;
|
||
private readonly RingBuffer _lowBuffer;
|
||
private readonly RingBuffer _closeBuffer;
|
||
private readonly TBarPublishedHandler _barHandler;
|
||
private TBarSeries? _source;
|
||
private bool _disposed;
|
||
|
||
private const int ResyncInterval = 1000;
|
||
|
||
[StructLayout(LayoutKind.Auto)]
|
||
private record struct State(
|
||
double SumHigh,
|
||
double SumLow,
|
||
double SumClose,
|
||
double LastValidHigh,
|
||
double LastValidLow,
|
||
double LastValidClose,
|
||
int TickCount
|
||
);
|
||
private State _state;
|
||
private State _p_state;
|
||
|
||
/// <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 band width (must be > 0, default: 2.0)</param>
|
||
public AccBands(int period, double factor = 2.0)
|
||
{
|
||
if (period <= 0)
|
||
throw new ArgumentException("Period must be greater than 0", nameof(period));
|
||
if (factor <= 0)
|
||
throw new ArgumentException("Factor must be greater than 0", nameof(factor));
|
||
|
||
_period = period;
|
||
_factor = factor;
|
||
_highBuffer = new RingBuffer(period);
|
||
_lowBuffer = new RingBuffer(period);
|
||
_closeBuffer = new RingBuffer(period);
|
||
Name = $"AccBands({period},{factor:F2})";
|
||
WarmupPeriod = period;
|
||
_barHandler = HandleBar;
|
||
}
|
||
|
||
/// <summary>
|
||
/// Creates AccBands with TBarSeries source.
|
||
/// </summary>
|
||
public AccBands(TBarSeries source, int period, double factor = 2.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;
|
||
}
|
||
|
||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||
private void UpdateState(double high, double low, double close)
|
||
{
|
||
double removedHigh = _highBuffer.Count == _highBuffer.Capacity ? _highBuffer.Oldest : 0.0;
|
||
double removedLow = _lowBuffer.Count == _lowBuffer.Capacity ? _lowBuffer.Oldest : 0.0;
|
||
double removedClose = _closeBuffer.Count == _closeBuffer.Capacity ? _closeBuffer.Oldest : 0.0;
|
||
|
||
_state.SumHigh = _state.SumHigh - removedHigh + high;
|
||
_state.SumLow = _state.SumLow - removedLow + low;
|
||
_state.SumClose = _state.SumClose - removedClose + close;
|
||
|
||
_highBuffer.Add(high);
|
||
_lowBuffer.Add(low);
|
||
_closeBuffer.Add(close);
|
||
|
||
_state.TickCount++;
|
||
if (_closeBuffer.IsFull && _state.TickCount >= ResyncInterval)
|
||
{
|
||
_state.TickCount = 0;
|
||
_state.SumHigh = _highBuffer.RecalculateSum();
|
||
_state.SumLow = _lowBuffer.RecalculateSum();
|
||
_state.SumClose = _closeBuffer.RecalculateSum();
|
||
}
|
||
}
|
||
|
||
/// <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);
|
||
|
||
_highBuffer.UpdateNewest(high);
|
||
_lowBuffer.UpdateNewest(low);
|
||
_closeBuffer.UpdateNewest(close);
|
||
|
||
_state = _state with
|
||
{
|
||
SumHigh = _highBuffer.Sum,
|
||
SumLow = _lowBuffer.Sum,
|
||
SumClose = _closeBuffer.Sum,
|
||
};
|
||
}
|
||
|
||
int count = _closeBuffer.Count;
|
||
if (count == 0)
|
||
{
|
||
Last = new TValue(input.Time, double.NaN);
|
||
Upper = new TValue(input.Time, double.NaN);
|
||
Lower = new TValue(input.Time, double.NaN);
|
||
}
|
||
else
|
||
{
|
||
double smaHigh = _state.SumHigh / count;
|
||
double smaLow = _state.SumLow / count;
|
||
double smaClose = _state.SumClose / count;
|
||
double bandWidth = (smaHigh - smaLow) * _factor;
|
||
|
||
Last = new TValue(input.Time, smaClose);
|
||
Upper = new TValue(input.Time, smaHigh + bandWidth);
|
||
Lower = new TValue(input.Time, smaLow - bandWidth);
|
||
}
|
||
|
||
PubEvent(Last, isNew);
|
||
return Last;
|
||
}
|
||
|
||
/// <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
|
||
_highBuffer.Clear();
|
||
_lowBuffer.Clear();
|
||
_closeBuffer.Clear();
|
||
_state = default;
|
||
_p_state = default;
|
||
|
||
int warmupLength = Math.Min(source.Count, WarmupPeriod);
|
||
int startIndex = source.Count - warmupLength;
|
||
|
||
// Seed LastValidValue
|
||
_state.LastValidHigh = double.NaN;
|
||
_state.LastValidLow = double.NaN;
|
||
_state.LastValidClose = double.NaN;
|
||
|
||
for (int i = startIndex - 1; i >= 0; i--)
|
||
{
|
||
var bar = source[i];
|
||
if (double.IsFinite(bar.High) && double.IsNaN(_state.LastValidHigh))
|
||
_state.LastValidHigh = bar.High;
|
||
if (double.IsFinite(bar.Low) && double.IsNaN(_state.LastValidLow))
|
||
_state.LastValidLow = bar.Low;
|
||
if (double.IsFinite(bar.Close) && double.IsNaN(_state.LastValidClose))
|
||
_state.LastValidClose = bar.Close;
|
||
if (!double.IsNaN(_state.LastValidHigh) && !double.IsNaN(_state.LastValidLow) && !double.IsNaN(_state.LastValidClose))
|
||
break;
|
||
}
|
||
|
||
// Find valid values in warmup window if not found
|
||
if (double.IsNaN(_state.LastValidHigh) || double.IsNaN(_state.LastValidLow) || double.IsNaN(_state.LastValidClose))
|
||
{
|
||
for (int i = startIndex; i < source.Count; i++)
|
||
{
|
||
var bar = source[i];
|
||
if (double.IsFinite(bar.High) && double.IsNaN(_state.LastValidHigh))
|
||
_state.LastValidHigh = bar.High;
|
||
if (double.IsFinite(bar.Low) && double.IsNaN(_state.LastValidLow))
|
||
_state.LastValidLow = bar.Low;
|
||
if (double.IsFinite(bar.Close) && double.IsNaN(_state.LastValidClose))
|
||
_state.LastValidClose = bar.Close;
|
||
if (!double.IsNaN(_state.LastValidHigh) && !double.IsNaN(_state.LastValidLow) && !double.IsNaN(_state.LastValidClose))
|
||
break;
|
||
}
|
||
}
|
||
|
||
// Feed the buffers
|
||
for (int i = startIndex; i < source.Count; i++)
|
||
{
|
||
var bar = source[i];
|
||
double high = GetValidHigh(bar.High);
|
||
double low = GetValidLow(bar.Low);
|
||
double close = GetValidClose(bar.Close);
|
||
UpdateState(high, low, close);
|
||
}
|
||
|
||
// Finalize state
|
||
int count = _closeBuffer.Count;
|
||
if (count > 0)
|
||
{
|
||
var lastBar = source.Last;
|
||
double smaHigh = _state.SumHigh / count;
|
||
double smaLow = _state.SumLow / count;
|
||
double smaClose = _state.SumClose / count;
|
||
double bandWidth = (smaHigh - smaLow) * _factor;
|
||
|
||
Last = new TValue(lastBar.Time, smaClose);
|
||
Upper = new TValue(lastBar.Time, smaHigh + bandWidth);
|
||
Lower = new TValue(lastBar.Time, smaLow - bandWidth);
|
||
}
|
||
|
||
_p_state = _state;
|
||
}
|
||
|
||
/// <summary>
|
||
/// Resets the indicator state.
|
||
/// </summary>
|
||
public void Reset()
|
||
{
|
||
_highBuffer.Clear();
|
||
_lowBuffer.Clear();
|
||
_closeBuffer.Clear();
|
||
_state = new State(
|
||
SumHigh: 0,
|
||
SumLow: 0,
|
||
SumClose: 0,
|
||
LastValidHigh: double.NaN,
|
||
LastValidLow: double.NaN,
|
||
LastValidClose: double.NaN,
|
||
TickCount: 0
|
||
);
|
||
_p_state = _state;
|
||
Last = default;
|
||
Upper = default;
|
||
Lower = default;
|
||
}
|
||
|
||
/////////////////////////////////////////////////////////////////////////////////////////////////
|
||
// Static Batch Methods
|
||
/////////////////////////////////////////////////////////////////////////////////////////////////
|
||
|
||
/// <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>
|
||
[StructLayout(LayoutKind.Auto)]
|
||
#pragma warning disable S1104 // Fields should not have public accessibility
|
||
public ref struct BatchOutputs
|
||
{
|
||
/// <summary>Output middle band (SMA of close)</summary>
|
||
public Span<double> Middle;
|
||
/// <summary>Output upper band</summary>
|
||
public Span<double> Upper;
|
||
/// <summary>Output lower band</summary>
|
||
public 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;
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// Input buffers for batch AccBands calculation.
|
||
/// </summary>
|
||
[StructLayout(LayoutKind.Auto)]
|
||
#pragma warning disable S1104 // Fields should not have public accessibility
|
||
public ref struct BatchInputs
|
||
{
|
||
/// <summary>High price values</summary>
|
||
public ReadOnlySpan<double> High;
|
||
/// <summary>Low price values</summary>
|
||
public ReadOnlySpan<double> Low;
|
||
/// <summary>Close price values</summary>
|
||
public 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;
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// Internal state for scalar calculation.
|
||
/// </summary>
|
||
[StructLayout(LayoutKind.Auto)]
|
||
private ref struct ScalarState
|
||
{
|
||
public double SumHigh;
|
||
public double SumLow;
|
||
public double SumClose;
|
||
public double LastValidHigh;
|
||
public double LastValidLow;
|
||
public double LastValidClose;
|
||
public int BufferIndex;
|
||
public int TickCount;
|
||
}
|
||
|
||
/// <summary>
|
||
/// Working buffers for batch calculation.
|
||
/// </summary>
|
||
[StructLayout(LayoutKind.Auto)]
|
||
private readonly ref struct WorkBuffers(Span<double> high, Span<double> low, Span<double> close)
|
||
{
|
||
public readonly Span<double> High = high;
|
||
public readonly Span<double> Low = low;
|
||
public readonly Span<double> Close = close;
|
||
}
|
||
|
||
/// <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 = 2.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 = 2.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 = 2.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 = 2.0)
|
||
#pragma warning restore S107
|
||
{
|
||
int len = close.Length;
|
||
if (high.Length != len || low.Length != len)
|
||
throw new ArgumentException("High, Low, and Close must have the same length", nameof(high));
|
||
if (middle.Length < len || upper.Length < len || lower.Length < len)
|
||
throw new ArgumentException("Output buffers must be at least as long as input", nameof(middle));
|
||
if (period <= 0)
|
||
throw new ArgumentException("Period must be greater than 0", nameof(period));
|
||
if (factor <= 0)
|
||
throw new ArgumentException("Factor must be greater than 0", nameof(factor));
|
||
|
||
if (len == 0) return;
|
||
|
||
// Scalar implementation with NaN handling
|
||
var inputs = new BatchInputs(high, low, close);
|
||
var outputs = new BatchOutputs(middle, upper, lower);
|
||
CalculateScalarCore(inputs, outputs, period, factor);
|
||
}
|
||
|
||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||
private static void CalculateScalarCore(
|
||
scoped BatchInputs inputs,
|
||
scoped BatchOutputs outputs,
|
||
int period,
|
||
double factor)
|
||
{
|
||
int len = inputs.Close.Length;
|
||
|
||
// Always use ArrayPool to avoid span scope safety issues with stackalloc + ref structs
|
||
double[] rentedHigh = ArrayPool<double>.Shared.Rent(period);
|
||
double[] rentedLow = ArrayPool<double>.Shared.Rent(period);
|
||
double[] rentedClose = ArrayPool<double>.Shared.Rent(period);
|
||
|
||
try
|
||
{
|
||
var buffers = new WorkBuffers(
|
||
rentedHigh.AsSpan(0, period),
|
||
rentedLow.AsSpan(0, period),
|
||
rentedClose.AsSpan(0, period));
|
||
|
||
var state = new ScalarState
|
||
{
|
||
LastValidHigh = double.NaN,
|
||
LastValidLow = double.NaN,
|
||
LastValidClose = double.NaN,
|
||
};
|
||
|
||
SeedFirstValidValues(inputs, ref state);
|
||
|
||
int warmupEnd = Math.Min(period, len);
|
||
ProcessWarmupPhase(inputs, outputs, warmupEnd, factor, ref buffers, ref state);
|
||
ProcessMainLoop(inputs, outputs, warmupEnd, period, factor, ref buffers, ref state);
|
||
}
|
||
finally
|
||
{
|
||
ArrayPool<double>.Shared.Return(rentedHigh);
|
||
ArrayPool<double>.Shared.Return(rentedLow);
|
||
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);
|
||
}
|
||
|
||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||
private static void WriteBandOutputs(scoped BatchOutputs outputs, int i, double smaHigh, double smaLow, double smaClose, double factor)
|
||
{
|
||
double bandWidth = (smaHigh - smaLow) * factor;
|
||
outputs.Middle[i] = smaClose;
|
||
outputs.Upper[i] = smaHigh + bandWidth;
|
||
outputs.Lower[i] = smaLow - bandWidth;
|
||
}
|
||
|
||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||
private static void ProcessWarmupPhase(
|
||
scoped BatchInputs inputs,
|
||
scoped BatchOutputs outputs,
|
||
int warmupEnd,
|
||
double factor,
|
||
ref WorkBuffers buffers,
|
||
ref ScalarState state)
|
||
{
|
||
for (int i = 0; i < warmupEnd; i++)
|
||
{
|
||
var (h, l, c) = GetValidHLC(inputs, i, ref state);
|
||
|
||
state.SumHigh += h;
|
||
state.SumLow += l;
|
||
state.SumClose += c;
|
||
|
||
buffers.High[i] = h;
|
||
buffers.Low[i] = l;
|
||
buffers.Close[i] = c;
|
||
|
||
int count = i + 1;
|
||
WriteBandOutputs(outputs, i, state.SumHigh / count, state.SumLow / count, state.SumClose / count, factor);
|
||
}
|
||
}
|
||
|
||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||
private static void ProcessMainLoop(
|
||
scoped BatchInputs inputs,
|
||
scoped BatchOutputs outputs,
|
||
int startIndex,
|
||
int period,
|
||
double factor,
|
||
ref WorkBuffers buffers,
|
||
ref ScalarState state)
|
||
{
|
||
int len = inputs.Close.Length;
|
||
for (int i = startIndex; i < len; i++)
|
||
{
|
||
var (h, l, c) = GetValidHLC(inputs, i, ref state);
|
||
|
||
state.SumHigh = state.SumHigh - buffers.High[state.BufferIndex] + h;
|
||
state.SumLow = state.SumLow - buffers.Low[state.BufferIndex] + l;
|
||
state.SumClose = state.SumClose - buffers.Close[state.BufferIndex] + c;
|
||
|
||
buffers.High[state.BufferIndex] = h;
|
||
buffers.Low[state.BufferIndex] = l;
|
||
buffers.Close[state.BufferIndex] = c;
|
||
|
||
state.BufferIndex++;
|
||
if (state.BufferIndex >= period) state.BufferIndex = 0;
|
||
|
||
WriteBandOutputs(outputs, i, state.SumHigh / period, state.SumLow / period, state.SumClose / period, factor);
|
||
|
||
state.TickCount++;
|
||
if (state.TickCount >= ResyncInterval)
|
||
{
|
||
ResyncSums(period, ref buffers, ref state);
|
||
}
|
||
}
|
||
}
|
||
|
||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||
private static void ResyncSums(int period, ref WorkBuffers buffers, ref ScalarState state)
|
||
{
|
||
state.TickCount = 0;
|
||
ReadOnlySpan<double> highSpan = buffers.High[..period];
|
||
ReadOnlySpan<double> lowSpan = buffers.Low[..period];
|
||
ReadOnlySpan<double> closeSpan = buffers.Close[..period];
|
||
state.SumHigh = highSpan.SumSIMD();
|
||
state.SumLow = lowSpan.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 = 2.0)
|
||
{
|
||
var accBands = new AccBands(period, factor);
|
||
var results = accBands.Update(source);
|
||
return (results, accBands);
|
||
}
|
||
} |