feat: Add Prime method to various indicators for initializing state with historical data

- Implemented Prime method in Vel, Ao, Apo, Frama, Adl, Adosc, Aobv, Cmf, Efi, Eom, Iii, Kvo, Mfi, Nvi, Obv, Pvd, Pvi, Pvo, Pvr, Pvt, Tvi, Twap, Va, Vf, Vo, Vroc, Vwad, Vwap, and Vwma classes.
- The Prime method resets the indicator state and processes the provided historical bar data to initialize the indicator.
- Added warmup period property to Adl and Wad classes to define the minimum number of data points required for validity.
- Updated benchmark tests to use Batch methods for performance evaluation.
This commit is contained in:
Miha Kralj
2026-02-11 20:38:38 -08:00
parent 75c6a9f135
commit 653aafacd8
71 changed files with 10527 additions and 242 deletions
+658
View File
@@ -598,4 +598,662 @@ public class TBarSeriesTests
Assert.Equal(12.0, closes[0]);
Assert.Equal(22.0, closes[1]);
}
// ────────────────────────────────────────────────────────────────────
// NEW TESTS: TryGetLast
// ────────────────────────────────────────────────────────────────────
[Fact]
public void TryGetLast_EmptySeries_ReturnsFalseAndDefault()
{
var series = new TBarSeries();
bool result = series.TryGetLast(out TBar bar);
Assert.False(result);
Assert.Equal(0, bar.Time);
Assert.Equal(0.0, bar.Close);
}
[Fact]
public void TryGetLast_NonEmptySeries_ReturnsTrueAndLastBar()
{
var series = new TBarSeries();
series.Add(100, 10, 15, 5, 12, 100);
series.Add(200, 20, 25, 15, 22, 200);
bool result = series.TryGetLast(out TBar bar);
Assert.True(result);
Assert.Equal(200, bar.Time);
Assert.Equal(20.0, bar.Open);
Assert.Equal(25.0, bar.High);
Assert.Equal(15.0, bar.Low);
Assert.Equal(22.0, bar.Close);
Assert.Equal(200.0, bar.Volume);
}
[Fact]
public void TryGetLast_SingleBar_ReturnsOnlyBar()
{
var series = new TBarSeries();
series.Add(999, 50, 60, 40, 55, 500);
bool result = series.TryGetLast(out TBar bar);
Assert.True(result);
Assert.Equal(999, bar.Time);
Assert.Equal(55.0, bar.Close);
}
// ────────────────────────────────────────────────────────────────────
// NEW TESTS: Span Accessors (Times, OpenValues, HighValues, etc.)
// ────────────────────────────────────────────────────────────────────
[Fact]
public void Times_ReturnsCorrectSpan()
{
var series = new TBarSeries();
series.Add(100, 10, 15, 5, 12, 100);
series.Add(200, 20, 25, 15, 22, 200);
series.Add(300, 30, 35, 25, 32, 300);
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 Times_EmptySeries_ReturnsEmptySpan()
{
var series = new TBarSeries();
ReadOnlySpan<long> times = series.Times;
Assert.Equal(0, times.Length);
}
[Fact]
public void OpenValues_ReturnsCorrectSpan()
{
var series = new TBarSeries();
series.Add(100, 10, 15, 5, 12, 100);
series.Add(200, 20, 25, 15, 22, 200);
ReadOnlySpan<double> opens = series.OpenValues;
Assert.Equal(2, opens.Length);
Assert.Equal(10.0, opens[0]);
Assert.Equal(20.0, opens[1]);
}
[Fact]
public void HighValues_ReturnsCorrectSpan()
{
var series = new TBarSeries();
series.Add(100, 10, 15, 5, 12, 100);
series.Add(200, 20, 25, 15, 22, 200);
ReadOnlySpan<double> highs = series.HighValues;
Assert.Equal(2, highs.Length);
Assert.Equal(15.0, highs[0]);
Assert.Equal(25.0, highs[1]);
}
[Fact]
public void LowValues_ReturnsCorrectSpan()
{
var series = new TBarSeries();
series.Add(100, 10, 15, 5, 12, 100);
series.Add(200, 20, 25, 15, 22, 200);
ReadOnlySpan<double> lows = series.LowValues;
Assert.Equal(2, lows.Length);
Assert.Equal(5.0, lows[0]);
Assert.Equal(15.0, lows[1]);
}
[Fact]
public void CloseValues_ReturnsCorrectSpan()
{
var series = new TBarSeries();
series.Add(100, 10, 15, 5, 12, 100);
series.Add(200, 20, 25, 15, 22, 200);
ReadOnlySpan<double> closes = series.CloseValues;
Assert.Equal(2, closes.Length);
Assert.Equal(12.0, closes[0]);
Assert.Equal(22.0, closes[1]);
}
[Fact]
public void VolumeValues_ReturnsCorrectSpan()
{
var series = new TBarSeries();
series.Add(100, 10, 15, 5, 12, 100);
series.Add(200, 20, 25, 15, 22, 200);
ReadOnlySpan<double> vols = series.VolumeValues;
Assert.Equal(2, vols.Length);
Assert.Equal(100.0, vols[0]);
Assert.Equal(200.0, vols[1]);
}
[Fact]
public void SpanAccessors_EmptySeries_AllReturnEmptySpans()
{
var series = new TBarSeries();
Assert.Equal(0, series.OpenValues.Length);
Assert.Equal(0, series.HighValues.Length);
Assert.Equal(0, series.LowValues.Length);
Assert.Equal(0, series.CloseValues.Length);
Assert.Equal(0, series.VolumeValues.Length);
}
// ────────────────────────────────────────────────────────────────────
// NEW TESTS: AddRange with 6 span parameters
// ────────────────────────────────────────────────────────────────────
[Fact]
public void AddRange_WithSpans_AddsAllBars()
{
var series = new TBarSeries();
long[] times = [100, 200, 300];
double[] opens = [10, 20, 30];
double[] highs = [15, 25, 35];
double[] lows = [5, 15, 25];
double[] closes = [12, 22, 32];
double[] volumes = [1000, 2000, 3000];
series.AddRange(times, opens, highs, lows, closes, volumes);
Assert.Equal(3, series.Count);
Assert.Equal(100, series[0].Time);
Assert.Equal(10.0, series[0].Open);
Assert.Equal(15.0, series[0].High);
Assert.Equal(5.0, series[0].Low);
Assert.Equal(12.0, series[0].Close);
Assert.Equal(1000.0, series[0].Volume);
Assert.Equal(300, series[2].Time);
Assert.Equal(32.0, series[2].Close);
}
[Fact]
public void AddRange_WithSpans_EmptySpans_DoesNothing()
{
var series = new TBarSeries();
ReadOnlySpan<long> t = ReadOnlySpan<long>.Empty;
ReadOnlySpan<double> d = ReadOnlySpan<double>.Empty;
series.AddRange(t, d, d, d, d, d);
Assert.Empty(series);
}
[Fact]
public void AddRange_WithSpans_MismatchedLengths_ThrowsArgumentException()
{
var series = new TBarSeries();
long[] times = [100, 200, 300];
double[] opens = [10, 20]; // Mismatched
double[] highs = [15, 25, 35];
double[] lows = [5, 15, 25];
double[] closes = [12, 22, 32];
double[] volumes = [1000, 2000, 3000];
Assert.Throws<ArgumentException>(() =>
series.AddRange(times, opens, highs, lows, closes, volumes));
}
[Fact]
public void AddRange_WithSpans_DoesNotFirePubEvent()
{
var series = new TBarSeries();
int pubCount = 0;
series.Pub += (object? sender, in TBarEventArgs args) => pubCount++;
long[] times = [100, 200, 300];
double[] opens = [10, 20, 30];
double[] highs = [15, 25, 35];
double[] lows = [5, 15, 25];
double[] closes = [12, 22, 32];
double[] volumes = [1000, 2000, 3000];
series.AddRange(times, opens, highs, lows, closes, volumes);
Assert.Equal(0, pubCount);
Assert.Equal(3, series.Count);
}
[Fact]
public void AddRange_WithSpans_AppendsToExistingData()
{
var series = new TBarSeries();
series.Add(50, 5, 8, 3, 6, 500);
long[] times = [100, 200];
double[] opens = [10, 20];
double[] highs = [15, 25];
double[] lows = [5, 15];
double[] closes = [12, 22];
double[] volumes = [1000, 2000];
series.AddRange(times, opens, highs, lows, closes, volumes);
Assert.Equal(3, series.Count);
Assert.Equal(50, series[0].Time);
Assert.Equal(100, series[1].Time);
Assert.Equal(200, series[2].Time);
}
[Fact]
public void AddRange_WithSpans_SubSeriesReflectData()
{
var series = new TBarSeries();
long[] times = [100, 200];
double[] opens = [10, 20];
double[] highs = [15, 25];
double[] lows = [5, 15];
double[] closes = [12, 22];
double[] volumes = [1000, 2000];
series.AddRange(times, opens, highs, lows, closes, volumes);
// Sub-series share underlying storage, so they reflect AddRange data
Assert.Equal(2, series.Open.Count);
Assert.Equal(10.0, series.Open[0].Value);
Assert.Equal(20.0, series.Open[1].Value);
Assert.Equal(22.0, series.Close[1].Value);
}
// ────────────────────────────────────────────────────────────────────
// NEW TESTS: AddRange with ReadOnlySpan<TBar>
// ────────────────────────────────────────────────────────────────────
[Fact]
public void AddRange_WithTBarSpan_AddsAllBars()
{
var series = new TBarSeries();
TBar[] bars =
[
new TBar(100, 10, 15, 5, 12, 1000),
new TBar(200, 20, 25, 15, 22, 2000),
new TBar(300, 30, 35, 25, 32, 3000),
];
series.AddRange(bars);
Assert.Equal(3, series.Count);
Assert.Equal(100, series[0].Time);
Assert.Equal(12.0, series[0].Close);
Assert.Equal(300, series[2].Time);
Assert.Equal(32.0, series[2].Close);
Assert.Equal(3000.0, series[2].Volume);
}
[Fact]
public void AddRange_WithTBarSpan_EmptySpan_DoesNothing()
{
var series = new TBarSeries();
series.AddRange(ReadOnlySpan<TBar>.Empty);
Assert.Empty(series);
}
[Fact]
public void AddRange_WithTBarSpan_AppendsToExistingData()
{
var series = new TBarSeries();
series.Add(50, 5, 8, 3, 6, 500);
TBar[] bars =
[
new TBar(100, 10, 15, 5, 12, 1000),
new TBar(200, 20, 25, 15, 22, 2000),
];
series.AddRange(bars);
Assert.Equal(3, series.Count);
Assert.Equal(50, series[0].Time);
Assert.Equal(100, series[1].Time);
Assert.Equal(200, series[2].Time);
}
[Fact]
public void AddRange_WithTBarSpan_DoesNotFirePubEvent()
{
var series = new TBarSeries();
int pubCount = 0;
series.Pub += (object? sender, in TBarEventArgs args) => pubCount++;
TBar[] bars = [new TBar(100, 10, 15, 5, 12, 1000)];
series.AddRange(bars);
Assert.Equal(0, pubCount);
Assert.Single(series);
}
[Fact]
public void AddRange_WithTBarSpan_CorrectlySplitsToSoA()
{
var series = new TBarSeries();
TBar[] bars =
[
new TBar(100, 10, 15, 5, 12, 1000),
new TBar(200, 20, 25, 15, 22, 2000),
];
series.AddRange(bars);
// Verify SoA layout via span accessors
Assert.Equal(100, series.Times[0]);
Assert.Equal(200, series.Times[1]);
Assert.Equal(10.0, series.OpenValues[0]);
Assert.Equal(20.0, series.OpenValues[1]);
Assert.Equal(15.0, series.HighValues[0]);
Assert.Equal(25.0, series.HighValues[1]);
Assert.Equal(5.0, series.LowValues[0]);
Assert.Equal(15.0, series.LowValues[1]);
Assert.Equal(12.0, series.CloseValues[0]);
Assert.Equal(22.0, series.CloseValues[1]);
Assert.Equal(1000.0, series.VolumeValues[0]);
Assert.Equal(2000.0, series.VolumeValues[1]);
}
// ────────────────────────────────────────────────────────────────────
// NEW TESTS: TBarEventArgs struct
// ────────────────────────────────────────────────────────────────────
[Fact]
public void TBarEventArgs_Equals_SameValues_ReturnsTrue()
{
var bar = new TBar(100, 10, 15, 5, 12, 1000);
var a = new TBarEventArgs { Value = bar, IsNew = true };
var b = new TBarEventArgs { Value = bar, IsNew = true };
Assert.True(a.Equals(b));
Assert.True(a == b);
Assert.False(a != b);
}
[Fact]
public void TBarEventArgs_Equals_DifferentIsNew_ReturnsFalse()
{
var bar = new TBar(100, 10, 15, 5, 12, 1000);
var a = new TBarEventArgs { Value = bar, IsNew = true };
var b = new TBarEventArgs { Value = bar, IsNew = false };
Assert.False(a.Equals(b));
Assert.False(a == b);
Assert.True(a != b);
}
[Fact]
public void TBarEventArgs_Equals_DifferentValue_ReturnsFalse()
{
var bar1 = new TBar(100, 10, 15, 5, 12, 1000);
var bar2 = new TBar(200, 20, 25, 15, 22, 2000);
var a = new TBarEventArgs { Value = bar1, IsNew = true };
var b = new TBarEventArgs { Value = bar2, IsNew = true };
Assert.False(a.Equals(b));
}
[Fact]
public void TBarEventArgs_Equals_Object_SameValues_ReturnsTrue()
{
var bar = new TBar(100, 10, 15, 5, 12, 1000);
var a = new TBarEventArgs { Value = bar, IsNew = true };
object b = new TBarEventArgs { Value = bar, IsNew = true };
Assert.True(a.Equals(b));
}
[Fact]
public void TBarEventArgs_Equals_Object_DifferentType_ReturnsFalse()
{
var bar = new TBar(100, 10, 15, 5, 12, 1000);
var a = new TBarEventArgs { Value = bar, IsNew = true };
Assert.False(a.Equals("not a TBarEventArgs"));
Assert.False(a.Equals(null));
}
[Fact]
public void TBarEventArgs_GetHashCode_EqualObjects_SameHash()
{
var bar = new TBar(100, 10, 15, 5, 12, 1000);
var a = new TBarEventArgs { Value = bar, IsNew = true };
var b = new TBarEventArgs { Value = bar, IsNew = true };
Assert.Equal(a.GetHashCode(), b.GetHashCode());
}
[Fact]
public void TBarEventArgs_GetHashCode_DifferentObjects_LikelyDifferentHash()
{
var bar1 = new TBar(100, 10, 15, 5, 12, 1000);
var bar2 = new TBar(200, 20, 25, 15, 22, 2000);
var a = new TBarEventArgs { Value = bar1, IsNew = true };
var b = new TBarEventArgs { Value = bar2, IsNew = false };
// Not guaranteed but extremely likely for distinct values
Assert.NotEqual(a.GetHashCode(), b.GetHashCode());
}
// ────────────────────────────────────────────────────────────────────
// NEW TESTS: TBarSeriesEnumerator struct
// ────────────────────────────────────────────────────────────────────
[Fact]
public void TBarSeriesEnumerator_Reset_AllowsReIteration()
{
var series = new TBarSeries();
series.Add(100, 10, 15, 5, 12, 100);
series.Add(200, 20, 25, 15, 22, 200);
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 TBarSeriesEnumerator_Dispose_DoesNotThrow()
{
var series = new TBarSeries();
series.Add(100, 10, 15, 5, 12, 100);
var enumerator = series.GetEnumerator();
enumerator.MoveNext();
enumerator.Dispose(); // Should be no-op
Assert.Equal(12.0, enumerator.Current.Close);
}
[Fact]
public void TBarSeriesEnumerator_Equals_SameState_ReturnsTrue()
{
var series = new TBarSeries();
series.Add(100, 10, 15, 5, 12, 100);
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 TBarSeriesEnumerator_Equals_DifferentState_ReturnsFalse()
{
var series = new TBarSeries();
series.Add(100, 10, 15, 5, 12, 100);
series.Add(200, 20, 25, 15, 22, 200);
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 TBarSeriesEnumerator_Equals_Object_SameState_ReturnsTrue()
{
var series = new TBarSeries();
series.Add(100, 10, 15, 5, 12, 100);
var a = series.GetEnumerator();
object b = series.GetEnumerator();
Assert.True(a.Equals(b));
}
[Fact]
public void TBarSeriesEnumerator_Equals_Object_DifferentType_ReturnsFalse()
{
var series = new TBarSeries();
series.Add(100, 10, 15, 5, 12, 100);
var a = series.GetEnumerator();
Assert.False(a.Equals("not an enumerator"));
Assert.False(a.Equals(null));
}
[Fact]
public void TBarSeriesEnumerator_GetHashCode_SameState_SameHash()
{
var series = new TBarSeries();
series.Add(100, 10, 15, 5, 12, 100);
var a = series.GetEnumerator();
var b = series.GetEnumerator();
Assert.Equal(a.GetHashCode(), b.GetHashCode());
}
[Fact]
public void TBarSeriesEnumerator_GetHashCode_DifferentState_LikelyDifferentHash()
{
var series = new TBarSeries();
series.Add(100, 10, 15, 5, 12, 100);
series.Add(200, 20, 25, 15, 22, 200);
var a = series.GetEnumerator();
var b = series.GetEnumerator();
a.MoveNext();
Assert.NotEqual(a.GetHashCode(), b.GetHashCode());
}
[Fact]
public void TBarSeriesEnumerator_Current_ViaIEnumerator_ReturnsBoxedTBar()
{
var series = new TBarSeries();
series.Add(100, 10, 15, 5, 12, 100);
IEnumerator enumerator = series.GetEnumerator();
enumerator.MoveNext();
object current = enumerator.Current;
Assert.IsType<TBar>(current);
Assert.Equal(12.0, ((TBar)current).Close);
}
[Fact]
public void TBarSeriesEnumerator_DifferentSeries_NotEqual()
{
var series1 = new TBarSeries();
series1.Add(100, 10, 15, 5, 12, 100);
var series2 = new TBarSeries();
series2.Add(100, 10, 15, 5, 12, 100);
var a = series1.GetEnumerator();
var b = series2.GetEnumerator();
// Different underlying list references -> not equal
Assert.False(a.Equals(b));
}
// ────────────────────────────────────────────────────────────────────
// NEW TESTS: AddRange large data set (capacity pre-alloc path)
// ────────────────────────────────────────────────────────────────────
[Fact]
public void AddRange_LargeDataSet_AllBarsAccessible()
{
var series = new TBarSeries();
const int N = 1000;
var times = new long[N];
var opens = new double[N];
var highs = new double[N];
var lows = new double[N];
var closes = new double[N];
var volumes = new double[N];
for (int i = 0; i < N; i++)
{
times[i] = i;
opens[i] = i * 10.0;
highs[i] = i * 10.0 + 5.0;
lows[i] = i * 10.0 - 5.0;
closes[i] = i * 10.0 + 2.0;
volumes[i] = i * 100.0;
}
series.AddRange(times, opens, highs, lows, closes, volumes);
Assert.Equal(N, series.Count);
Assert.Equal(0, series[0].Time);
Assert.Equal((N - 1) * 10.0 + 2.0, series[N - 1].Close);
Assert.Equal((N - 1) * 100.0, series[N - 1].Volume);
}
[Fact]
public void AddRange_TBarSpan_LargeDataSet_AllBarsAccessible()
{
var series = new TBarSeries();
const int N = 500;
var bars = new TBar[N];
for (int i = 0; i < N; i++)
{
bars[i] = new TBar(i, i * 10.0, i * 10.0 + 5.0, i * 10.0 - 5.0, i * 10.0 + 2.0, i * 100.0);
}
series.AddRange(bars);
Assert.Equal(N, series.Count);
Assert.Equal(0, series[0].Time);
Assert.Equal(2.0, series[0].Close);
Assert.Equal((N - 1) * 10.0 + 2.0, series[N - 1].Close);
}
}