SIMD Refactor: Merge simd-dev into dev (#55)

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>
This commit is contained in:
Miha Kralj
2026-01-18 19:02:03 -08:00
committed by GitHub
co-authored by Claude Opus 4.5 aider Warp
parent 5bcdf8d614
commit 86fe32a682
1750 changed files with 198235 additions and 80539 deletions
+541
View File
@@ -0,0 +1,541 @@
namespace QuanTAlib.Tests;
public class TValueEventArgsTests
{
[Fact]
public void Constructor_WithValueAndIsNew_SetsPropertiesCorrectly()
{
var tValue = new TValue(DateTime.UtcNow.Ticks, 123.45);
const bool isNew = true;
var eventArgs = new TValueEventArgs { Value = tValue, IsNew = isNew };
Assert.Equal(tValue, eventArgs.Value);
Assert.True(eventArgs.IsNew);
}
[Fact]
public void Constructor_Default_SetsDefaultValues()
{
var eventArgs = new TValueEventArgs();
Assert.Equal(default(TValue), eventArgs.Value);
Assert.False(eventArgs.IsNew);
}
[Fact]
public void Constructor_WithNaNValue_PreservesNaN()
{
var tValue = new TValue(DateTime.UtcNow.Ticks, double.NaN);
var eventArgs = new TValueEventArgs { Value = tValue, IsNew = false };
Assert.True(double.IsNaN(eventArgs.Value.Value));
}
[Fact]
public void Constructor_WithInfinityValue_PreservesInfinity()
{
var tValue = new TValue(DateTime.UtcNow.Ticks, double.PositiveInfinity);
var eventArgs = new TValueEventArgs { Value = tValue, IsNew = true };
Assert.True(double.IsPositiveInfinity(eventArgs.Value.Value));
}
[Fact]
public void Equals_SameValues_ReturnsTrue()
{
var tValue = new TValue(12345, 100.0);
var args1 = new TValueEventArgs { Value = tValue, IsNew = true };
var args2 = new TValueEventArgs { Value = tValue, IsNew = true };
Assert.True(args1.Equals(args2));
}
[Fact]
public void Equals_DifferentValue_ReturnsFalse()
{
var tValue1 = new TValue(12345, 100.0);
var tValue2 = new TValue(12345, 101.0);
var args1 = new TValueEventArgs { Value = tValue1, IsNew = true };
var args2 = new TValueEventArgs { Value = tValue2, IsNew = true };
Assert.False(args1.Equals(args2));
}
[Fact]
public void Equals_DifferentTime_ReturnsFalse()
{
var tValue1 = new TValue(12345, 100.0);
var tValue2 = new TValue(12346, 100.0);
var args1 = new TValueEventArgs { Value = tValue1, IsNew = true };
var args2 = new TValueEventArgs { Value = tValue2, IsNew = true };
Assert.False(args1.Equals(args2));
}
[Fact]
public void Equals_DifferentIsNew_ReturnsFalse()
{
var tValue = new TValue(12345, 100.0);
var args1 = new TValueEventArgs { Value = tValue, IsNew = true };
var args2 = new TValueEventArgs { Value = tValue, IsNew = false };
Assert.False(args1.Equals(args2));
}
[Fact]
public void Equals_Object_SameTValueEventArgs_ReturnsTrue()
{
var tValue = new TValue(12345, 100.0);
var args1 = new TValueEventArgs { Value = tValue, IsNew = true };
object args2 = new TValueEventArgs { Value = tValue, IsNew = true };
Assert.True(args1.Equals(args2));
}
[Fact]
public void Equals_Object_DifferentType_ReturnsFalse()
{
var args = new TValueEventArgs { Value = new TValue(12345, 100.0), IsNew = true };
object other = "not a TValueEventArgs";
Assert.False(args.Equals(other));
}
[Fact]
public void Equals_Object_Null_ReturnsFalse()
{
var args = new TValueEventArgs { Value = new TValue(12345, 100.0), IsNew = true };
Assert.False(args.Equals(null));
}
[Fact]
public void GetHashCode_SameValues_ReturnsSameHashCode()
{
var tValue = new TValue(12345, 100.0);
var args1 = new TValueEventArgs { Value = tValue, IsNew = true };
var args2 = new TValueEventArgs { Value = tValue, IsNew = true };
Assert.Equal(args1.GetHashCode(), args2.GetHashCode());
}
[Fact]
public void GetHashCode_DifferentValues_ReturnsDifferentHashCode()
{
var tValue1 = new TValue(12345, 100.0);
var tValue2 = new TValue(12346, 100.0);
var args1 = new TValueEventArgs { Value = tValue1, IsNew = true };
var args2 = new TValueEventArgs { Value = tValue2, IsNew = true };
Assert.NotEqual(args1.GetHashCode(), args2.GetHashCode());
}
[Fact]
public void GetHashCode_DifferentIsNew_ReturnsDifferentHashCode()
{
var tValue = new TValue(12345, 100.0);
var args1 = new TValueEventArgs { Value = tValue, IsNew = true };
var args2 = new TValueEventArgs { Value = tValue, IsNew = false };
Assert.NotEqual(args1.GetHashCode(), args2.GetHashCode());
}
[Fact]
public void EqualityOperator_SameValues_ReturnsTrue()
{
var tValue = new TValue(12345, 100.0);
var args1 = new TValueEventArgs { Value = tValue, IsNew = true };
var args2 = new TValueEventArgs { Value = tValue, IsNew = true };
Assert.True(args1 == args2);
}
[Fact]
public void EqualityOperator_DifferentValues_ReturnsFalse()
{
var tValue1 = new TValue(12345, 100.0);
var tValue2 = new TValue(12346, 100.0);
var args1 = new TValueEventArgs { Value = tValue1, IsNew = true };
var args2 = new TValueEventArgs { Value = tValue2, IsNew = true };
Assert.False(args1 == args2);
}
[Fact]
public void InequalityOperator_SameValues_ReturnsFalse()
{
var tValue = new TValue(12345, 100.0);
var args1 = new TValueEventArgs { Value = tValue, IsNew = true };
var args2 = new TValueEventArgs { Value = tValue, IsNew = true };
Assert.False(args1 != args2);
}
[Fact]
public void InequalityOperator_DifferentValues_ReturnsTrue()
{
var tValue1 = new TValue(12345, 100.0);
var tValue2 = new TValue(12346, 100.0);
var args1 = new TValueEventArgs { Value = tValue1, IsNew = true };
var args2 = new TValueEventArgs { Value = tValue2, IsNew = true };
Assert.True(args1 != args2);
}
[Fact]
public void Equals_WithNaN_BothNaN_ReturnsTrue()
{
var tValue1 = new TValue(12345, double.NaN);
var tValue2 = new TValue(12345, double.NaN);
var args1 = new TValueEventArgs { Value = tValue1, IsNew = true };
var args2 = new TValueEventArgs { Value = tValue2, IsNew = true };
Assert.True(args1.Equals(args2));
}
[Fact]
public void GetHashCode_WithNaN_DoesNotThrow()
{
var tValue = new TValue(12345, double.NaN);
var args = new TValueEventArgs { Value = tValue, IsNew = true };
var hash = args.GetHashCode();
Assert.True(hash != 0 || hash == 0); // Just verify it doesn't throw
}
[Fact]
public void Constructor_WithZeroTime_Allowed()
{
var tValue = new TValue(0, 100.0);
var args = new TValueEventArgs { Value = tValue, IsNew = false };
Assert.Equal(0, args.Value.Time);
Assert.Equal(100.0, args.Value.Value);
Assert.False(args.IsNew);
}
[Fact]
public void Constructor_WithNegativeTime_Allowed()
{
var tValue = new TValue(-12345, 100.0);
var args = new TValueEventArgs { Value = tValue, IsNew = true };
Assert.Equal(-12345, args.Value.Time);
Assert.Equal(100.0, args.Value.Value);
Assert.True(args.IsNew);
}
[Fact]
public void Constructor_WithMaxLongTime_Allowed()
{
var tValue = new TValue(long.MaxValue, 100.0);
var args = new TValueEventArgs { Value = tValue, IsNew = false };
Assert.Equal(long.MaxValue, args.Value.Time);
Assert.Equal(100.0, args.Value.Value);
Assert.False(args.IsNew);
}
[Fact]
public void Constructor_WithMaxDoubleValue_Allowed()
{
var tValue = new TValue(DateTime.UtcNow.Ticks, double.MaxValue);
var args = new TValueEventArgs { Value = tValue, IsNew = true };
Assert.Equal(double.MaxValue, args.Value.Value);
Assert.True(args.IsNew);
}
[Fact]
public void Constructor_WithMinDoubleValue_Allowed()
{
var tValue = new TValue(DateTime.UtcNow.Ticks, double.MinValue);
var args = new TValueEventArgs { Value = tValue, IsNew = false };
Assert.Equal(double.MinValue, args.Value.Value);
Assert.False(args.IsNew);
}
[Fact]
public void Constructor_WithEpsilonValue_Allowed()
{
var tValue = new TValue(DateTime.UtcNow.Ticks, double.Epsilon);
var args = new TValueEventArgs { Value = tValue, IsNew = true };
Assert.Equal(double.Epsilon, args.Value.Value);
Assert.True(args.IsNew);
}
}
public class TValuePublishedHandlerTests
{
private class MockPublisher : ITValuePublisher
{
#pragma warning disable CS0067 // Event is never used - intentional for delegate testing
public event TValuePublishedHandler? Pub;
#pragma warning restore CS0067
}
[Fact]
public void Delegate_CanBeAssigned()
{
TValuePublishedHandler handler = (_, in _) => { };
Assert.NotNull(handler);
}
[Fact]
public void Delegate_CanBeInvoked()
{
bool wasCalled = false;
TValuePublishedHandler handler = (object? sender, in TValueEventArgs args) => wasCalled = true;
var args = new TValueEventArgs { Value = new TValue(12345, 100.0), IsNew = true };
handler(null, args);
Assert.True(wasCalled);
}
[Fact]
public void Delegate_ReceivesCorrectArguments()
{
object? receivedSender = null;
TValueEventArgs receivedArgs = default;
TValuePublishedHandler handler = (object? sender, in TValueEventArgs args) =>
{
receivedSender = sender;
receivedArgs = args;
};
var publisher = new MockPublisher();
var expectedArgs = new TValueEventArgs { Value = new TValue(12345, 100.0), IsNew = true };
handler(publisher, expectedArgs);
Assert.Equal(publisher, receivedSender);
Assert.Equal(expectedArgs, receivedArgs);
}
[Fact]
public void Delegate_CanBeNull()
{
TValuePublishedHandler? handler = null;
var exception = Record.Exception(() => handler?.Invoke(null, new TValueEventArgs()));
Assert.Null(exception);
}
}
public class ITValuePublisherTests
{
private class MockPublisher : ITValuePublisher
{
public event TValuePublishedHandler? Pub;
public void RaiseEvent(TValue value, bool isNew = true)
{
Pub?.Invoke(this, new TValueEventArgs { Value = value, IsNew = isNew });
}
}
[Fact]
public void Interface_CanBeImplemented()
{
ITValuePublisher publisher = new MockPublisher();
Assert.NotNull(publisher);
}
[Fact]
public void Pub_Event_CanBeSubscribed()
{
var publisher = new MockPublisher();
bool eventRaised = false;
publisher.Pub += (object? sender, in TValueEventArgs args) => eventRaised = true;
publisher.RaiseEvent(new TValue(12345, 100.0));
Assert.True(eventRaised);
}
[Fact]
public void Pub_Event_CanBeUnsubscribed()
{
var publisher = new MockPublisher();
bool eventRaised = false;
TValuePublishedHandler handler = (object? sender, in TValueEventArgs args) => eventRaised = true;
publisher.Pub += handler;
publisher.RaiseEvent(new TValue(12345, 100.0));
Assert.True(eventRaised);
eventRaised = false;
publisher.Pub -= handler;
publisher.RaiseEvent(new TValue(12346, 101.0));
Assert.False(eventRaised);
}
[Fact]
public void Pub_Event_MultipleSubscribers_AllReceiveEvent()
{
var publisher = new MockPublisher();
bool event1Raised = false;
bool event2Raised = false;
publisher.Pub += (object? sender, in TValueEventArgs args) => event1Raised = true;
publisher.Pub += (object? sender, in TValueEventArgs args) => event2Raised = true;
publisher.RaiseEvent(new TValue(12345, 100.0));
Assert.True(event1Raised);
Assert.True(event2Raised);
}
[Fact]
public void Pub_Event_NoSubscribers_DoesNotThrow()
{
var publisher = new MockPublisher();
var exception = Record.Exception(() => publisher.RaiseEvent(new TValue(12345, 100.0)));
Assert.Null(exception);
}
[Fact]
public void Pub_Event_ReceivesCorrectSender()
{
var publisher = new MockPublisher();
object? receivedSender = null;
publisher.Pub += (object? sender, in TValueEventArgs args) => receivedSender = sender;
publisher.RaiseEvent(new TValue(12345, 100.0));
Assert.Equal(publisher, receivedSender);
}
[Fact]
public void Pub_Event_ReceivesCorrectEventArgs()
{
var publisher = new MockPublisher();
TValueEventArgs receivedArgs = default;
publisher.Pub += (object? sender, in TValueEventArgs args) => receivedArgs = args;
var expectedValue = new TValue(12345, 100.0);
publisher.RaiseEvent(expectedValue, isNew: true);
Assert.Equal(expectedValue, receivedArgs.Value);
Assert.True(receivedArgs.IsNew);
}
[Fact]
public void Pub_Event_IsNew_False_ReceivedCorrectly()
{
var publisher = new MockPublisher();
TValueEventArgs receivedArgs = default;
publisher.Pub += (object? sender, in TValueEventArgs args) => receivedArgs = args;
var expectedValue = new TValue(12345, 100.0);
publisher.RaiseEvent(expectedValue, isNew: false);
Assert.Equal(expectedValue, receivedArgs.Value);
Assert.False(receivedArgs.IsNew);
}
[Fact]
public void Pub_Event_HandlerThrows_ExceptionPropagates()
{
var publisher = new MockPublisher();
publisher.Pub += (object? sender, in TValueEventArgs args) =>
{
throw new InvalidOperationException("Test exception");
};
Assert.Throws<InvalidOperationException>(() =>
publisher.RaiseEvent(new TValue(12345, 100.0)));
}
[Fact]
public void Pub_Event_HandlerWithNaNValue_ReceivesNaN()
{
var publisher = new MockPublisher();
double receivedValue = 0;
publisher.Pub += (object? sender, in TValueEventArgs args) => receivedValue = args.Value.Value;
publisher.RaiseEvent(new TValue(12345, double.NaN));
Assert.True(double.IsNaN(receivedValue));
}
[Fact]
public void Pub_Event_HandlerWithInfinityValue_ReceivesInfinity()
{
var publisher = new MockPublisher();
double receivedValue = 0;
publisher.Pub += (object? sender, in TValueEventArgs args) => receivedValue = args.Value.Value;
publisher.RaiseEvent(new TValue(12345, double.PositiveInfinity));
Assert.True(double.IsPositiveInfinity(receivedValue));
}
[Fact]
public void Pub_Event_MultipleEvents_AllReceived()
{
var publisher = new MockPublisher();
var receivedValues = new List<double>();
publisher.Pub += (object? sender, in TValueEventArgs args) => receivedValues.Add(args.Value.Value);
publisher.RaiseEvent(new TValue(12345, 100.0));
publisher.RaiseEvent(new TValue(12346, 200.0));
publisher.RaiseEvent(new TValue(12347, 300.0));
Assert.Equal(3, receivedValues.Count);
Assert.Equal(100.0, receivedValues[0]);
Assert.Equal(200.0, receivedValues[1]);
Assert.Equal(300.0, receivedValues[2]);
}
[Fact]
public void Pub_Event_SubscribeUnsubscribeMultipleTimes_Works()
{
var publisher = new MockPublisher();
int callCount = 0;
TValuePublishedHandler handler = (object? sender, in TValueEventArgs args) => callCount++;
// Subscribe multiple times
publisher.Pub += handler;
publisher.Pub += handler;
publisher.RaiseEvent(new TValue(12345, 100.0));
Assert.Equal(2, callCount); // Called twice
callCount = 0;
// Unsubscribe once
publisher.Pub -= handler;
publisher.RaiseEvent(new TValue(12346, 200.0));
Assert.Equal(1, callCount); // Called once
callCount = 0;
// Unsubscribe remaining
publisher.Pub -= handler;
publisher.RaiseEvent(new TValue(12347, 300.0));
Assert.Equal(0, callCount); // Not called
}
}
+52
View File
@@ -0,0 +1,52 @@
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
namespace QuanTAlib;
/// <summary>
/// Performance-focused event args for TValue updates.
/// Implemented as struct to avoid heap allocations in high-frequency event dispatch.
/// </summary>
[StructLayout(LayoutKind.Auto)]
public readonly struct TValueEventArgs : IEquatable<TValueEventArgs>
{
public TValue Value { get; init; }
public bool IsNew { get; init; }
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public bool Equals(TValueEventArgs other) =>
Value.Equals(other.Value) && IsNew == other.IsNew;
public override bool Equals(object? obj) =>
obj is TValueEventArgs other && Equals(other);
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public override int GetHashCode() =>
HashCode.Combine(Value, IsNew);
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static bool operator ==(TValueEventArgs left, TValueEventArgs right) =>
left.Equals(right);
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static bool operator !=(TValueEventArgs left, TValueEventArgs right) =>
!left.Equals(right);
}
// Performance-focused event args struct; not derived from EventArgs by design.
// We intentionally deviate from the standard EventArgs pattern here for perf.
// MA0046 suppressed: struct-based args avoid heap allocations in high-frequency events.
#pragma warning disable MA0046 // The second parameter must be of type 'System.EventArgs' or a derived type
public delegate void TValuePublishedHandler(object? sender, in TValueEventArgs args);
/// <summary>
/// Interface for objects that publish TValue updates.
/// </summary>
public interface ITValuePublisher
{
/// <summary>
/// Event triggered when a new TValue is available.
/// </summary>
event TValuePublishedHandler? Pub;
}
#pragma warning restore MA0046
+586
View File
@@ -0,0 +1,586 @@
namespace QuanTAlib.Tests;
public class TSeriesTests
{
[Fact]
public void Constructor_Default_CreatesEmptySeries()
{
var series = new TSeries();
Assert.Empty(series);
}
[Fact]
public void Constructor_WithCapacity_CreatesEmptySeries()
{
var series = new TSeries(100);
Assert.Empty(series);
}
[Fact]
public void Constructor_WithLists_WrapsExistingData()
{
var times = new List<long> { 100, 200, 300 };
var values = new List<double> { 1.0, 2.0, 3.0 };
var series = new TSeries(times, values);
Assert.Equal(3, series.Count);
Assert.Equal(1.0, series[0].Value);
Assert.Equal(2.0, series[1].Value);
Assert.Equal(3.0, series[2].Value);
}
[Fact]
public void Name_DefaultValue_IsData()
{
var series = new TSeries();
Assert.Equal("Data", series.Name);
}
[Fact]
public void Name_CanBeSet()
{
var series = new TSeries { Name = "TestSeries" };
Assert.Equal("TestSeries", series.Name);
}
[Fact]
public void Add_NewValue_IncreasesCount()
{
var series = new TSeries();
long time = DateTime.UtcNow.Ticks;
series.Add(time, 10.0, isNew: true);
Assert.Single(series);
Assert.Equal(10.0, series.Last.Value);
}
[Fact]
public void Add_UpdateValue_DoesNotIncreaseCount()
{
var series = new TSeries();
long time = DateTime.UtcNow.Ticks;
series.Add(time, 10.0, isNew: true);
series.Add(time, 11.0, isNew: false);
Assert.Single(series);
Assert.Equal(11.0, series.Last.Value);
}
[Fact]
public void Add_MultipleValues_MaintainsOrder()
{
var series = new TSeries();
long t0 = DateTime.UtcNow.Ticks;
long t1 = t0 + TimeSpan.TicksPerMinute;
series.Add(t0, 10.0, isNew: true);
series.Add(t1, 20.0, isNew: true);
Assert.Equal(2, series.Count);
Assert.Equal(10.0, series[0].Value);
Assert.Equal(20.0, series[1].Value);
}
[Fact]
public void Add_TValue_AddsNewItem()
{
var series = new TSeries();
var tv = new TValue(DateTime.UtcNow.Ticks, 42.0);
series.Add(tv);
Assert.Single(series);
Assert.Equal(42.0, series.Last.Value);
}
[Fact]
public void Add_TValueWithIsNew_AddsOrUpdates()
{
var series = new TSeries();
var tv1 = new TValue(DateTime.UtcNow.Ticks, 42.0);
var tv2 = new TValue(DateTime.UtcNow.Ticks, 43.0);
series.Add(tv1, isNew: true);
series.Add(tv2, isNew: false);
Assert.Single(series);
Assert.Equal(43.0, series.Last.Value);
}
[Fact]
public void Add_WithDateTime_AddsValue()
{
var series = new TSeries();
var dt = new DateTime(2024, 6, 15, 10, 30, 0, DateTimeKind.Utc);
series.Add(dt, 100.0, isNew: true);
Assert.Single(series);
Assert.Equal(dt.Ticks, series.Last.Time);
Assert.Equal(100.0, series.Last.Value);
}
[Fact]
public void Add_EnumerableDoubles_AddsAllValues()
{
var series = new TSeries();
var values = new[] { 1.0, 2.0, 3.0, 4.0, 5.0 };
series.Add(values);
Assert.Equal(5, series.Count);
Assert.Equal(1.0, series[0].Value);
Assert.Equal(5.0, series[4].Value);
}
[Fact]
public void Add_UpdateOnEmptySeries_AddsNewItem()
{
var series = new TSeries();
var tv = new TValue(DateTime.UtcNow.Ticks, 42.0);
series.Add(tv, isNew: false);
Assert.Single(series);
Assert.Equal(42.0, series.Last.Value);
}
[Fact]
public void Last_EmptySeries_ReturnsDefault()
{
var series = new TSeries();
var last = series.Last;
Assert.Equal(0, last.Time);
Assert.Equal(0.0, last.Value);
}
[Fact]
public void Last_NonEmptySeries_ReturnsLastValue()
{
var series = new TSeries();
series.Add(100, 10.0);
series.Add(200, 20.0);
var last = series.Last;
Assert.Equal(200, last.Time);
Assert.Equal(20.0, last.Value);
}
[Fact]
public void LastValue_EmptySeries_ReturnsNaN()
{
var series = new TSeries();
Assert.True(double.IsNaN(series.LastValue));
}
[Fact]
public void LastValue_NonEmptySeries_ReturnsLastValue()
{
var series = new TSeries();
series.Add(100, 10.0);
series.Add(200, 20.0);
Assert.Equal(20.0, series.LastValue);
}
[Fact]
public void LastTime_EmptySeries_ReturnsZero()
{
var series = new TSeries();
Assert.Equal(0, series.LastTime);
}
[Fact]
public void LastTime_NonEmptySeries_ReturnsLastTime()
{
var series = new TSeries();
series.Add(100, 10.0);
series.Add(200, 20.0);
Assert.Equal(200, series.LastTime);
}
[Fact]
public void Values_ReturnsReadOnlySpanOfValues()
{
var series = new TSeries();
series.Add(100, 1.0);
series.Add(200, 2.0);
series.Add(300, 3.0);
ReadOnlySpan<double> values = series.Values;
Assert.Equal(3, values.Length);
Assert.Equal(1.0, values[0]);
Assert.Equal(2.0, values[1]);
Assert.Equal(3.0, values[2]);
}
[Fact]
public void Times_ReturnsReadOnlySpanOfTimes()
{
var series = new TSeries();
series.Add(100, 1.0);
series.Add(200, 2.0);
series.Add(300, 3.0);
ReadOnlySpan<long> times = series.Times;
Assert.Equal(3, times.Length);
Assert.Equal(100, times[0]);
Assert.Equal(200, times[1]);
Assert.Equal(300, times[2]);
}
[Fact]
public void Indexer_ReturnsCorrectTValue()
{
var series = new TSeries();
series.Add(100, 10.0);
series.Add(200, 20.0);
series.Add(300, 30.0);
Assert.Equal(100, series[0].Time);
Assert.Equal(10.0, series[0].Value);
Assert.Equal(200, series[1].Time);
Assert.Equal(20.0, series[1].Value);
Assert.Equal(300, series[2].Time);
Assert.Equal(30.0, series[2].Value);
}
[Fact]
public void GetEnumerator_IteratesAllValues()
{
var series = new TSeries();
series.Add(100, 1.0);
series.Add(200, 2.0);
series.Add(300, 3.0);
var list = series.ToList();
Assert.Equal(3, list.Count);
Assert.Equal(1.0, list[0].Value);
Assert.Equal(2.0, list[1].Value);
Assert.Equal(3.0, list[2].Value);
}
[Fact]
public void GetEnumerator_NonGeneric_Works()
{
var series = new TSeries();
series.Add(100, 1.0);
series.Add(200, 2.0);
var list = new List<object>();
IEnumerable enumerable = series;
#pragma warning disable S4158
foreach (var item in enumerable)
{
list.Add(item);
}
Assert.Equal(2, series.Count);
Assert.Equal(2, list.Count);
}
[Fact]
public void Pub_Event_IsRaisedOnAdd()
{
var series = new TSeries();
TValue? received = null;
series.Pub += (object? sender, in TValueEventArgs args) => received = args.Value;
series.Add(100, 42.0);
Assert.NotNull(received);
Assert.Equal(100, received.Value.Time);
Assert.Equal(42.0, received.Value.Value);
}
[Fact]
public void Pub_Event_IsRaisedOnUpdate()
{
var series = new TSeries();
TValue? received = null;
series.Add(100, 42.0);
series.Pub += (object? sender, in TValueEventArgs args) => received = args.Value;
series.Add(100, 43.0, isNew: false);
Assert.NotNull(received);
Assert.Equal(43.0, received.Value.Value);
}
[Fact]
public void Count_ReturnsCorrectValue()
{
var series = new TSeries();
series.Add(100, 1.0);
Assert.Single(series);
series.Add(200, 2.0);
Assert.Equal(2, series.Count);
}
[Fact]
public void Constructor_WithMismatchedLists_WrapsData()
{
// TSeries wraps the lists directly if they're List<T>, no length validation
var times = new List<long> { 100, 200, 300 };
var values = new List<double> { 1.0, 2.0 }; // Different length
var series = new TSeries(times, values);
// Count is based on values list
Assert.Equal(2, series.Count);
}
[Fact]
public void Indexer_OutOfBounds_ThrowsException()
{
var series = new TSeries();
Assert.Throws<ArgumentOutOfRangeException>(() => _ = series[0]);
}
[Fact]
public void Indexer_NegativeIndex_ThrowsException()
{
var series = new TSeries();
series.Add(100, 1.0);
#pragma warning disable DS003 // Invalid index - intentional for testing exception
Assert.Throws<ArgumentOutOfRangeException>(() => _ = series[-1]);
#pragma warning restore DS003
}
[Fact]
public void Indexer_BeyondCount_ThrowsException()
{
var series = new TSeries();
series.Add(100, 1.0);
Assert.Throws<ArgumentOutOfRangeException>(() => _ = series[1]);
}
[Fact]
public void Values_EmptySeries_ReturnsEmptySpan()
{
var series = new TSeries();
ReadOnlySpan<double> values = series.Values;
Assert.Equal(0, values.Length);
}
[Fact]
public void Times_EmptySeries_ReturnsEmptySpan()
{
var series = new TSeries();
ReadOnlySpan<long> times = series.Times;
Assert.Equal(0, times.Length);
}
[Fact]
public void Add_WithNaN_PreservesNaN()
{
var series = new TSeries();
series.Add(100, double.NaN);
Assert.True(double.IsNaN(series.Last.Value));
Assert.True(double.IsNaN(series.LastValue));
}
[Fact]
public void Add_WithInfinity_PreservesInfinity()
{
var series = new TSeries();
series.Add(100, double.PositiveInfinity);
Assert.True(double.IsPositiveInfinity(series.Last.Value));
Assert.True(double.IsPositiveInfinity(series.LastValue));
}
[Fact]
public void Add_WithNegativeInfinity_PreservesNegativeInfinity()
{
var series = new TSeries();
series.Add(100, double.NegativeInfinity);
Assert.True(double.IsNegativeInfinity(series.Last.Value));
}
[Fact]
public void Add_EnumerableDoubles_GeneratesIncreasingTimes()
{
var series = new TSeries();
var values = new[] { 1.0, 2.0, 3.0 };
series.Add(values);
Assert.Equal(3, series.Count);
// Times should be increasing by TicksPerMinute
Assert.True(series[1].Time > series[0].Time);
Assert.True(series[2].Time > series[1].Time);
Assert.Equal(TimeSpan.TicksPerMinute, series[1].Time - series[0].Time);
}
[Fact]
public void Add_EnumerableDoubles_EmptyArray_AddsNothing()
{
var series = new TSeries();
series.Add(Array.Empty<double>());
Assert.Empty(series);
}
[Fact]
public void Pub_EventArgs_ContainsIsNewFlag()
{
var series = new TSeries();
bool? receivedIsNew = null;
series.Pub += (object? sender, in TValueEventArgs args) => receivedIsNew = args.IsNew;
series.Add(new TValue(100, 42.0), isNew: true);
Assert.True(receivedIsNew);
series.Add(new TValue(100, 43.0), isNew: false);
Assert.False(receivedIsNew);
}
[Fact]
public void Constructor_WithCapacity_DoesNotAffectCount()
{
var series = new TSeries(1000);
Assert.Empty(series);
}
[Fact]
public void Add_WithDateTimeLocal_ConvertsToUtc()
{
var series = new TSeries();
var localTime = new DateTime(2024, 6, 15, 10, 30, 0, DateTimeKind.Local);
series.Add(localTime, 100.0);
// The stored time should be UTC
var storedTime = new DateTime(series.Last.Time, DateTimeKind.Utc);
Assert.Equal(DateTimeKind.Utc, storedTime.Kind);
}
[Fact]
public void Add_WithDateTimeUnspecified_TreatsAsLocal()
{
var series = new TSeries();
var unspecifiedTime = new DateTime(2024, 6, 15, 10, 30, 0, DateTimeKind.Unspecified);
series.Add(unspecifiedTime, 100.0);
Assert.Single(series);
}
[Fact]
public void Values_ModifyingUnderlyingList_ReflectsInSpan()
{
var series = new TSeries();
series.Add(100, 1.0);
series.Add(200, 2.0);
// Get the span
ReadOnlySpan<double> values1 = series.Values;
Assert.Equal(2, values1.Length);
// Add more data
series.Add(300, 3.0);
// Get new span - should reflect the change
ReadOnlySpan<double> values2 = series.Values;
Assert.Equal(3, values2.Length);
Assert.Equal(3.0, values2[2]);
}
[Fact]
public void Constructor_WithReadOnlyLists_CopiesData()
{
// Using arrays which implement IReadOnlyList but aren't List<T>
long[] times = [100, 200, 300];
IReadOnlyList<double> values = [1.0, 2.0, 3.0];
var series = new TSeries(times, values);
Assert.Equal(3, series.Count);
Assert.Equal(1.0, series[0].Value);
Assert.Equal(3.0, series[2].Value);
}
[Fact]
public void GetEnumerator_ExplicitGenericInterface_Works()
{
var series = new TSeries();
series.Add(100, 1.0);
series.Add(200, 2.0);
series.Add(300, 3.0);
// Explicitly call IEnumerable<TValue>.GetEnumerator() through interface cast
IEnumerable<TValue> genericEnumerable = series;
using var enumerator = genericEnumerable.GetEnumerator();
var values = new List<double>();
while (enumerator.MoveNext())
{
values.Add(enumerator.Current.Value);
}
Assert.Equal(3, values.Count);
Assert.Equal(1.0, values[0]);
Assert.Equal(2.0, values[1]);
Assert.Equal(3.0, values[2]);
}
[Fact]
public void GetEnumerator_ExplicitNonGenericInterface_Works()
{
var series = new TSeries();
series.Add(100, 1.0);
series.Add(200, 2.0);
// Explicitly call IEnumerable.GetEnumerator() through interface cast
IEnumerable nonGenericEnumerable = series;
var enumerator = nonGenericEnumerable.GetEnumerator();
var values = new List<double>();
while (enumerator.MoveNext())
{
var tv = (TValue)enumerator.Current;
values.Add(tv.Value);
}
Assert.Equal(2, values.Count);
Assert.Equal(1.0, values[0]);
Assert.Equal(2.0, values[1]);
}
}
+114
View File
@@ -0,0 +1,114 @@
# TSeries: Time Series Data Container
## What It Does
`TSeries` is a high-performance, memory-efficient container for time-series data. Unlike standard collections (like `List<TValue>`), it uses a **Structure of Arrays (SoA)** layout internally. This means it stores timestamps and values in separate contiguous arrays, optimizing memory access patterns for numerical processing and SIMD vectorization.
## Design Philosophy
Standard object-oriented collections (Array of Structures - AoS) are cache-inefficient for numerical algorithms. When calculating a moving average, the CPU only needs the values, but an AoS layout forces it to load interleaved timestamps into the cache, wasting bandwidth.
`TSeries` solves this by decoupling time and value storage:
* **Cache Locality**: Iterating over values loads only values.
* **SIMD Readiness**: The internal value array can be exposed directly as a `Span<double>` for AVX/SSE processing.
* **Zero-Copy Views**: Data is accessed without defensive copying, ensuring maximum throughput.
## How It Works
`TSeries` maintains two parallel internal lists:
1. `List<long> _t`: Stores timestamps.
2. `List<double> _v`: Stores values.
It implements `IReadOnlyList<TValue>`, allowing it to be treated as a standard collection of `TValue` structs when needed, but its true power lies in its column-oriented properties (`Values`, `Times`).
## Structure
### Definition
```csharp
public class TSeries : IReadOnlyList<TValue>, ITValuePublisher
```
### Core Properties
| Property | Type | Description |
| ------ | ------ | ------ |
| `Values` | `ReadOnlySpan<double>` | Direct access to the value array (SIMD-ready). |
| `Times` | `ReadOnlySpan<long>` | Direct access to the timestamp array. |
| `Last` | `TValue` | The most recent time-value pair. |
| `Count` | `int` | Number of elements in the series. |
| `Name` | `string` | Optional identifier for the series. |
### Events
| Event | Type | Description |
| ------ | ------ | ------ |
| `Pub` | `Action<TValue>` | Fired whenever a new value is added or updated. |
## Usage
### Creating and Populating
```csharp
var series = new TSeries();
// Add a new bar (isNew = true by default)
series.Add(DateTime.UtcNow, 100.0);
// Add multiple values
series.Add(new List<double> { 1.0, 2.0, 3.0 });
```
### Streaming Updates (Real-time)
`TSeries` supports "bar updates" where the last value changes until the bar closes.
```csharp
// New minute starts
series.Add(time, 100.0, isNew: true);
// Price updates within the same minute
series.Add(time, 101.0, isNew: false); // Overwrites last value
series.Add(time, 102.0, isNew: false); // Overwrites last value
```
### SIMD Processing
```csharp
// Calculate average using SIMD (via Span)
double sum = 0;
foreach (var v in series.Values) { sum += v; } // Compiler vectorizes this
```
### Reactive Subscription
```csharp
series.Pub += (item) => Console.WriteLine($"New value: {item}");
```
## Performance Profile
* **Memory Layout**: SoA (Structure of Arrays).
* **Access Speed**: O(1) for random access.
* **Iteration**: Cache-friendly linear scan.
* **SIMD**: Fully supported via `Values` span.
## Integration
`TSeries` is the standard output format for all indicators in QuanTAlib.
* **Input**: Can be fed into indicators via `Update(TSeries)`.
* **Output**: Indicators return `TSeries` from their `Calculate` methods.
* **Visualization**: Easily mappable to charting libraries due to separate Time/Value arrays.
## Architecture Notes
* **CollectionsMarshal**: Uses `CollectionsMarshal.AsSpan` to expose internal list storage as spans without copying. This is unsafe if the list is modified during span access, but provides maximum performance for single-threaded algorithms.
* **Virtual Methods**: `Add` is virtual to allow derived classes (like `TBarSeries` components) to intercept updates if necessary.
## References
* [Data-Oriented Design](https://en.wikipedia.org/wiki/Data-oriented_design)
* [SIMD in .NET](https://learn.microsoft.com/en-us/dotnet/standard/simd)
+230
View File
@@ -0,0 +1,230 @@
using System.Collections;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
namespace QuanTAlib;
/// <summary>
/// Tag type indicating that a TSeries constructor should share storage with the provided lists
/// rather than making defensive copies. Used internally for synchronized sub-series.
/// </summary>
public readonly struct ShareStorageTag
{
/// <summary>
/// Singleton instance for the share storage tag.
/// </summary>
public static readonly ShareStorageTag Instance = default;
}
/// <summary>
/// High-performance enumerator for TSeries.
/// </summary>
public struct TSeriesEnumerator : IEnumerator<TValue>, IEquatable<TSeriesEnumerator>
{
private readonly List<long> _t;
private readonly List<double> _v;
private readonly int _count;
private int _index;
private TValue _current;
[MethodImpl(MethodImplOptions.AggressiveInlining)]
internal TSeriesEnumerator(List<long> t, List<double> v)
{
_t = t;
_v = v;
_count = v.Count;
_index = -1;
_current = default;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public bool MoveNext()
{
if (_index + 1 >= _count)
return false;
_index++;
_current = new TValue(_t[_index], _v[_index]);
return true;
}
public readonly TValue Current => _current;
readonly object IEnumerator.Current => Current;
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void Reset()
{
_index = -1;
_current = default;
}
public readonly void Dispose() { }
public readonly bool Equals(TSeriesEnumerator other) =>
ReferenceEquals(_t, other._t) &&
ReferenceEquals(_v, other._v) &&
_count == other._count &&
_index == other._index;
public override readonly bool Equals(object? obj) =>
obj is TSeriesEnumerator other && Equals(other);
public override readonly int GetHashCode() =>
HashCode.Combine(RuntimeHelpers.GetHashCode(_t), RuntimeHelpers.GetHashCode(_v), _count, _index);
public static bool operator ==(TSeriesEnumerator left, TSeriesEnumerator right) => left.Equals(right);
public static bool operator !=(TSeriesEnumerator left, TSeriesEnumerator right) => !left.Equals(right);
}
/// <summary>
/// A high-performance time series implementation using Structure of Arrays (SoA) layout.
/// Stores Time (long) and Value (double) in separate contiguous arrays for SIMD efficiency.
/// Supports "New Bar" vs "Update Last" streaming semantics.
/// </summary>
public class TSeries : IReadOnlyList<TValue>, ITValuePublisher
{
#pragma warning disable MA0016 // Prefer using collection abstraction instead of implementation
protected readonly List<long> _t;
protected readonly List<double> _v;
#pragma warning restore MA0016
public string Name { get; set; } = "Data";
public event TValuePublishedHandler? Pub;
public TSeries() : this(0)
{
}
public TSeries(int capacity)
{
_t = new List<long>(capacity);
_v = new List<double>(capacity);
}
public TSeries(IReadOnlyList<long> time, IReadOnlyList<double> values)
{
// Always make defensive copies to prevent external mutation of internal state
_t = [.. time];
_v = [.. values];
}
/// <summary>
/// Internal constructor for creating views that share underlying storage.
/// Used by TBarSeries to create synchronized OHLCV sub-series.
/// </summary>
/// <param name="time">The time list to share (not copied).</param>
/// <param name="values">The values list to share (not copied).</param>
/// <param name="_">Tag type indicating intentional storage sharing.</param>
internal TSeries(List<long> time, List<double> values, ShareStorageTag _)
{
// Direct assignment for intentional storage sharing (internal use only)
_t = time;
_v = values;
}
public int Count
{
[MethodImpl(MethodImplOptions.AggressiveInlining)]
get => _v.Count;
}
public TValue this[int index]
{
[MethodImpl(MethodImplOptions.AggressiveInlining)]
get => new TValue(_t[index], _v[index]);
}
public TValue Last
{
[MethodImpl(MethodImplOptions.AggressiveInlining)]
get => _v.Count > 0 ? new(_t[^1], _v[^1]) : default;
}
public double LastValue
{
[MethodImpl(MethodImplOptions.AggressiveInlining)]
get => _v.Count > 0 ? _v[^1] : double.NaN;
}
public long LastTime
{
[MethodImpl(MethodImplOptions.AggressiveInlining)]
get => _t.Count > 0 ? _t[^1] : 0;
}
/// <summary>
/// Direct access to the underlying Value array as a Span for SIMD operations.
/// </summary>
/// <remarks>
/// <para><b>Lifetime:</b> The returned span is only valid while no structural mutations
/// (Add, Clear, etc.) occur on this TSeries. Structural changes invalidate the span.</para>
/// <para><b>Mutation:</b> The span reflects the internal List storage; modifications
/// to the series after obtaining the span may cause undefined behavior if the span is still in use.</para>
/// </remarks>
public ReadOnlySpan<double> Values
{
[MethodImpl(MethodImplOptions.AggressiveInlining)]
get => CollectionsMarshal.AsSpan(_v);
}
/// <summary>
/// Direct access to the underlying Time array as a Span.
/// </summary>
/// <remarks>
/// <para><b>Lifetime:</b> The returned span is only valid while no structural mutations
/// (Add, Clear, etc.) occur on this TSeries. Structural changes invalidate the span.</para>
/// <para><b>Mutation:</b> The span reflects the internal List storage; modifications
/// to the series after obtaining the span may cause undefined behavior if the span is still in use.</para>
/// </remarks>
public ReadOnlySpan<long> Times
{
[MethodImpl(MethodImplOptions.AggressiveInlining)]
get => CollectionsMarshal.AsSpan(_t);
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public virtual void Add(TValue value, bool isNew)
{
if (isNew || _v.Count == 0)
{
_t.Add(value.Time);
_v.Add(value.Value);
}
else
{
int lastIdx = _v.Count - 1;
_t[lastIdx] = value.Time;
_v[lastIdx] = value.Value;
}
Pub?.Invoke(this, new TValueEventArgs { Value = value, IsNew = isNew });
}
// Overload for backward compatibility (assumes isNew=true)
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public virtual void Add(TValue value) => Add(value, isNew: true);
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void Add(long time, double value, bool isNew = true) => Add(new TValue(time, value), isNew);
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void Add(DateTime time, double value, bool isNew = true) => Add(new TValue(time, value), isNew);
public void Add(IEnumerable<double> values)
{
long t = DateTime.UtcNow.Ticks;
foreach (var v in values)
{
Add(new TValue(t, v), isNew: true);
t += TimeSpan.TicksPerMinute;
}
}
// IEnumerable implementation with struct enumerator for zero-allocation iteration
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public TSeriesEnumerator GetEnumerator() => new(_t, _v);
IEnumerator<TValue> IEnumerable<TValue>.GetEnumerator() => GetEnumerator();
IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
}