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
+333 -31
View File
@@ -11,6 +11,8 @@ public class WienerTests
_gbm = new GBM(startPrice: 100, mu: 0.05, sigma: 0.2, seed: 123);
}
// ── Constructor ──────────────────────────────────────────────────────
[Fact]
public void Constructor_ValidatesInput()
{
@@ -22,6 +24,208 @@ public class WienerTests
Assert.Equal("Wiener(10,10)", wiener.Name);
}
[Fact]
public void Constructor_Period1_Throws()
{
Assert.Throws<ArgumentOutOfRangeException>(() => new Wiener(1));
}
[Fact]
public void Constructor_SmoothPeriod1_Throws()
{
Assert.Throws<ArgumentOutOfRangeException>(() => new Wiener(10, 1));
}
[Fact]
public void Constructor_DefaultSmoothPeriod()
{
var wiener = new Wiener(10);
Assert.Equal("Wiener(10,10)", wiener.Name);
}
[Fact]
public void Constructor_WarmupPeriod_IsMax()
{
var wiener = new Wiener(20, 5);
Assert.Equal(20, wiener.WarmupPeriod);
var wiener2 = new Wiener(5, 20);
Assert.Equal(20, wiener2.WarmupPeriod);
}
// ── IsHot / WarmupPeriod ────────────────────────────────────────────
[Fact]
public void WarmupPeriod_IsCorrect()
{
int period = 20;
int smooth = 10;
var wiener = new Wiener(period, smooth);
// Requirement: WarmupPeriod = Math.Max(period, smooth)
Assert.Equal(Math.Max(period, smooth), wiener.WarmupPeriod);
Assert.False(wiener.IsHot);
for (int i = 0; i < Math.Max(period, smooth); i++)
{
wiener.Update(new TValue(DateTime.UtcNow, 100));
}
Assert.True(wiener.IsHot);
}
[Fact]
public void IsHot_FalseBeforeWarmup()
{
var wiener = new Wiener(10, 10);
for (int i = 0; i < 9; i++)
{
wiener.Update(new TValue(DateTime.UtcNow, 100));
}
Assert.False(wiener.IsHot);
}
// ── Update ──────────────────────────────────────────────────────────
[Fact]
public void Update_SingleValue_ReturnsItself()
{
var wiener = new Wiener(5, 5);
var result = wiener.Update(new TValue(DateTime.UtcNow, 42.0));
Assert.Equal(42.0, result.Value);
}
[Fact]
public void Update_ConstantInput_ConvergesToConstant()
{
var wiener = new Wiener(10, 5);
for (int i = 0; i < 50; i++)
{
wiener.Update(new TValue(DateTime.UtcNow, 42.0));
}
Assert.Equal(42.0, wiener.Last.Value, 1e-6);
}
[Fact]
public void Update_SmoothsNoisySignal()
{
var wiener = new Wiener(20, 10);
var gbm = new GBM(seed: 42);
var bars = gbm.Fetch(100, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1));
foreach (var bar in bars)
{
wiener.Update(new TValue(bar.Time, bar.Close));
}
Assert.True(wiener.IsHot);
Assert.True(double.IsFinite(wiener.Last.Value));
}
[Fact]
public void Update_BarCorrection_Works()
{
var wiener = new Wiener(5, 5);
var now = DateTime.UtcNow;
for (int i = 0; i < 10; i++)
{
wiener.Update(new TValue(now.AddMinutes(i), 100 + i));
}
// New bar
var result1 = wiener.Update(new TValue(now.AddMinutes(10), 200), isNew: true);
// Correction
var result2 = wiener.Update(new TValue(now.AddMinutes(10), 150), isNew: false);
// Different correction values should yield different results
Assert.NotEqual(result1.Value, result2.Value);
}
// ── NaN handling ────────────────────────────────────────────────────
[Fact]
public void HandlesNaN()
{
var wiener = new Wiener(5, 5);
wiener.Update(new TValue(DateTime.UtcNow, 100));
wiener.Update(new TValue(DateTime.UtcNow, double.NaN));
wiener.Update(new TValue(DateTime.UtcNow, 102));
// Should produce valid result if sufficient valid data exists within window (or handle it gracefully)
Assert.True(double.IsFinite(wiener.Last.Value));
}
[Fact]
public void NaN_FirstValue_UsesFallback()
{
var wiener = new Wiener(5, 5);
var result = wiener.Update(new TValue(DateTime.UtcNow, double.NaN));
// Fallback: Last.Value defaults to 0.0 (finite), so code returns 0.0
// The code: double.IsFinite(Last.Value) ? Last.Value : input.Value
Assert.Equal(0.0, result.Value);
}
[Fact]
public void NaN_AfterValid_UsesLastValid()
{
var wiener = new Wiener(5, 5);
wiener.Update(new TValue(DateTime.UtcNow, 100));
var result = wiener.Update(new TValue(DateTime.UtcNow, double.NaN));
Assert.Equal(100.0, result.Value);
}
[Fact]
public void Infinity_AfterValid_UsesLastValid()
{
var wiener = new Wiener(5, 5);
wiener.Update(new TValue(DateTime.UtcNow, 100));
var result = wiener.Update(new TValue(DateTime.UtcNow, double.PositiveInfinity));
Assert.Equal(100.0, result.Value);
}
// ── Reset ───────────────────────────────────────────────────────────
[Fact]
public void Reset_ClearsState()
{
var wiener = new Wiener(10, 5);
int warmup = Math.Max(10, 5);
// Fill up to make it Hot
for (int i = 0; i < warmup; i++)
{
wiener.Update(new TValue(DateTime.UtcNow, 100 + i));
}
Assert.True(wiener.IsHot);
wiener.Reset();
Assert.False(wiener.IsHot);
}
[Fact]
public void Reset_AllowsReuse()
{
var wiener = new Wiener(5, 5);
for (int i = 0; i < 10; i++)
{
wiener.Update(new TValue(DateTime.UtcNow, 100 + i));
}
var firstResult = wiener.Last.Value;
wiener.Reset();
for (int i = 0; i < 10; i++)
{
wiener.Update(new TValue(DateTime.UtcNow, 100 + i));
}
Assert.Equal(firstResult, wiener.Last.Value, 1e-10);
}
// ── Batch ───────────────────────────────────────────────────────────
[Fact]
public void AllModes_ProduceSameResult()
{
@@ -67,50 +271,148 @@ public class WienerTests
}
[Fact]
public void HandlesNaN()
public void Batch_TSeries_Static()
{
var wiener = new Wiener(5, 5);
wiener.Update(new TValue(DateTime.UtcNow, 100));
wiener.Update(new TValue(DateTime.UtcNow, double.NaN));
wiener.Update(new TValue(DateTime.UtcNow, 102));
// Should produce valid result if sufficient valid data exists within window (or handle it gracefully)
Assert.True(double.IsFinite(wiener.Last.Value));
var data = _gbm.Fetch(50, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1));
var result = Wiener.Batch(data.Close, 10, 5);
Assert.Equal(50, result.Count);
}
[Fact]
public void WarmupPeriod_IsCorrect()
public void Batch_Span_OutputTooShort_Throws()
{
int period = 20;
int smooth = 10;
var wiener = new Wiener(period, smooth);
// Requirement: WarmupPeriod = Math.Max(period, smooth)
Assert.Equal(Math.Max(period, smooth), wiener.WarmupPeriod);
Assert.False(wiener.IsHot);
for (int i = 0; i < Math.Max(period, smooth); i++)
{
wiener.Update(new TValue(DateTime.UtcNow, 100));
}
Assert.True(wiener.IsHot);
var source = new double[10];
var output = new double[5];
Assert.Throws<ArgumentException>(() => Wiener.Batch(source, output, 5, 5));
}
[Fact]
public void Reset_ClearsState()
public void Update_EmptyTSeries_ReturnsEmpty()
{
var wiener = new Wiener(10, 5);
int warmup = Math.Max(10, 5);
var result = wiener.Update(new TSeries());
Assert.Empty(result);
}
// Fill up to make it Hot
for (int i = 0; i < warmup; i++)
[Fact]
public void Update_TSeries_ReturnsCorrectCount()
{
var wiener = new Wiener(10, 5);
var data = _gbm.Fetch(50, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1));
var result = wiener.Update(data.Close);
Assert.Equal(50, result.Count);
}
// ── Calculate ───────────────────────────────────────────────────────
[Fact]
public void Calculate_ReturnsResultsAndIndicator()
{
var data = _gbm.Fetch(50, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1));
var (results, indicator) = Wiener.Calculate(data.Close, 10, 5);
Assert.Equal(50, results.Count);
Assert.True(indicator.IsHot);
}
// ── Prime ───────────────────────────────────────────────────────────
[Fact]
public void Prime_WarmsUpIndicator()
{
var wiener = new Wiener(10, 5);
var values = new double[20];
for (int i = 0; i < 20; i++)
{
wiener.Update(new TValue(DateTime.UtcNow, 100 + i));
values[i] = 100 + i;
}
Assert.True(wiener.IsHot);
wiener.Reset();
Assert.False(wiener.IsHot);
wiener.Prime(values);
Assert.True(wiener.IsHot);
}
[Fact]
public void Prime_WithStepParameter()
{
var wiener = new Wiener(10, 5);
var values = new double[20];
for (int i = 0; i < 20; i++)
{
values[i] = 100 + i;
}
wiener.Prime(values, TimeSpan.FromMinutes(5));
Assert.True(wiener.IsHot);
}
// ── Determinism ─────────────────────────────────────────────────────
[Fact]
public void TwoInstances_SameInput_SameOutput()
{
var w1 = new Wiener(10, 5);
var w2 = new Wiener(10, 5);
var gbm = new GBM(seed: 42);
var bars = gbm.Fetch(50, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1));
foreach (var bar in bars)
{
var tv = new TValue(bar.Time, bar.Close);
var r1 = w1.Update(tv);
var r2 = w2.Update(tv);
Assert.Equal(r1.Value, r2.Value);
}
}
// ── Filter behavior ─────────────────────────────────────────────────
[Fact]
public void LargerPeriod_MoreSmoothing()
{
var smallPeriod = new Wiener(5, 5);
var largePeriod = new Wiener(20, 5);
var gbm = new GBM(seed: 42);
var bars = gbm.Fetch(100, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1));
double sumDiffSmall = 0, sumDiffLarge = 0;
int count = 0;
foreach (var bar in bars)
{
var tv = new TValue(bar.Time, bar.Close);
var rSmall = smallPeriod.Update(tv);
var rLarge = largePeriod.Update(tv);
if (smallPeriod.IsHot && largePeriod.IsHot)
{
sumDiffSmall += Math.Abs(bar.Close - rSmall.Value);
sumDiffLarge += Math.Abs(bar.Close - rLarge.Value);
count++;
}
}
// Both should produce finite outputs
Assert.True(double.IsFinite(smallPeriod.Last.Value));
Assert.True(double.IsFinite(largePeriod.Last.Value));
Assert.True(count > 0);
}
[Fact]
public void DifferentSmoothPeriod_ProduceDifferentOutputs()
{
var smooth5 = new Wiener(10, 5);
var smooth15 = new Wiener(10, 15);
var gbm = new GBM(seed: 42);
var bars = gbm.Fetch(50, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1));
double last5 = 0, last15 = 0;
foreach (var bar in bars)
{
var tv = new TValue(bar.Time, bar.Close);
last5 = smooth5.Update(tv).Value;
last15 = smooth15.Update(tv).Value;
}
Assert.NotEqual(last5, last15, 1e-6);
}
}