diff --git a/lib/core/ringbuffer/RingBuffer.cs b/lib/core/ringbuffer/RingBuffer.cs index a9018708..ebda5222 100644 --- a/lib/core/ringbuffer/RingBuffer.cs +++ b/lib/core/ringbuffer/RingBuffer.cs @@ -412,6 +412,29 @@ public sealed class RingBuffer : IEnumerable } } + /// + /// Copies elements to a destination span in chronological order. + /// Destination must have at least Count elements. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void CopyTo(Span 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)); + } + } + /// /// Creates a copy of the current state for bar correction support. /// diff --git a/lib/trends/jma/Jma.cs b/lib/trends/jma/Jma.cs index 23c0c7cd..3d47dea0 100644 --- a/lib/trends/jma/Jma.cs +++ b/lib/trends/jma/Jma.cs @@ -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 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)_sorted.AsSpan(start, len)).SumSIMD() / len; + return ((ReadOnlySpan)sorted.Slice(start, len)).SumSIMD() / len; } } diff --git a/lib/trends/sma/Sma.cs b/lib/trends/sma/Sma.cs index 7f3a961c..ce6de921 100644 --- a/lib/trends/sma/Sma.cs +++ b/lib/trends/sma/Sma.cs @@ -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 buffer = period <= StackAllocThreshold - ? stackalloc double[period] - : new double[period]; + double[]? rented = period > StackAllocThreshold ? ArrayPool.Shared.Rent(period) : null; + Span 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.Shared.Return(rented); } } @@ -605,7 +613,6 @@ public sealed class Sma : AbstractBase _buffer.Clear(); _state = default; _p_state = default; - _currentBarValue = default; Last = default; } }