Files
Miha Kralj 060649192f docs: remove C# Implementation Considerations sections, clean up temp scripts, reorganize test files
- Remove 'C# Implementation Considerations' sections from 34 indicator .md files
- Delete 29 temp PowerShell scripts (_fix_mojibake.ps1, _hex_scan.ps1, etc.)
- Move test files into tests/ subdirectories for consistent project structure
- Add trader-focused bullet points to indicator documentation
2026-03-12 12:34:16 -07:00

795 lines
21 KiB
C#

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]);
}
// ────────────────────────────────────────────────────────────────────
// NEW TESTS: ShareStorageTag struct
// ────────────────────────────────────────────────────────────────────
[Fact]
public void ShareStorageTag_Instance_IsDefault()
{
var tag = ShareStorageTag.Instance;
// It's a readonly struct with no fields — default should work
Assert.Equal(default(ShareStorageTag), tag);
}
[Fact]
public void ShareStorageTag_TwoInstances_AreEqual()
{
var a = ShareStorageTag.Instance;
var b = ShareStorageTag.Instance;
Assert.Equal(a, b);
}
// ────────────────────────────────────────────────────────────────────
// NEW TESTS: TSeriesEnumerator struct
// ────────────────────────────────────────────────────────────────────
[Fact]
public void TSeriesEnumerator_Reset_AllowsReIteration()
{
var series = new TSeries();
series.Add(100, 1.0);
series.Add(200, 2.0);
var enumerator = series.GetEnumerator();
// First pass
int count1 = 0;
while (enumerator.MoveNext()) { count1++; }
Assert.Equal(2, count1);
// Reset and re-iterate
enumerator.Reset();
int count2 = 0;
while (enumerator.MoveNext()) { count2++; }
Assert.Equal(2, count2);
}
[Fact]
public void TSeriesEnumerator_Dispose_DoesNotThrow()
{
var series = new TSeries();
series.Add(100, 1.0);
var enumerator = series.GetEnumerator();
enumerator.MoveNext();
enumerator.Dispose(); // Should be no-op
Assert.Equal(1.0, enumerator.Current.Value);
}
[Fact]
public void TSeriesEnumerator_Equals_SameState_ReturnsTrue()
{
var series = new TSeries();
series.Add(100, 1.0);
var a = series.GetEnumerator();
var b = series.GetEnumerator();
// Both at initial state (_index = -1)
Assert.True(a.Equals(b));
Assert.True(a == b);
Assert.False(a != b);
}
[Fact]
public void TSeriesEnumerator_Equals_DifferentState_ReturnsFalse()
{
var series = new TSeries();
series.Add(100, 1.0);
series.Add(200, 2.0);
var a = series.GetEnumerator();
var b = series.GetEnumerator();
a.MoveNext(); // a is at index 0, b is at -1
Assert.False(a.Equals(b));
Assert.False(a == b);
Assert.True(a != b);
}
[Fact]
public void TSeriesEnumerator_Equals_Object_SameState_ReturnsTrue()
{
var series = new TSeries();
series.Add(100, 1.0);
var a = series.GetEnumerator();
object b = series.GetEnumerator();
Assert.True(a.Equals(b));
}
[Fact]
public void TSeriesEnumerator_Equals_Object_DifferentType_ReturnsFalse()
{
var series = new TSeries();
series.Add(100, 1.0);
var a = series.GetEnumerator();
Assert.False(a.Equals("not an enumerator"));
Assert.False(a.Equals(null));
}
[Fact]
public void TSeriesEnumerator_GetHashCode_SameState_SameHash()
{
var series = new TSeries();
series.Add(100, 1.0);
var a = series.GetEnumerator();
var b = series.GetEnumerator();
Assert.Equal(a.GetHashCode(), b.GetHashCode());
}
[Fact]
public void TSeriesEnumerator_GetHashCode_DifferentState_LikelyDifferentHash()
{
var series = new TSeries();
series.Add(100, 1.0);
series.Add(200, 2.0);
var a = series.GetEnumerator();
var b = series.GetEnumerator();
a.MoveNext();
Assert.NotEqual(a.GetHashCode(), b.GetHashCode());
}
[Fact]
public void TSeriesEnumerator_Current_ViaIEnumerator_ReturnsBoxedTValue()
{
var series = new TSeries();
series.Add(100, 42.0);
IEnumerator enumerator = series.GetEnumerator();
enumerator.MoveNext();
object current = enumerator.Current;
Assert.IsType<TValue>(current);
Assert.Equal(42.0, ((TValue)current).Value);
}
[Fact]
public void TSeriesEnumerator_DifferentSeries_NotEqual()
{
var series1 = new TSeries();
series1.Add(100, 1.0);
var series2 = new TSeries();
series2.Add(100, 1.0);
var a = series1.GetEnumerator();
var b = series2.GetEnumerator();
// Different underlying list references -> not equal
Assert.False(a.Equals(b));
}
// ────────────────────────────────────────────────────────────────────
// NEW TESTS: Constructor defensive copy verification
// ────────────────────────────────────────────────────────────────────
[Fact]
public void Constructor_WithReadOnlyLists_MakesDefensiveCopy()
{
var times = new List<long> { 100, 200 };
var values = new List<double> { 1.0, 2.0 };
// Use IReadOnlyList<T> constructor which makes defensive copies
var series = new TSeries((IReadOnlyList<long>)times, (IReadOnlyList<double>)values);
// Mutate original lists
times.Add(300);
values.Add(3.0);
// Series should NOT reflect the mutation (defensive copy was made)
Assert.Equal(2, series.Count);
}
[Fact]
public void Constructor_WithReadOnlyLists_OriginalMutationDoesNotAffectSeries()
{
long[] originalTimes = [100, 200, 300];
double[] originalValues = [1.0, 2.0, 3.0];
var series = new TSeries(originalTimes, originalValues);
// Mutate original arrays
originalValues[0] = 999.0;
// Series should NOT reflect the mutation (defensive copy was made)
Assert.Equal(1.0, series[0].Value);
}
}