mirror of
https://github.com/mihakralj/QuanTAlib.git
synced 2026-08-19 02:58:05 +00:00
Refactor error handling and calculations in TheilU, Wmape, and TukeyBiweight classes; update buffer handling for consistency
- Updated buffer handling in TheilU and Wmape classes to ensure consistency after adding new values. - Changed the resync interval constant in TukeyBiweight for better clarity. - Refactored state structures to record structs in Gauss, Hann, Hp, Hpf, Kalman, Loess, Notch, and other filter classes for improved performance and readability. - Enhanced numerical stability in Mama class calculations using Fused Multiply-Add (FMA) for precision. - Added comprehensive tests for Atan2 validation to compare .NET's Math.Atan2 with PineScript's implementation, ensuring accuracy across various edge cases. - Updated NDepend badges to reflect changes in classes, methods, and lines of code.
This commit is contained in:
@@ -0,0 +1,189 @@
|
||||
using System.Runtime.CompilerServices;
|
||||
|
||||
namespace QuanTAlib;
|
||||
|
||||
/// <summary>
|
||||
/// A monotonic deque for O(1) amortized sliding window min/max queries.
|
||||
/// Maintains elements in strictly monotonic order (non-increasing for max, non-decreasing for min).
|
||||
/// Used by channel indicators (Donchian, MinMax) for efficient rolling extrema.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Algorithm: For each new element, expire indices outside the window, then pop elements
|
||||
/// from the back that would violate monotonicity, then push the new index.
|
||||
/// Time complexity: O(1) amortized per operation (each element pushed/popped at most once).
|
||||
/// Space complexity: O(period) for the deque array.
|
||||
/// </remarks>
|
||||
[SkipLocalsInit]
|
||||
public sealed class MonotonicDeque
|
||||
{
|
||||
private readonly int[] _deque;
|
||||
private readonly int _period;
|
||||
private int _head;
|
||||
private int _count;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the current front index (the index of the current extremum).
|
||||
/// </summary>
|
||||
public int FrontIndex => _count > 0 ? _deque[_head] : -1;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the current element count in the deque.
|
||||
/// </summary>
|
||||
public int Count => _count;
|
||||
|
||||
/// <summary>
|
||||
/// Creates a new monotonic deque with the specified period.
|
||||
/// </summary>
|
||||
/// <param name="period">The sliding window size.</param>
|
||||
public MonotonicDeque(int period)
|
||||
{
|
||||
if (period <= 0)
|
||||
throw new ArgumentOutOfRangeException(nameof(period), "Period must be greater than 0");
|
||||
|
||||
_period = period;
|
||||
_deque = new int[period];
|
||||
_head = 0;
|
||||
_count = 0;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Pushes a value for maximum tracking (maintains non-increasing order).
|
||||
/// Smaller or equal values are removed from the back before pushing.
|
||||
/// </summary>
|
||||
/// <param name="logicalIndex">The logical index of the value (used for expiration).</param>
|
||||
/// <param name="value">The value to push.</param>
|
||||
/// <param name="buffer">The circular buffer containing values (indexed by logicalIndex % period).</param>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public void PushMax(long logicalIndex, double value, double[] buffer)
|
||||
{
|
||||
// Expire old indices outside the window
|
||||
long expire = logicalIndex - _period;
|
||||
while (_count > 0 && _deque[_head] <= expire)
|
||||
{
|
||||
_head = (_head + 1) % _period;
|
||||
_count--;
|
||||
}
|
||||
|
||||
// Pop elements from back that are <= value (maintain non-increasing order)
|
||||
while (_count > 0)
|
||||
{
|
||||
int backIdx = (_head + _count - 1) % _period;
|
||||
int bufIdx = _deque[backIdx] % _period;
|
||||
if (buffer[bufIdx] <= value)
|
||||
{
|
||||
_count--;
|
||||
}
|
||||
else
|
||||
{
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Push new index
|
||||
int tail = (_head + _count) % _period;
|
||||
_deque[tail] = (int)logicalIndex;
|
||||
_count++;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Pushes a value for minimum tracking (maintains non-decreasing order).
|
||||
/// Larger or equal values are removed from the back before pushing.
|
||||
/// </summary>
|
||||
/// <param name="logicalIndex">The logical index of the value (used for expiration).</param>
|
||||
/// <param name="value">The value to push.</param>
|
||||
/// <param name="buffer">The circular buffer containing values (indexed by logicalIndex % period).</param>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public void PushMin(long logicalIndex, double value, double[] buffer)
|
||||
{
|
||||
// Expire old indices outside the window
|
||||
long expire = logicalIndex - _period;
|
||||
while (_count > 0 && _deque[_head] <= expire)
|
||||
{
|
||||
_head = (_head + 1) % _period;
|
||||
_count--;
|
||||
}
|
||||
|
||||
// Pop elements from back that are >= value (maintain non-decreasing order)
|
||||
while (_count > 0)
|
||||
{
|
||||
int backIdx = (_head + _count - 1) % _period;
|
||||
int bufIdx = _deque[backIdx] % _period;
|
||||
if (buffer[bufIdx] >= value)
|
||||
{
|
||||
_count--;
|
||||
}
|
||||
else
|
||||
{
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Push new index
|
||||
int tail = (_head + _count) % _period;
|
||||
_deque[tail] = (int)logicalIndex;
|
||||
_count++;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the current extremum value from the buffer.
|
||||
/// </summary>
|
||||
/// <param name="buffer">The circular buffer containing values.</param>
|
||||
/// <returns>The value at the front of the deque, or NaN if empty.</returns>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public double GetExtremum(double[] buffer)
|
||||
{
|
||||
return _count > 0 ? buffer[_deque[_head] % _period] : double.NaN;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Resets the deque to empty state.
|
||||
/// </summary>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public void Reset()
|
||||
{
|
||||
_head = 0;
|
||||
_count = 0;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Rebuilds the max deque from scratch using the buffer contents.
|
||||
/// Used after bar corrections (isNew=false) to maintain consistency.
|
||||
/// </summary>
|
||||
/// <param name="buffer">The circular buffer containing values.</param>
|
||||
/// <param name="currentIndex">The current logical index.</param>
|
||||
/// <param name="count">The number of valid elements in the buffer.</param>
|
||||
public void RebuildMax(double[] buffer, long currentIndex, int count)
|
||||
{
|
||||
Reset();
|
||||
if (count == 0) return;
|
||||
|
||||
long startLogical = currentIndex - count + 1;
|
||||
for (int i = 0; i < count; i++)
|
||||
{
|
||||
long logicalIndex = startLogical + i;
|
||||
int bufIdx = (int)(logicalIndex % _period);
|
||||
PushMax(logicalIndex, buffer[bufIdx], buffer);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Rebuilds the min deque from scratch using the buffer contents.
|
||||
/// Used after bar corrections (isNew=false) to maintain consistency.
|
||||
/// </summary>
|
||||
/// <param name="buffer">The circular buffer containing values.</param>
|
||||
/// <param name="currentIndex">The current logical index.</param>
|
||||
/// <param name="count">The number of valid elements in the buffer.</param>
|
||||
public void RebuildMin(double[] buffer, long currentIndex, int count)
|
||||
{
|
||||
Reset();
|
||||
if (count == 0) return;
|
||||
|
||||
long startLogical = currentIndex - count + 1;
|
||||
for (int i = 0; i < count; i++)
|
||||
{
|
||||
long logicalIndex = startLogical + i;
|
||||
int bufIdx = (int)(logicalIndex % _period);
|
||||
PushMin(logicalIndex, buffer[bufIdx], buffer);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -31,6 +31,13 @@ public sealed class RingBuffer : IEnumerable<double>
|
||||
private double _savedSum;
|
||||
private double _savedValue;
|
||||
|
||||
/// <summary>
|
||||
/// Immutable snapshot token for multi-buffer scenarios.
|
||||
/// Allows capturing and restoring buffer state without using the built-in single snapshot.
|
||||
/// </summary>
|
||||
[StructLayout(LayoutKind.Auto)]
|
||||
public readonly record struct SnapshotToken(int Head, int Count, double Sum, double Value);
|
||||
|
||||
/// <summary>
|
||||
/// Creates a new RingBuffer with the specified capacity.
|
||||
/// Uses pinned memory for SIMD compatibility.
|
||||
@@ -241,9 +248,7 @@ public sealed class RingBuffer : IEnumerable<double>
|
||||
private double GetAt(Index index)
|
||||
{
|
||||
int actualIndex = index.IsFromEnd ? _count - index.Value : index.Value;
|
||||
#pragma warning disable S3236 // Caller information arguments should not be provided explicitly - intentionally using cleaner parameter name
|
||||
ArgumentOutOfRangeException.ThrowIfGreaterThanOrEqual((uint)actualIndex, (uint)_count, nameof(index));
|
||||
#pragma warning restore S3236
|
||||
|
||||
int start = _count == Capacity ? _head : 0;
|
||||
int bufferIdx = (start + actualIndex) % Capacity;
|
||||
@@ -254,9 +259,7 @@ public sealed class RingBuffer : IEnumerable<double>
|
||||
private void SetAt(Index index, double value)
|
||||
{
|
||||
int actualIndex = index.IsFromEnd ? _count - index.Value : index.Value;
|
||||
#pragma warning disable S3236 // Caller information arguments should not be provided explicitly - intentionally using cleaner parameter name
|
||||
ArgumentOutOfRangeException.ThrowIfGreaterThanOrEqual((uint)actualIndex, (uint)_count, nameof(index));
|
||||
#pragma warning restore S3236
|
||||
|
||||
int start = _count == Capacity ? _head : 0;
|
||||
int bufferIdx = (start + actualIndex) % Capacity;
|
||||
@@ -272,7 +275,7 @@ public sealed class RingBuffer : IEnumerable<double>
|
||||
/// If wrapped, returns span over a copy.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// <para><b> Allocation Warning:</b> When the buffer wraps around (i.e., when data spans
|
||||
/// <para><b> Allocation Warning:</b> When the buffer wraps around (i.e., when data spans
|
||||
/// from the end of the internal array back to the beginning), this method allocates a new
|
||||
/// array via <see cref="ToArray"/> to return contiguous data. For allocation-free iteration
|
||||
/// over wrapped buffers, use <see cref="GetSequencedSpans"/> instead.</para>
|
||||
@@ -569,6 +572,32 @@ public sealed class RingBuffer : IEnumerable<double>
|
||||
_buffer[_head] = _savedValue;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a snapshot token that captures the current buffer state.
|
||||
/// Use this for multi-buffer scenarios where you need to snapshot multiple buffers atomically.
|
||||
/// Must be called BEFORE adding a new value if you intend to restore later.
|
||||
/// </summary>
|
||||
/// <returns>An immutable token containing the buffer state.</returns>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public SnapshotToken GetSnapshot()
|
||||
{
|
||||
return new SnapshotToken(_head, _count, _sum, _buffer[_head]);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Restores the buffer to the state captured in the provided snapshot token.
|
||||
/// Use this for multi-buffer scenarios where you need to restore multiple buffers atomically.
|
||||
/// </summary>
|
||||
/// <param name="token">The snapshot token to restore from.</param>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public void RestoreSnapshot(SnapshotToken token)
|
||||
{
|
||||
_head = token.Head;
|
||||
_count = token.Count;
|
||||
_sum = token.Sum;
|
||||
_buffer[_head] = token.Value;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns an enumerator that iterates through the buffer in chronological order.
|
||||
/// </summary>
|
||||
|
||||
@@ -14,6 +14,18 @@ namespace QuanTAlib;
|
||||
/// </summary>
|
||||
public static class ErrorHelpers
|
||||
{
|
||||
/// <summary>
|
||||
/// Default stack allocation threshold for temporary buffers.
|
||||
/// 256 doubles = 2KB, safe margin for nested calls on 1MB thread stack.
|
||||
/// Beyond this threshold, ArrayPool is used instead of stackalloc.
|
||||
/// </summary>
|
||||
public const int StackAllocThreshold = 256;
|
||||
|
||||
/// <summary>
|
||||
/// Default resync interval for running sums to correct floating-point drift.
|
||||
/// </summary>
|
||||
public const int DefaultResyncInterval = 1000;
|
||||
|
||||
private const string SpanLengthMismatchMessage = "All spans must have the same length";
|
||||
|
||||
/// <summary>
|
||||
@@ -434,7 +446,6 @@ public static class ErrorHelpers
|
||||
if (len == 0)
|
||||
return;
|
||||
|
||||
const int StackAllocThreshold = 256;
|
||||
double[]? rented = null;
|
||||
|
||||
#pragma warning disable S1121 // Assignments should not be made from within sub-expressions
|
||||
@@ -508,7 +519,6 @@ public static class ErrorHelpers
|
||||
if (len == 0)
|
||||
return;
|
||||
|
||||
const int StackAllocThreshold = 256;
|
||||
double[]? rented = null;
|
||||
|
||||
#pragma warning disable S1121 // Assignments should not be made from within sub-expressions
|
||||
@@ -584,7 +594,6 @@ public static class ErrorHelpers
|
||||
if (len == 0)
|
||||
return;
|
||||
|
||||
const int StackAllocThreshold = 256;
|
||||
double[]? rentedErrors = null;
|
||||
double[]? rentedWeights = null;
|
||||
|
||||
@@ -661,10 +670,49 @@ public static class ErrorHelpers
|
||||
}
|
||||
}
|
||||
|
||||
#region Private Helpers
|
||||
|
||||
/// <summary>
|
||||
/// Sanitizes input spans by replacing NaN/Infinity values with the last valid value.
|
||||
/// Writes sanitized values to output spans for use in batch calculations.
|
||||
/// </summary>
|
||||
/// <param name="actual">Input actual values</param>
|
||||
/// <param name="predicted">Input predicted values</param>
|
||||
/// <param name="actualOut">Output sanitized actual values</param>
|
||||
/// <param name="predictedOut">Output sanitized predicted values</param>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
private static double FindFirstValidValue(ReadOnlySpan<double> span)
|
||||
public static void SanitizeInputs(
|
||||
ReadOnlySpan<double> actual,
|
||||
ReadOnlySpan<double> predicted,
|
||||
Span<double> actualOut,
|
||||
Span<double> predictedOut)
|
||||
{
|
||||
if (actual.Length != predicted.Length || actual.Length != actualOut.Length || actual.Length != predictedOut.Length)
|
||||
throw new ArgumentException(SpanLengthMismatchMessage, nameof(predictedOut));
|
||||
|
||||
int len = actual.Length;
|
||||
if (len == 0)
|
||||
return;
|
||||
|
||||
double lastValidActual = FindFirstValidValue(actual);
|
||||
double lastValidPredicted = FindFirstValidValue(predicted);
|
||||
|
||||
for (int i = 0; i < len; i++)
|
||||
{
|
||||
double act = actual[i];
|
||||
double pred = predicted[i];
|
||||
|
||||
if (double.IsFinite(act)) lastValidActual = act; else act = lastValidActual;
|
||||
if (double.IsFinite(pred)) lastValidPredicted = pred; else pred = lastValidPredicted;
|
||||
|
||||
actualOut[i] = act;
|
||||
predictedOut[i] = pred;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Finds the first finite value in a span, or returns 0.0 if none found.
|
||||
/// </summary>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static double FindFirstValidValue(ReadOnlySpan<double> span)
|
||||
{
|
||||
for (int i = 0; i < span.Length; i++)
|
||||
{
|
||||
@@ -674,6 +722,8 @@ public static class ErrorHelpers
|
||||
return 0.0;
|
||||
}
|
||||
|
||||
#region Private Helpers
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
private static bool IsDataClean(ReadOnlySpan<double> actual, ReadOnlySpan<double> predicted)
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user