mirror of
https://github.com/mihakralj/QuanTAlib.git
synced 2026-08-18 18:48:05 +00:00
Add CopyTo method in RingBuffer and optimize JMA and SMA implementations
This commit is contained in:
@@ -412,6 +412,29 @@ public sealed class RingBuffer : IEnumerable<double>
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Copies elements to a destination span in chronological order.
|
||||
/// Destination must have at least Count elements.
|
||||
/// </summary>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public void CopyTo(Span<double> destination)
|
||||
{
|
||||
if (_count == 0) return;
|
||||
|
||||
int start = _count == _capacity ? _head : 0;
|
||||
|
||||
if (start + _count <= _capacity)
|
||||
{
|
||||
_buffer.AsSpan(start, _count).CopyTo(destination);
|
||||
}
|
||||
else
|
||||
{
|
||||
int firstPartLength = _capacity - start;
|
||||
_buffer.AsSpan(start, firstPartLength).CopyTo(destination);
|
||||
_buffer.AsSpan(0, _count - firstPartLength).CopyTo(destination.Slice(firstPartLength));
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a copy of the current state for bar correction support.
|
||||
/// </summary>
|
||||
|
||||
+11
-30
@@ -30,7 +30,6 @@ public sealed class Jma : AbstractBase
|
||||
// Buffers
|
||||
private readonly RingBuffer _devBuffer;
|
||||
private readonly RingBuffer _volBuffer;
|
||||
private readonly double[] _sorted;
|
||||
private readonly TValuePublishedHandler _handler;
|
||||
|
||||
// Streaming state (current + previous snapshot for isNew=false)
|
||||
@@ -102,7 +101,6 @@ public sealed class Jma : AbstractBase
|
||||
|
||||
_devBuffer = new RingBuffer(DevWindowSize);
|
||||
_volBuffer = new RingBuffer(VolWindowSize);
|
||||
_sorted = new double[VolWindowSize];
|
||||
|
||||
Reset();
|
||||
}
|
||||
@@ -120,7 +118,6 @@ public sealed class Jma : AbstractBase
|
||||
_p_state = default;
|
||||
_devBuffer.Clear();
|
||||
_volBuffer.Clear();
|
||||
Array.Clear(_sorted, 0, _sorted.Length);
|
||||
Last = default;
|
||||
}
|
||||
|
||||
@@ -262,35 +259,17 @@ public sealed class Jma : AbstractBase
|
||||
|
||||
source.Times.CopyTo(tSpan);
|
||||
|
||||
// Use static Calculate for performance
|
||||
// But JMA has complex parameters, so we need to pass them.
|
||||
// We can use the instance to calculate, but we need to be careful about state.
|
||||
// Or we can just loop using Step, which is what the original code did.
|
||||
// Since JMA is complex and not easily vectorizable, looping is fine.
|
||||
// But we should restore state afterwards.
|
||||
|
||||
// RingBuffers are reference types, so we need to clone them or replay.
|
||||
// Replaying is safer and cleaner for complex state.
|
||||
|
||||
// Reset and calculate in a single pass.
|
||||
// The IIR filter state after processing the full series is mathematically correct.
|
||||
// No need for a second replay - that would truncate the infinite impulse response
|
||||
// and actually reduce precision.
|
||||
Reset();
|
||||
for (int i = 0; i < len; i++)
|
||||
{
|
||||
double j = Step(source.Values[i], true);
|
||||
vSpan[i] = j;
|
||||
}
|
||||
|
||||
// Restore state by replaying history
|
||||
// JMA needs a lot of history (128 bars for volatility).
|
||||
Reset();
|
||||
int lookback = Math.Max(VolWindowSize + 10, WarmupPeriod + 10);
|
||||
int startIndex = Math.Max(0, len - lookback);
|
||||
for (int i = startIndex; i < len; i++)
|
||||
{
|
||||
Step(source.Values[i], true);
|
||||
vSpan[i] = Step(source.Values[i], true);
|
||||
}
|
||||
|
||||
Last = new TValue(tSpan[len - 1], vSpan[len - 1]);
|
||||
|
||||
return new TSeries(t, v);
|
||||
}
|
||||
|
||||
@@ -338,9 +317,11 @@ public sealed class Jma : AbstractBase
|
||||
return fallback;
|
||||
}
|
||||
|
||||
// Copy current buffer to _sorted for sorting
|
||||
_volBuffer.CopyTo(_sorted, 0);
|
||||
Array.Sort(_sorted, 0, count);
|
||||
// Stack-allocate scratch buffer for sorting (max 128 * 8 bytes = 1KB)
|
||||
// This eliminates the heap-allocated _sorted field and improves cache locality
|
||||
Span<double> sorted = stackalloc double[count];
|
||||
_volBuffer.CopyTo(sorted);
|
||||
sorted.Sort();
|
||||
|
||||
int start, end;
|
||||
if (count >= VolWindowSize)
|
||||
@@ -364,6 +345,6 @@ public sealed class Jma : AbstractBase
|
||||
if (end >= count) end = count - 1;
|
||||
|
||||
int len = end - start + 1;
|
||||
return ((ReadOnlySpan<double>)_sorted.AsSpan(start, len)).SumSIMD() / len;
|
||||
return ((ReadOnlySpan<double>)sorted.Slice(start, len)).SumSIMD() / len;
|
||||
}
|
||||
}
|
||||
|
||||
+74
-67
@@ -1,3 +1,4 @@
|
||||
using System.Buffers;
|
||||
using System.Numerics;
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Runtime.InteropServices;
|
||||
@@ -35,7 +36,6 @@ public sealed class Sma : AbstractBase
|
||||
private record struct State(double Sum, double LastInput, double LastValidValue, int TickCount);
|
||||
private State _state;
|
||||
private State _p_state;
|
||||
private double _currentBarValue; // Value added during isNew=true, survives state restore
|
||||
|
||||
private const int ResyncInterval = 1000;
|
||||
|
||||
@@ -192,21 +192,20 @@ public sealed class Sma : AbstractBase
|
||||
double val = GetValidValue(input.Value);
|
||||
UpdateState(val);
|
||||
_state.LastInput = val;
|
||||
_currentBarValue = val; // Store the value added for this bar
|
||||
}
|
||||
else
|
||||
{
|
||||
// Restore scalar state to pre-mutation values
|
||||
_state = _p_state;
|
||||
// Restore scalar state to pre-mutation values (except Sum which we'll recalculate)
|
||||
var restoredState = _p_state;
|
||||
|
||||
double val = GetValidValue(input.Value);
|
||||
// Update sum: remove the value that was added during isNew=true, add the new correction value
|
||||
_state.Sum = _state.Sum - _currentBarValue + val;
|
||||
|
||||
// Update the buffer's newest value and sync its internal sum with our state sum
|
||||
// Update the buffer's newest value - this also updates buffer's internal sum
|
||||
_buffer.UpdateNewest(val);
|
||||
_state.Sum = _buffer.RecalculateSum(); // Ensure sums stay in sync
|
||||
// DO NOT update _currentBarValue here - it must remain the original value from isNew=true
|
||||
|
||||
// Use buffer's authoritative sum (UpdateNewest already did the differential update internally)
|
||||
_state = restoredState with { Sum = _buffer.Sum };
|
||||
// Note: Resync is only done on isNew=true path via UpdateState()
|
||||
}
|
||||
|
||||
double result = _buffer.Count > 0 ? _state.Sum / _buffer.Count : double.NaN;
|
||||
@@ -321,69 +320,78 @@ public sealed class Sma : AbstractBase
|
||||
int len = source.Length;
|
||||
|
||||
const int StackAllocThreshold = 256;
|
||||
Span<double> buffer = period <= StackAllocThreshold
|
||||
? stackalloc double[period]
|
||||
: new double[period];
|
||||
double[]? rented = period > StackAllocThreshold ? ArrayPool<double>.Shared.Rent(period) : null;
|
||||
Span<double> buffer = rented != null
|
||||
? rented.AsSpan(0, period)
|
||||
: stackalloc double[period];
|
||||
|
||||
double sum = 0;
|
||||
double lastValid = double.NaN;
|
||||
|
||||
// Find first valid value to seed lastValid
|
||||
for (int k = 0; k < len; k++)
|
||||
try
|
||||
{
|
||||
if (double.IsFinite(source[k]))
|
||||
double sum = 0;
|
||||
double lastValid = double.NaN;
|
||||
|
||||
// Find first valid value to seed lastValid
|
||||
for (int k = 0; k < len; k++)
|
||||
{
|
||||
lastValid = source[k];
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
int bufferIndex = 0;
|
||||
int i = 0;
|
||||
|
||||
int warmupEnd = Math.Min(period, len);
|
||||
for (; i < warmupEnd; i++)
|
||||
{
|
||||
double val = source[i];
|
||||
if (double.IsFinite(val))
|
||||
lastValid = val;
|
||||
else
|
||||
val = lastValid;
|
||||
|
||||
sum += val;
|
||||
buffer[i] = val;
|
||||
output[i] = sum / (i + 1);
|
||||
}
|
||||
|
||||
int tickCount = 0;
|
||||
for (; i < len; i++)
|
||||
{
|
||||
double val = source[i];
|
||||
if (double.IsFinite(val))
|
||||
lastValid = val;
|
||||
else
|
||||
val = lastValid;
|
||||
|
||||
sum = sum - buffer[bufferIndex] + val;
|
||||
buffer[bufferIndex] = val;
|
||||
|
||||
bufferIndex++;
|
||||
if (bufferIndex >= period)
|
||||
bufferIndex = 0;
|
||||
|
||||
output[i] = sum / period;
|
||||
|
||||
tickCount++;
|
||||
if (tickCount >= ResyncInterval)
|
||||
{
|
||||
tickCount = 0;
|
||||
double recalcSum = 0;
|
||||
for (int k = 0; k < period; k++)
|
||||
if (double.IsFinite(source[k]))
|
||||
{
|
||||
recalcSum += buffer[k];
|
||||
lastValid = source[k];
|
||||
break;
|
||||
}
|
||||
sum = recalcSum;
|
||||
}
|
||||
|
||||
int bufferIndex = 0;
|
||||
int i = 0;
|
||||
|
||||
int warmupEnd = Math.Min(period, len);
|
||||
for (; i < warmupEnd; i++)
|
||||
{
|
||||
double val = source[i];
|
||||
if (double.IsFinite(val))
|
||||
lastValid = val;
|
||||
else
|
||||
val = lastValid;
|
||||
|
||||
sum += val;
|
||||
buffer[i] = val;
|
||||
output[i] = sum / (i + 1);
|
||||
}
|
||||
|
||||
int tickCount = 0;
|
||||
for (; i < len; i++)
|
||||
{
|
||||
double val = source[i];
|
||||
if (double.IsFinite(val))
|
||||
lastValid = val;
|
||||
else
|
||||
val = lastValid;
|
||||
|
||||
sum = sum - buffer[bufferIndex] + val;
|
||||
buffer[bufferIndex] = val;
|
||||
|
||||
bufferIndex++;
|
||||
if (bufferIndex >= period)
|
||||
bufferIndex = 0;
|
||||
|
||||
output[i] = sum / period;
|
||||
|
||||
tickCount++;
|
||||
if (tickCount >= ResyncInterval)
|
||||
{
|
||||
tickCount = 0;
|
||||
double recalcSum = 0;
|
||||
for (int k = 0; k < period; k++)
|
||||
{
|
||||
recalcSum += buffer[k];
|
||||
}
|
||||
sum = recalcSum;
|
||||
}
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
if (rented != null)
|
||||
ArrayPool<double>.Shared.Return(rented);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -605,7 +613,6 @@ public sealed class Sma : AbstractBase
|
||||
_buffer.Clear();
|
||||
_state = default;
|
||||
_p_state = default;
|
||||
_currentBarValue = default;
|
||||
Last = default;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user