Refactor tests and implementations for various indicators

- Updated RsiIndicatorTests to ensure proper initialization and state checks.
- Added new tests for Rsx, Vel, and Adosc indicators to validate behavior under iterative corrections and edge cases (NaN, Infinity).
- Enhanced Bessel indicator tests and implementation with consistent formatting.
- Improved Ema and Pwma implementations by ensuring proper handling of values.
- Introduced mock classes for charting to facilitate testing without dependencies.
- Ensured all indicators produce consistent results across different modes of operation.
- Cleaned up code formatting and added missing commas for better readability.
This commit is contained in:
Miha Kralj
2025-12-28 21:07:37 -08:00
parent 52af7057bb
commit 3cc2726654
39 changed files with 7535 additions and 840 deletions
+204 -1
View File
@@ -285,11 +285,14 @@ public class TSeriesTests
series.Add(200, 2.0);
var list = new List<object>();
foreach (var item in (IEnumerable)series)
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);
}
@@ -332,4 +335,204 @@ public class TSeriesTests
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>
IReadOnlyList<long> times = new long[] { 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);
}
}
+64 -8
View File
@@ -6,6 +6,66 @@ using System.Runtime.InteropServices;
namespace QuanTAlib;
/// <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.
@@ -132,14 +192,10 @@ public class TSeries : IReadOnlyList<TValue>, ITValuePublisher
}
}
// IEnumerable implementation
public IEnumerator<TValue> GetEnumerator()
{
for (int i = 0; i < _v.Count; i++)
{
yield return new TValue(_t[i], _v[i]);
}
}
// 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();
}