mirror of
https://github.com/mihakralj/QuanTAlib.git
synced 2026-08-09 14:30:56 +00:00
feat: Implement SIMD-optimized Multi-Period Simple Moving Average (SMA) with RingBuffer
- Added SmaVector class for calculating multiple SMAs in parallel using SIMD. - Introduced RingBuffer class for efficient circular buffer management with running sum. - Implemented unit tests for RingBuffer to ensure correctness and performance. - Enhanced Add method in RingBuffer to support bar correction semantics. - Added methods for calculating Min and Max using SIMD acceleration. - Improved performance with pinned memory and direct span access for SIMD compatibility.
This commit is contained in:
@@ -0,0 +1,726 @@
|
||||
using System;
|
||||
using System.Collections;
|
||||
using Xunit;
|
||||
using QuanTAlib;
|
||||
|
||||
namespace QuanTAlib.Tests;
|
||||
|
||||
public class RingBufferTests
|
||||
{
|
||||
[Fact]
|
||||
public void Constructor_ValidCapacity_CreatesBuffer()
|
||||
{
|
||||
var buffer = new RingBuffer(10);
|
||||
|
||||
Assert.Equal(10, buffer.Capacity);
|
||||
Assert.Equal(0, buffer.Count);
|
||||
Assert.False(buffer.IsFull);
|
||||
Assert.Equal(0, buffer.Sum);
|
||||
Assert.Equal(0, buffer.Average);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Constructor_ZeroCapacity_ThrowsArgumentException()
|
||||
{
|
||||
Assert.Throws<ArgumentException>(() => new RingBuffer(0));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Constructor_NegativeCapacity_ThrowsArgumentException()
|
||||
{
|
||||
Assert.Throws<ArgumentException>(() => new RingBuffer(-1));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Add_SingleValue_UpdatesState()
|
||||
{
|
||||
var buffer = new RingBuffer(5);
|
||||
|
||||
buffer.Add(10.0);
|
||||
|
||||
Assert.Equal(1, buffer.Count);
|
||||
Assert.Equal(10.0, buffer.Sum);
|
||||
Assert.Equal(10.0, buffer.Average);
|
||||
Assert.Equal(10.0, buffer.Newest);
|
||||
Assert.Equal(10.0, buffer.Oldest);
|
||||
Assert.False(buffer.IsFull);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Add_MultipleValues_UpdatesState()
|
||||
{
|
||||
var buffer = new RingBuffer(5);
|
||||
|
||||
buffer.Add(10.0);
|
||||
buffer.Add(20.0);
|
||||
buffer.Add(30.0);
|
||||
|
||||
Assert.Equal(3, buffer.Count);
|
||||
Assert.Equal(60.0, buffer.Sum);
|
||||
Assert.Equal(20.0, buffer.Average);
|
||||
Assert.Equal(30.0, buffer.Newest);
|
||||
Assert.Equal(10.0, buffer.Oldest);
|
||||
Assert.False(buffer.IsFull);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Add_FillBuffer_BecomesFullAndWraps()
|
||||
{
|
||||
var buffer = new RingBuffer(3);
|
||||
|
||||
buffer.Add(10.0);
|
||||
buffer.Add(20.0);
|
||||
buffer.Add(30.0);
|
||||
|
||||
Assert.Equal(3, buffer.Count);
|
||||
Assert.True(buffer.IsFull);
|
||||
Assert.Equal(60.0, buffer.Sum);
|
||||
Assert.Equal(20.0, buffer.Average);
|
||||
|
||||
// Add one more - should remove 10.0
|
||||
double removed = buffer.Add(40.0);
|
||||
|
||||
Assert.Equal(10.0, removed);
|
||||
Assert.Equal(3, buffer.Count);
|
||||
Assert.True(buffer.IsFull);
|
||||
Assert.Equal(90.0, buffer.Sum); // 20 + 30 + 40
|
||||
Assert.Equal(30.0, buffer.Average);
|
||||
Assert.Equal(40.0, buffer.Newest);
|
||||
Assert.Equal(20.0, buffer.Oldest);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Add_MultipleWraps_MaintainsCorrectState()
|
||||
{
|
||||
var buffer = new RingBuffer(3);
|
||||
|
||||
// Fill and wrap multiple times
|
||||
for (int i = 1; i <= 10; i++)
|
||||
{
|
||||
buffer.Add(i * 10.0);
|
||||
}
|
||||
|
||||
// Should contain: 80, 90, 100
|
||||
Assert.Equal(3, buffer.Count);
|
||||
Assert.True(buffer.IsFull);
|
||||
Assert.Equal(270.0, buffer.Sum); // 80 + 90 + 100
|
||||
Assert.Equal(90.0, buffer.Average);
|
||||
Assert.Equal(100.0, buffer.Newest);
|
||||
Assert.Equal(80.0, buffer.Oldest);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void UpdateNewest_ModifiesLastValue()
|
||||
{
|
||||
var buffer = new RingBuffer(5);
|
||||
|
||||
buffer.Add(10.0);
|
||||
buffer.Add(20.0);
|
||||
buffer.Add(30.0);
|
||||
|
||||
Assert.Equal(60.0, buffer.Sum);
|
||||
|
||||
buffer.UpdateNewest(35.0);
|
||||
|
||||
Assert.Equal(65.0, buffer.Sum); // 10 + 20 + 35
|
||||
Assert.Equal(35.0, buffer.Newest);
|
||||
Assert.Equal(3, buffer.Count);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void UpdateNewest_EmptyBuffer_DoesNothing()
|
||||
{
|
||||
var buffer = new RingBuffer(5);
|
||||
|
||||
buffer.UpdateNewest(100.0); // Should not throw
|
||||
|
||||
Assert.Equal(0, buffer.Count);
|
||||
Assert.Equal(0, buffer.Sum);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Indexer_AccessesCorrectValues()
|
||||
{
|
||||
var buffer = new RingBuffer(5);
|
||||
|
||||
buffer.Add(10.0);
|
||||
buffer.Add(20.0);
|
||||
buffer.Add(30.0);
|
||||
|
||||
// Index 0 = oldest, Index 2 = newest
|
||||
Assert.Equal(10.0, buffer[0]);
|
||||
Assert.Equal(20.0, buffer[1]);
|
||||
Assert.Equal(30.0, buffer[2]);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Indexer_AfterWrap_AccessesCorrectValues()
|
||||
{
|
||||
var buffer = new RingBuffer(3);
|
||||
|
||||
buffer.Add(10.0);
|
||||
buffer.Add(20.0);
|
||||
buffer.Add(30.0);
|
||||
buffer.Add(40.0); // Wraps, removes 10
|
||||
|
||||
// Should contain: 20, 30, 40
|
||||
Assert.Equal(20.0, buffer[0]);
|
||||
Assert.Equal(30.0, buffer[1]);
|
||||
Assert.Equal(40.0, buffer[2]);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Indexer_OutOfRange_ThrowsException()
|
||||
{
|
||||
var buffer = new RingBuffer(5);
|
||||
|
||||
buffer.Add(10.0);
|
||||
buffer.Add(20.0);
|
||||
|
||||
// Valid indices are 0 and 1 (2 elements)
|
||||
// Index 2 should throw ArgumentOutOfRangeException
|
||||
Assert.Throws<ArgumentOutOfRangeException>(() => _ = buffer[(Index)2]);
|
||||
Assert.Throws<ArgumentOutOfRangeException>(() => _ = buffer[(Index)10]);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Clear_ResetsState()
|
||||
{
|
||||
var buffer = new RingBuffer(5);
|
||||
|
||||
buffer.Add(10.0);
|
||||
buffer.Add(20.0);
|
||||
buffer.Add(30.0);
|
||||
|
||||
buffer.Clear();
|
||||
|
||||
Assert.Equal(0, buffer.Count);
|
||||
Assert.Equal(0, buffer.Sum);
|
||||
Assert.Equal(0, buffer.Average);
|
||||
Assert.False(buffer.IsFull);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Clear_AllowsReuse()
|
||||
{
|
||||
var buffer = new RingBuffer(3);
|
||||
|
||||
buffer.Add(10.0);
|
||||
buffer.Add(20.0);
|
||||
buffer.Add(30.0);
|
||||
buffer.Clear();
|
||||
|
||||
buffer.Add(100.0);
|
||||
|
||||
Assert.Equal(1, buffer.Count);
|
||||
Assert.Equal(100.0, buffer.Sum);
|
||||
Assert.Equal(100.0, buffer.Newest);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Clone_CreatesIndependentCopy()
|
||||
{
|
||||
var buffer = new RingBuffer(5);
|
||||
|
||||
buffer.Add(10.0);
|
||||
buffer.Add(20.0);
|
||||
buffer.Add(30.0);
|
||||
|
||||
var clone = buffer.Clone();
|
||||
|
||||
// Verify clone has same state
|
||||
Assert.Equal(buffer.Count, clone.Count);
|
||||
Assert.Equal(buffer.Sum, clone.Sum);
|
||||
Assert.Equal(buffer.Newest, clone.Newest);
|
||||
Assert.Equal(buffer.Oldest, clone.Oldest);
|
||||
|
||||
// Modify original - clone should be unaffected
|
||||
buffer.Add(40.0);
|
||||
|
||||
Assert.Equal(4, buffer.Count);
|
||||
Assert.Equal(3, clone.Count);
|
||||
Assert.Equal(100.0, buffer.Sum);
|
||||
Assert.Equal(60.0, clone.Sum);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CopyFrom_CopiesState()
|
||||
{
|
||||
var source = new RingBuffer(5);
|
||||
var target = new RingBuffer(5);
|
||||
|
||||
source.Add(10.0);
|
||||
source.Add(20.0);
|
||||
source.Add(30.0);
|
||||
|
||||
target.Add(100.0); // Different initial state
|
||||
|
||||
target.CopyFrom(source);
|
||||
|
||||
Assert.Equal(source.Count, target.Count);
|
||||
Assert.Equal(source.Sum, target.Sum);
|
||||
Assert.Equal(source.Newest, target.Newest);
|
||||
Assert.Equal(source.Oldest, target.Oldest);
|
||||
|
||||
// Verify independence after copy
|
||||
source.Add(40.0);
|
||||
Assert.NotEqual(source.Sum, target.Sum);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CopyFrom_DifferentCapacity_ThrowsException()
|
||||
{
|
||||
var source = new RingBuffer(5);
|
||||
var target = new RingBuffer(10);
|
||||
|
||||
Assert.Throws<ArgumentException>(() => target.CopyFrom(source));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GetSpan_ReturnsChronologicalOrder()
|
||||
{
|
||||
var buffer = new RingBuffer(5);
|
||||
|
||||
buffer.Add(10.0);
|
||||
buffer.Add(20.0);
|
||||
buffer.Add(30.0);
|
||||
|
||||
var span = buffer.GetSpan();
|
||||
|
||||
Assert.Equal(3, span.Length);
|
||||
Assert.Equal(10.0, span[0]);
|
||||
Assert.Equal(20.0, span[1]);
|
||||
Assert.Equal(30.0, span[2]);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GetSpan_AfterWrap_ReturnsChronologicalOrder()
|
||||
{
|
||||
var buffer = new RingBuffer(3);
|
||||
|
||||
buffer.Add(10.0);
|
||||
buffer.Add(20.0);
|
||||
buffer.Add(30.0);
|
||||
buffer.Add(40.0);
|
||||
buffer.Add(50.0);
|
||||
|
||||
var span = buffer.GetSpan();
|
||||
|
||||
// Should be: 30, 40, 50
|
||||
Assert.Equal(3, span.Length);
|
||||
Assert.Equal(30.0, span[0]);
|
||||
Assert.Equal(40.0, span[1]);
|
||||
Assert.Equal(50.0, span[2]);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GetSpan_EmptyBuffer_ReturnsEmpty()
|
||||
{
|
||||
var buffer = new RingBuffer(5);
|
||||
|
||||
var span = buffer.GetSpan();
|
||||
|
||||
Assert.True(span.IsEmpty);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Min_ReturnsMinimumValue()
|
||||
{
|
||||
var buffer = new RingBuffer(5);
|
||||
|
||||
buffer.Add(30.0);
|
||||
buffer.Add(10.0);
|
||||
buffer.Add(50.0);
|
||||
buffer.Add(20.0);
|
||||
buffer.Add(40.0);
|
||||
|
||||
Assert.Equal(10.0, buffer.Min());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Max_ReturnsMaximumValue()
|
||||
{
|
||||
var buffer = new RingBuffer(5);
|
||||
|
||||
buffer.Add(30.0);
|
||||
buffer.Add(10.0);
|
||||
buffer.Add(50.0);
|
||||
buffer.Add(20.0);
|
||||
buffer.Add(40.0);
|
||||
|
||||
Assert.Equal(50.0, buffer.Max());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Min_EmptyBuffer_ReturnsNaN()
|
||||
{
|
||||
var buffer = new RingBuffer(5);
|
||||
|
||||
Assert.True(double.IsNaN(buffer.Min()));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Max_EmptyBuffer_ReturnsNaN()
|
||||
{
|
||||
var buffer = new RingBuffer(5);
|
||||
|
||||
Assert.True(double.IsNaN(buffer.Max()));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Enumerator_IteratesInChronologicalOrder()
|
||||
{
|
||||
var buffer = new RingBuffer(5);
|
||||
|
||||
buffer.Add(10.0);
|
||||
buffer.Add(20.0);
|
||||
buffer.Add(30.0);
|
||||
|
||||
var values = new List<double>();
|
||||
foreach (var v in buffer)
|
||||
{
|
||||
values.Add(v);
|
||||
}
|
||||
|
||||
Assert.Equal(3, values.Count);
|
||||
Assert.Equal(10.0, values[0]);
|
||||
Assert.Equal(20.0, values[1]);
|
||||
Assert.Equal(30.0, values[2]);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Enumerator_AfterWrap_IteratesInChronologicalOrder()
|
||||
{
|
||||
var buffer = new RingBuffer(3);
|
||||
|
||||
buffer.Add(10.0);
|
||||
buffer.Add(20.0);
|
||||
buffer.Add(30.0);
|
||||
buffer.Add(40.0);
|
||||
buffer.Add(50.0);
|
||||
|
||||
var values = new List<double>();
|
||||
foreach (var v in buffer)
|
||||
{
|
||||
values.Add(v);
|
||||
}
|
||||
|
||||
// Should be: 30, 40, 50
|
||||
Assert.Equal(3, values.Count);
|
||||
Assert.Equal(30.0, values[0]);
|
||||
Assert.Equal(40.0, values[1]);
|
||||
Assert.Equal(50.0, values[2]);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Add_WithIsNew_WorksCorrectly()
|
||||
{
|
||||
var buffer = new RingBuffer(5);
|
||||
|
||||
buffer.Add(10.0, isNew: true);
|
||||
buffer.Add(20.0, isNew: true);
|
||||
buffer.Add(25.0, isNew: false); // Should update 20.0 to 25.0
|
||||
|
||||
Assert.Equal(2, buffer.Count);
|
||||
Assert.Equal(25.0, buffer.Newest);
|
||||
Assert.Equal(35.0, buffer.Sum); // 10 + 25
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Indexer_WithIndexType_SupportsFromEnd()
|
||||
{
|
||||
var buffer = new RingBuffer(5);
|
||||
|
||||
buffer.Add(10.0);
|
||||
buffer.Add(20.0);
|
||||
buffer.Add(30.0);
|
||||
|
||||
Assert.Equal(30.0, buffer[^1]); // Newest
|
||||
Assert.Equal(20.0, buffer[^2]);
|
||||
Assert.Equal(10.0, buffer[^3]); // Oldest
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Newest_EmptyBuffer_ReturnsZero()
|
||||
{
|
||||
var buffer = new RingBuffer(5);
|
||||
|
||||
Assert.Equal(0, buffer.Newest);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Oldest_EmptyBuffer_ReturnsZero()
|
||||
{
|
||||
var buffer = new RingBuffer(5);
|
||||
|
||||
Assert.Equal(0, buffer.Oldest);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Average_EmptyBuffer_ReturnsZero()
|
||||
{
|
||||
var buffer = new RingBuffer(5);
|
||||
|
||||
Assert.Equal(0, buffer.Average);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GetInternalSpan_ReturnsFullBuffer()
|
||||
{
|
||||
var buffer = new RingBuffer(5);
|
||||
|
||||
buffer.Add(10.0);
|
||||
buffer.Add(20.0);
|
||||
|
||||
var span = buffer.GetInternalSpan();
|
||||
|
||||
Assert.Equal(5, span.Length); // Full capacity, not count
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ToArray_AfterWrap_ReturnsChronologicalOrder()
|
||||
{
|
||||
var buffer = new RingBuffer(3);
|
||||
|
||||
buffer.Add(10.0);
|
||||
buffer.Add(20.0);
|
||||
buffer.Add(30.0);
|
||||
buffer.Add(40.0); // Wraps
|
||||
|
||||
var arr = buffer.ToArray();
|
||||
|
||||
Assert.Equal(3, arr.Length);
|
||||
Assert.Equal(20.0, arr[0]);
|
||||
Assert.Equal(30.0, arr[1]);
|
||||
Assert.Equal(40.0, arr[2]);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CopyTo_AfterWrap_CopiesInChronologicalOrder()
|
||||
{
|
||||
var buffer = new RingBuffer(3);
|
||||
|
||||
buffer.Add(10.0);
|
||||
buffer.Add(20.0);
|
||||
buffer.Add(30.0);
|
||||
buffer.Add(40.0); // Wraps
|
||||
|
||||
var dest = new double[5];
|
||||
buffer.CopyTo(dest, 1);
|
||||
|
||||
Assert.Equal(0, dest[0]); // Untouched
|
||||
Assert.Equal(20.0, dest[1]);
|
||||
Assert.Equal(30.0, dest[2]);
|
||||
Assert.Equal(40.0, dest[3]);
|
||||
Assert.Equal(0, dest[4]); // Untouched
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Indexer_Set_UpdatesValueAndSum()
|
||||
{
|
||||
var buffer = new RingBuffer(5);
|
||||
|
||||
buffer.Add(10.0);
|
||||
buffer.Add(20.0);
|
||||
buffer.Add(30.0);
|
||||
|
||||
Assert.Equal(60.0, buffer.Sum);
|
||||
|
||||
buffer[(Index)1] = 25.0; // Change 20.0 to 25.0
|
||||
|
||||
Assert.Equal(65.0, buffer.Sum);
|
||||
Assert.Equal(25.0, buffer[1]);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Indexer_SetFromEnd_UpdatesValueAndSum()
|
||||
{
|
||||
var buffer = new RingBuffer(5);
|
||||
|
||||
buffer.Add(10.0);
|
||||
buffer.Add(20.0);
|
||||
buffer.Add(30.0);
|
||||
|
||||
buffer[^1] = 35.0; // Change newest (30.0) to 35.0
|
||||
|
||||
Assert.Equal(65.0, buffer.Sum);
|
||||
Assert.Equal(35.0, buffer[^1]);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Min_LargeBuffer_UsesSimd()
|
||||
{
|
||||
var buffer = new RingBuffer(100);
|
||||
|
||||
for (int i = 0; i < 100; i++)
|
||||
{
|
||||
buffer.Add(i + 1); // 1 to 100
|
||||
}
|
||||
|
||||
Assert.Equal(1.0, buffer.Min());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Max_LargeBuffer_UsesSimd()
|
||||
{
|
||||
var buffer = new RingBuffer(100);
|
||||
|
||||
for (int i = 0; i < 100; i++)
|
||||
{
|
||||
buffer.Add(i + 1); // 1 to 100
|
||||
}
|
||||
|
||||
Assert.Equal(100.0, buffer.Max());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ToArray_EmptyBuffer_ReturnsEmpty()
|
||||
{
|
||||
var buffer = new RingBuffer(5);
|
||||
|
||||
var arr = buffer.ToArray();
|
||||
|
||||
Assert.Empty(arr);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CopyTo_EmptyBuffer_DoesNothing()
|
||||
{
|
||||
var buffer = new RingBuffer(5);
|
||||
double[] dest = [1.0, 2.0, 3.0];
|
||||
|
||||
buffer.CopyTo(dest, 0);
|
||||
|
||||
Assert.Equal(1.0, dest[0]);
|
||||
Assert.Equal(2.0, dest[1]);
|
||||
Assert.Equal(3.0, dest[2]);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void InternalBuffer_ReturnsSpan()
|
||||
{
|
||||
var buffer = new RingBuffer(5);
|
||||
|
||||
buffer.Add(10.0);
|
||||
|
||||
var span = buffer.InternalBuffer;
|
||||
|
||||
Assert.Equal(5, span.Length);
|
||||
Assert.Equal(10.0, span[0]);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Enumerator_Reset_AllowsReIteration()
|
||||
{
|
||||
var buffer = new RingBuffer(3);
|
||||
buffer.Add(10.0);
|
||||
buffer.Add(20.0);
|
||||
|
||||
var enumerator = buffer.GetEnumerator();
|
||||
|
||||
// First iteration
|
||||
Assert.True(enumerator.MoveNext());
|
||||
Assert.Equal(10.0, enumerator.Current);
|
||||
Assert.True(enumerator.MoveNext());
|
||||
Assert.Equal(20.0, enumerator.Current);
|
||||
Assert.False(enumerator.MoveNext());
|
||||
|
||||
// Reset and iterate again
|
||||
enumerator.Reset();
|
||||
Assert.True(enumerator.MoveNext());
|
||||
Assert.Equal(10.0, enumerator.Current);
|
||||
|
||||
enumerator.Dispose(); // Coverage for Dispose
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void IEnumerable_GetEnumerator_Works()
|
||||
{
|
||||
var buffer = new RingBuffer(3);
|
||||
buffer.Add(10.0);
|
||||
buffer.Add(20.0);
|
||||
|
||||
IEnumerable<double> enumerable = buffer;
|
||||
var values = new List<double>();
|
||||
foreach (var v in enumerable)
|
||||
{
|
||||
values.Add(v);
|
||||
}
|
||||
|
||||
Assert.Equal(2, values.Count);
|
||||
Assert.Equal(10.0, values[0]);
|
||||
Assert.Equal(20.0, values[1]);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void IEnumerable_NonGeneric_GetEnumerator_Works()
|
||||
{
|
||||
var buffer = new RingBuffer(3);
|
||||
buffer.Add(10.0);
|
||||
buffer.Add(20.0);
|
||||
|
||||
IEnumerable enumerable = buffer;
|
||||
var values = new List<double>();
|
||||
foreach (var v in enumerable)
|
||||
{
|
||||
values.Add((double)v);
|
||||
}
|
||||
|
||||
Assert.Equal(2, values.Count);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Indexer_Set_AfterWrap_UpdatesCorrectly()
|
||||
{
|
||||
var buffer = new RingBuffer(3);
|
||||
|
||||
buffer.Add(10.0);
|
||||
buffer.Add(20.0);
|
||||
buffer.Add(30.0);
|
||||
buffer.Add(40.0); // Wraps - now has 20, 30, 40
|
||||
|
||||
buffer[(Index)0] = 25.0; // Change oldest (20.0) to 25.0
|
||||
|
||||
Assert.Equal(95.0, buffer.Sum); // 25 + 30 + 40
|
||||
Assert.Equal(25.0, buffer[0]);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Add_WithIsNew_EmptyBuffer_AddsValue()
|
||||
{
|
||||
var buffer = new RingBuffer(5);
|
||||
|
||||
buffer.Add(10.0, isNew: false); // isNew=false but buffer empty, should still add
|
||||
|
||||
Assert.Equal(1, buffer.Count);
|
||||
Assert.Equal(10.0, buffer.Newest);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void BarCorrection_Workflow()
|
||||
{
|
||||
// Simulate bar correction (isNew=false) workflow
|
||||
var buffer = new RingBuffer(3);
|
||||
var backup = new RingBuffer(3);
|
||||
|
||||
// Add values as new bars
|
||||
buffer.Add(10.0);
|
||||
backup.CopyFrom(buffer);
|
||||
|
||||
buffer.Add(20.0);
|
||||
backup.CopyFrom(buffer);
|
||||
|
||||
buffer.Add(30.0);
|
||||
backup.CopyFrom(buffer);
|
||||
|
||||
double avgBeforeCorrection = buffer.Average;
|
||||
|
||||
// Simulate correction (isNew=false)
|
||||
buffer.CopyFrom(backup); // Restore previous state
|
||||
buffer.UpdateNewest(35.0); // Update with corrected value
|
||||
|
||||
// Average should reflect the correction
|
||||
Assert.Equal(21.666666666666668, buffer.Average, 1e-10);
|
||||
Assert.NotEqual(avgBeforeCorrection, buffer.Average);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,468 @@
|
||||
using System.Collections;
|
||||
using System.Numerics;
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Runtime.InteropServices;
|
||||
|
||||
namespace QuanTAlib;
|
||||
|
||||
/// <summary>
|
||||
/// A high-performance circular buffer for double values optimized for SIMD operations.
|
||||
/// Uses pinned memory and maintains running sum for O(1) average calculations.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Key characteristics:
|
||||
/// - Fixed capacity set at construction
|
||||
/// - Pinned memory for SIMD compatibility
|
||||
/// - O(1) Add and Sum operations via running sum
|
||||
/// - SIMD-accelerated Min/Max operations
|
||||
/// - Direct span access when buffer is contiguous
|
||||
/// - Thread-unsafe for maximum performance
|
||||
/// </remarks>
|
||||
[SkipLocalsInit]
|
||||
public sealed class RingBuffer : IEnumerable<double>
|
||||
{
|
||||
private readonly double[] _buffer;
|
||||
private readonly int _capacity;
|
||||
private int _head; // Next write position (also start position when full)
|
||||
private int _count; // Current number of elements
|
||||
private double _sum; // Running sum of all elements
|
||||
|
||||
/// <summary>
|
||||
/// Creates a new RingBuffer with the specified capacity.
|
||||
/// Uses pinned memory for SIMD compatibility.
|
||||
/// </summary>
|
||||
/// <param name="capacity">Maximum number of elements (must be > 0)</param>
|
||||
public RingBuffer(int capacity)
|
||||
{
|
||||
if (capacity <= 0)
|
||||
throw new ArgumentException("Capacity must be greater than 0", nameof(capacity));
|
||||
|
||||
_capacity = capacity;
|
||||
_buffer = GC.AllocateArray<double>(capacity, pinned: true);
|
||||
_head = 0;
|
||||
_count = 0;
|
||||
_sum = 0;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Maximum number of elements the buffer can hold.
|
||||
/// </summary>
|
||||
public int Capacity
|
||||
{
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
get => _capacity;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Current number of elements in the buffer.
|
||||
/// </summary>
|
||||
public int Count
|
||||
{
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
get => _count;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// True if the buffer is full (Count == Capacity).
|
||||
/// </summary>
|
||||
public bool IsFull
|
||||
{
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
get => _count == _capacity;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Running sum of all elements in the buffer.
|
||||
/// O(1) operation using maintained running sum.
|
||||
/// </summary>
|
||||
public double Sum
|
||||
{
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
get => _sum;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Average of all elements in the buffer.
|
||||
/// Returns 0 if buffer is empty.
|
||||
/// </summary>
|
||||
public double Average
|
||||
{
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
get => _count > 0 ? _sum / _count : 0;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the newest (most recently added) value.
|
||||
/// </summary>
|
||||
public double Newest
|
||||
{
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
get
|
||||
{
|
||||
if (_count == 0) return 0;
|
||||
int idx = (_head - 1 + _capacity) % _capacity;
|
||||
return _buffer[idx];
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the oldest value in the buffer.
|
||||
/// </summary>
|
||||
public double Oldest
|
||||
{
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
get
|
||||
{
|
||||
if (_count == 0) return 0;
|
||||
// When full, _head points to oldest; otherwise start is 0
|
||||
int start = _count == _capacity ? _head : 0;
|
||||
return _buffer[start];
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets a read-only span over the internal buffer array for direct SIMD access.
|
||||
/// </summary>
|
||||
public ReadOnlySpan<double> InternalBuffer
|
||||
{
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
get => _buffer.AsSpan();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Adds a value to the buffer.
|
||||
/// If full, the oldest value is overwritten and its value is subtracted from the sum.
|
||||
/// Returns the value that was removed (0 if buffer was not full).
|
||||
/// </summary>
|
||||
/// <param name="value">Value to add</param>
|
||||
/// <returns>The removed oldest value, or 0 if buffer was not full</returns>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public double Add(double value)
|
||||
{
|
||||
double removed = 0;
|
||||
|
||||
if (_count == _capacity)
|
||||
{
|
||||
// Buffer is full: remove oldest value from sum
|
||||
removed = _buffer[_head];
|
||||
_sum -= removed;
|
||||
}
|
||||
else
|
||||
{
|
||||
_count++;
|
||||
}
|
||||
|
||||
_buffer[_head] = value;
|
||||
_sum += value;
|
||||
_head = (_head + 1) % _capacity;
|
||||
|
||||
return removed;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Adds a value with support for bar correction semantics.
|
||||
/// </summary>
|
||||
/// <param name="value">Value to add</param>
|
||||
/// <param name="isNew">True for new bar, false for update to current bar</param>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public void Add(double value, bool isNew)
|
||||
{
|
||||
if (isNew || _count == 0)
|
||||
{
|
||||
Add(value);
|
||||
}
|
||||
else
|
||||
{
|
||||
UpdateNewest(value);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Updates the newest (most recently added) value.
|
||||
/// This is used for bar correction (isNew=false semantics).
|
||||
/// </summary>
|
||||
/// <param name="value">New value to replace the newest</param>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public void UpdateNewest(double value)
|
||||
{
|
||||
if (_count == 0) return;
|
||||
|
||||
int idx = (_head - 1 + _capacity) % _capacity;
|
||||
double oldValue = _buffer[idx];
|
||||
_sum -= oldValue;
|
||||
_sum += value;
|
||||
_buffer[idx] = value;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets element at the specified index (0 = oldest, Count-1 = newest).
|
||||
/// Supports negative indexing via Index type.
|
||||
/// </summary>
|
||||
public double this[Index index]
|
||||
{
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
get => GetAt(index);
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
set => SetAt(index, value);
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
private double GetAt(Index index)
|
||||
{
|
||||
int actualIndex = index.IsFromEnd ? _count - index.Value : index.Value;
|
||||
ArgumentOutOfRangeException.ThrowIfGreaterThanOrEqual((uint)actualIndex, (uint)_count);
|
||||
|
||||
int start = _count == _capacity ? _head : 0;
|
||||
int bufferIdx = (start + actualIndex) % _capacity;
|
||||
return _buffer[bufferIdx];
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
private void SetAt(Index index, double value)
|
||||
{
|
||||
int actualIndex = index.IsFromEnd ? _count - index.Value : index.Value;
|
||||
ArgumentOutOfRangeException.ThrowIfGreaterThanOrEqual((uint)actualIndex, (uint)_count);
|
||||
|
||||
int start = _count == _capacity ? _head : 0;
|
||||
int bufferIdx = (start + actualIndex) % _capacity;
|
||||
|
||||
_sum -= _buffer[bufferIdx];
|
||||
_sum += value;
|
||||
_buffer[bufferIdx] = value;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns a span over the buffer contents.
|
||||
/// If buffer is contiguous, returns direct span (SIMD-friendly).
|
||||
/// If wrapped, returns span over a copy.
|
||||
/// </summary>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public ReadOnlySpan<double> GetSpan()
|
||||
{
|
||||
if (_count == 0) return ReadOnlySpan<double>.Empty;
|
||||
|
||||
int start = _count == _capacity ? _head : 0;
|
||||
|
||||
// Check if contiguous (no wrap)
|
||||
if (start + _count <= _capacity)
|
||||
{
|
||||
return new ReadOnlySpan<double>(_buffer, start, _count);
|
||||
}
|
||||
|
||||
// Wrapped - need to copy
|
||||
return new ReadOnlySpan<double>(ToArray());
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns a span over the entire internal buffer (for advanced SIMD use).
|
||||
/// </summary>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public ReadOnlySpan<double> GetInternalSpan() => _buffer.AsSpan();
|
||||
|
||||
/// <summary>
|
||||
/// Returns the maximum value in the buffer using SIMD acceleration.
|
||||
/// </summary>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public double Max()
|
||||
{
|
||||
if (_count == 0) return double.NaN;
|
||||
return MaxSimd();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns the minimum value in the buffer using SIMD acceleration.
|
||||
/// </summary>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public double Min()
|
||||
{
|
||||
if (_count == 0) return double.NaN;
|
||||
return MinSimd();
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)]
|
||||
private double MaxSimd()
|
||||
{
|
||||
var span = GetSpan();
|
||||
var vectorSize = Vector<double>.Count;
|
||||
var maxVector = new Vector<double>(double.MinValue);
|
||||
|
||||
int i = 0;
|
||||
ref double spanRef = ref MemoryMarshal.GetReference(span);
|
||||
|
||||
for (; i <= span.Length - vectorSize; i += vectorSize)
|
||||
{
|
||||
maxVector = Vector.Max(maxVector, Unsafe.As<double, Vector<double>>(ref Unsafe.Add(ref spanRef, i)));
|
||||
}
|
||||
|
||||
double max = double.MinValue;
|
||||
for (int j = 0; j < vectorSize; j++)
|
||||
{
|
||||
max = Math.Max(max, maxVector[j]);
|
||||
}
|
||||
|
||||
for (; i < span.Length; i++)
|
||||
{
|
||||
max = Math.Max(max, span[i]);
|
||||
}
|
||||
|
||||
return max;
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)]
|
||||
private double MinSimd()
|
||||
{
|
||||
var span = GetSpan();
|
||||
var vectorSize = Vector<double>.Count;
|
||||
var minVector = new Vector<double>(double.MaxValue);
|
||||
|
||||
int i = 0;
|
||||
ref double spanRef = ref MemoryMarshal.GetReference(span);
|
||||
|
||||
for (; i <= span.Length - vectorSize; i += vectorSize)
|
||||
{
|
||||
minVector = Vector.Min(minVector, Unsafe.As<double, Vector<double>>(ref Unsafe.Add(ref spanRef, i)));
|
||||
}
|
||||
|
||||
double min = double.MaxValue;
|
||||
for (int j = 0; j < vectorSize; j++)
|
||||
{
|
||||
min = Math.Min(min, minVector[j]);
|
||||
}
|
||||
|
||||
for (; i < span.Length; i++)
|
||||
{
|
||||
min = Math.Min(min, span[i]);
|
||||
}
|
||||
|
||||
return min;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Clears all elements from the buffer.
|
||||
/// </summary>
|
||||
public void Clear()
|
||||
{
|
||||
Array.Clear(_buffer, 0, _buffer.Length);
|
||||
_head = 0;
|
||||
_count = 0;
|
||||
_sum = 0;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Copies the buffer elements to a new array in chronological order.
|
||||
/// </summary>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public double[] ToArray()
|
||||
{
|
||||
if (_count == 0) return Array.Empty<double>();
|
||||
|
||||
double[] array = new double[_count];
|
||||
CopyTo(array, 0);
|
||||
return array;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Copies elements to destination array starting at destinationIndex.
|
||||
/// </summary>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public void CopyTo(double[] destination, int destinationIndex)
|
||||
{
|
||||
if (_count == 0) return;
|
||||
|
||||
int start = _count == _capacity ? _head : 0;
|
||||
|
||||
if (start + _count <= _capacity)
|
||||
{
|
||||
Array.Copy(_buffer, start, destination, destinationIndex, _count);
|
||||
}
|
||||
else
|
||||
{
|
||||
int firstPartLength = _capacity - start;
|
||||
Array.Copy(_buffer, start, destination, destinationIndex, firstPartLength);
|
||||
Array.Copy(_buffer, 0, destination, destinationIndex + firstPartLength, _count - firstPartLength);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a copy of the current state for bar correction support.
|
||||
/// </summary>
|
||||
public RingBuffer Clone()
|
||||
{
|
||||
var clone = new RingBuffer(_capacity);
|
||||
Array.Copy(_buffer, clone._buffer, _capacity);
|
||||
clone._head = _head;
|
||||
clone._count = _count;
|
||||
clone._sum = _sum;
|
||||
return clone;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Copies state from another RingBuffer.
|
||||
/// Both buffers must have the same capacity.
|
||||
/// </summary>
|
||||
public void CopyFrom(RingBuffer source)
|
||||
{
|
||||
if (source._capacity != _capacity)
|
||||
throw new ArgumentException("Source buffer must have same capacity", nameof(source));
|
||||
|
||||
Array.Copy(source._buffer, _buffer, _capacity);
|
||||
_head = source._head;
|
||||
_count = source._count;
|
||||
_sum = source._sum;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns an enumerator that iterates through the buffer in chronological order.
|
||||
/// </summary>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public Enumerator GetEnumerator() => new(this);
|
||||
|
||||
IEnumerator<double> IEnumerable<double>.GetEnumerator() => GetEnumerator();
|
||||
IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
|
||||
|
||||
/// <summary>
|
||||
/// High-performance enumerator for the RingBuffer.
|
||||
/// </summary>
|
||||
public struct Enumerator : IEnumerator<double>
|
||||
{
|
||||
private readonly RingBuffer _buffer;
|
||||
private readonly int _start;
|
||||
private readonly int _count;
|
||||
private int _index;
|
||||
private double _current;
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
internal Enumerator(RingBuffer buffer)
|
||||
{
|
||||
_buffer = buffer;
|
||||
_count = buffer._count;
|
||||
_start = buffer._count == buffer._capacity ? buffer._head : 0;
|
||||
_index = -1;
|
||||
_current = default;
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public bool MoveNext()
|
||||
{
|
||||
if (_index + 1 >= _count)
|
||||
return false;
|
||||
|
||||
_index++;
|
||||
int bufferIdx = (_start + _index) % _buffer._capacity;
|
||||
_current = _buffer._buffer[bufferIdx];
|
||||
return true;
|
||||
}
|
||||
|
||||
public double Current => _current;
|
||||
object IEnumerator.Current => Current;
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public void Reset()
|
||||
{
|
||||
_index = -1;
|
||||
_current = default;
|
||||
}
|
||||
|
||||
public void Dispose() { }
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user