mirror of
https://github.com/mihakralj/QuanTAlib.git
synced 2026-08-15 17:18:05 +00:00
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:
@@ -12,6 +12,8 @@ public class Cheby1Tests
|
||||
_gbm = new GBM(startPrice: 100, mu: 0.05, sigma: 0.2, seed: 1234);
|
||||
}
|
||||
|
||||
// ── Constructor ──────────────────────────────────────────────────────
|
||||
|
||||
[Fact]
|
||||
public void Constructor_ValidatesParameters()
|
||||
{
|
||||
@@ -21,13 +23,48 @@ public class Cheby1Tests
|
||||
Assert.NotNull(filter);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Constructor_NegativeRipple_Throws()
|
||||
{
|
||||
Assert.Throws<ArgumentOutOfRangeException>(() => new Cheby1(10, -1.0));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Constructor_SetsProperties()
|
||||
{
|
||||
var filter = new Cheby1(20, 2.0);
|
||||
Assert.Equal(20, filter.Period);
|
||||
Assert.Equal(2.0, filter.Ripple);
|
||||
Assert.Equal("Cheby1(20,2.0)", filter.Name);
|
||||
Assert.Equal(20, filter.WarmupPeriod);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Constructor_DefaultRipple()
|
||||
{
|
||||
var filter = new Cheby1(10);
|
||||
Assert.Equal(1.0, filter.Ripple);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Constructor_WithPublisher_Subscribes()
|
||||
{
|
||||
var source = new TSeries();
|
||||
var filter = new Cheby1(source, 10, 1.0);
|
||||
|
||||
source.Add(new TValue(DateTime.UtcNow, 100));
|
||||
Assert.True(double.IsFinite(filter.Last.Value));
|
||||
}
|
||||
|
||||
// ── IsHot ───────────────────────────────────────────────────────────
|
||||
|
||||
[Fact]
|
||||
public void IsHot_BecomesTrueWhenReady()
|
||||
{
|
||||
var filter = new Cheby1(20, 1.0);
|
||||
Assert.False(filter.IsHot);
|
||||
|
||||
// Feed some data
|
||||
// Feed some data - IsHot becomes true when Count >= 2
|
||||
for (int i = 0; i < 3; i++)
|
||||
{
|
||||
filter.Update(new TValue(DateTime.UtcNow, 100.0));
|
||||
@@ -36,6 +73,36 @@ public class Cheby1Tests
|
||||
Assert.True(filter.IsHot);
|
||||
}
|
||||
|
||||
// ── Update ──────────────────────────────────────────────────────────
|
||||
|
||||
[Fact]
|
||||
public void Update_ConstantInput_ConvergesToConstant()
|
||||
{
|
||||
var filter = new Cheby1(10, 1.0);
|
||||
for (int i = 0; i < 50; i++)
|
||||
{
|
||||
filter.Update(new TValue(DateTime.UtcNow, 42.0));
|
||||
}
|
||||
Assert.Equal(42.0, filter.Last.Value, 1e-4);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Update_SmoothsNoisySignal()
|
||||
{
|
||||
var filter = new Cheby1(20, 1.0);
|
||||
var gbm = new GBM(seed: 42);
|
||||
var bars = gbm.Fetch(100, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1));
|
||||
|
||||
foreach (var bar in bars)
|
||||
{
|
||||
filter.Update(new TValue(bar.Time, bar.Close));
|
||||
}
|
||||
|
||||
Assert.True(double.IsFinite(filter.Last.Value));
|
||||
}
|
||||
|
||||
// ── NaN handling ────────────────────────────────────────────────────
|
||||
|
||||
[Fact]
|
||||
public void Update_HandlesNaNSafely()
|
||||
{
|
||||
@@ -51,6 +118,18 @@ public class Cheby1Tests
|
||||
Assert.True(double.IsFinite(result.Value));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Update_HandlesInfinitySafely()
|
||||
{
|
||||
var filter = new Cheby1(10, 1.0);
|
||||
filter.Update(new TValue(DateTime.UtcNow, 100.0));
|
||||
|
||||
var result = filter.Update(new TValue(DateTime.UtcNow, double.PositiveInfinity));
|
||||
Assert.True(double.IsFinite(result.Value));
|
||||
}
|
||||
|
||||
// ── Bar Correction ──────────────────────────────────────────────────
|
||||
|
||||
[Fact]
|
||||
public void IterativeCorrections_RestoreState()
|
||||
{
|
||||
@@ -77,14 +156,48 @@ public class Cheby1Tests
|
||||
Assert.NotEqual(valAfterNew, valAfterCorrection);
|
||||
|
||||
// 3. Correction back to original value (isNew=false)
|
||||
// Note: For IIR this should theoretically restore state, but due to FP math it might drift slightly
|
||||
// But the previous state property should make it exact if we haven't advanced further
|
||||
filter.Update(new TValue(time.AddSeconds(5), 200.0), isNew: false);
|
||||
double valRestored = filter.Last.Value;
|
||||
|
||||
Assert.Equal(valAfterNew, valRestored, 1e-10);
|
||||
}
|
||||
|
||||
// ── Reset ───────────────────────────────────────────────────────────
|
||||
|
||||
[Fact]
|
||||
public void Reset_ClearsState()
|
||||
{
|
||||
var filter = new Cheby1(10, 1.0);
|
||||
for (int i = 0; i < 10; i++)
|
||||
{
|
||||
filter.Update(new TValue(DateTime.UtcNow, 100 + i));
|
||||
}
|
||||
Assert.True(filter.IsHot);
|
||||
|
||||
filter.Reset();
|
||||
Assert.False(filter.IsHot);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Reset_AllowsReuse()
|
||||
{
|
||||
var filter = new Cheby1(10, 1.0);
|
||||
for (int i = 0; i < 20; i++)
|
||||
{
|
||||
filter.Update(new TValue(DateTime.UtcNow, 100 + i));
|
||||
}
|
||||
var firstResult = filter.Last.Value;
|
||||
|
||||
filter.Reset();
|
||||
for (int i = 0; i < 20; i++)
|
||||
{
|
||||
filter.Update(new TValue(DateTime.UtcNow, 100 + i));
|
||||
}
|
||||
Assert.Equal(firstResult, filter.Last.Value, 1e-10);
|
||||
}
|
||||
|
||||
// ── Batch ───────────────────────────────────────────────────────────
|
||||
|
||||
[Fact]
|
||||
public void SpanBatch_MatchesIterative()
|
||||
{
|
||||
@@ -111,4 +224,122 @@ public class Cheby1Tests
|
||||
Assert.Equal(iterativeResults[i], spanResults[i], 1e-10);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Batch_TSeries_Static()
|
||||
{
|
||||
var data = _gbm.Fetch(50, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1));
|
||||
var result = Cheby1.Batch(data.Close, 10, 1.0);
|
||||
Assert.Equal(50, result.Count);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Update_EmptyTSeries_ReturnsEmpty()
|
||||
{
|
||||
var filter = new Cheby1(10, 1.0);
|
||||
var result = filter.Update(new TSeries());
|
||||
Assert.Empty(result);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Update_TSeries_ReturnsCorrectCount()
|
||||
{
|
||||
var filter = new Cheby1(10, 1.0);
|
||||
var data = _gbm.Fetch(50, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1));
|
||||
var result = filter.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) = Cheby1.Calculate(data.Close, 10, 1.0);
|
||||
Assert.Equal(50, results.Count);
|
||||
Assert.True(indicator.IsHot);
|
||||
}
|
||||
|
||||
// ── Prime ───────────────────────────────────────────────────────────
|
||||
|
||||
[Fact]
|
||||
public void Prime_WarmsUpIndicator()
|
||||
{
|
||||
var filter = new Cheby1(10, 1.0);
|
||||
var values = new double[20];
|
||||
for (int i = 0; i < 20; i++)
|
||||
{
|
||||
values[i] = 100 + i;
|
||||
}
|
||||
|
||||
filter.Prime(values);
|
||||
Assert.True(filter.IsHot);
|
||||
}
|
||||
|
||||
// ── Dispose ─────────────────────────────────────────────────────────
|
||||
|
||||
[Fact]
|
||||
public void Dispose_WithPublisher_Unsubscribes()
|
||||
{
|
||||
var source = new TSeries();
|
||||
var filter = new Cheby1(source, 10, 1.0);
|
||||
source.Add(new TValue(DateTime.UtcNow, 100));
|
||||
|
||||
filter.Dispose();
|
||||
var lastBefore = filter.Last;
|
||||
source.Add(new TValue(DateTime.UtcNow, 200));
|
||||
Assert.Equal(lastBefore, filter.Last);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Dispose_WithoutPublisher_DoesNotThrow()
|
||||
{
|
||||
var filter = new Cheby1(10, 1.0);
|
||||
filter.Update(new TValue(DateTime.UtcNow, 100));
|
||||
filter.Dispose();
|
||||
Assert.True(true); // S2699: explicit assertion for dispose-only test
|
||||
}
|
||||
|
||||
// ── Determinism ─────────────────────────────────────────────────────
|
||||
|
||||
[Fact]
|
||||
public void TwoInstances_SameInput_SameOutput()
|
||||
{
|
||||
var f1 = new Cheby1(10, 1.0);
|
||||
var f2 = new Cheby1(10, 1.0);
|
||||
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 = f1.Update(tv);
|
||||
var r2 = f2.Update(tv);
|
||||
Assert.Equal(r1.Value, r2.Value);
|
||||
}
|
||||
}
|
||||
|
||||
// ── Filter behavior ─────────────────────────────────────────────────
|
||||
|
||||
[Fact]
|
||||
public void DifferentRipple_ProduceDifferentOutputs()
|
||||
{
|
||||
var lowRipple = new Cheby1(10, 0.5);
|
||||
var highRipple = new Cheby1(10, 3.0);
|
||||
|
||||
var gbm = new GBM(seed: 42);
|
||||
var bars = gbm.Fetch(50, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1));
|
||||
|
||||
double lastLow = 0, lastHigh = 0;
|
||||
foreach (var bar in bars)
|
||||
{
|
||||
var tv = new TValue(bar.Time, bar.Close);
|
||||
lastLow = lowRipple.Update(tv).Value;
|
||||
lastHigh = highRipple.Update(tv).Value;
|
||||
}
|
||||
|
||||
// Different ripple parameters should generally produce different outputs
|
||||
Assert.NotEqual(lastLow, lastHigh, 1e-6);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,6 +12,8 @@ public class Cheby2Tests
|
||||
_gbm = new GBM(startPrice: 100, mu: 0.05, sigma: 0.2, seed: 1234);
|
||||
}
|
||||
|
||||
// ── Constructor ──────────────────────────────────────────────────────
|
||||
|
||||
[Fact]
|
||||
public void Constructor_ValidatesParameters()
|
||||
{
|
||||
@@ -21,6 +23,41 @@ public class Cheby2Tests
|
||||
Assert.NotNull(filter);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Constructor_NegativeAttenuation_Throws()
|
||||
{
|
||||
Assert.Throws<ArgumentOutOfRangeException>(() => new Cheby2(10, -1.0));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Constructor_SetsProperties()
|
||||
{
|
||||
var filter = new Cheby2(20, 10.0);
|
||||
Assert.Equal(20, filter.Period);
|
||||
Assert.Equal(10.0, filter.Attenuation);
|
||||
Assert.Equal("Cheby2(20,10.0)", filter.Name);
|
||||
Assert.Equal(20, filter.WarmupPeriod);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Constructor_DefaultAttenuation()
|
||||
{
|
||||
var filter = new Cheby2(10);
|
||||
Assert.Equal(5.0, filter.Attenuation);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Constructor_WithPublisher_Subscribes()
|
||||
{
|
||||
var source = new TSeries();
|
||||
var filter = new Cheby2(source, 10, 5.0);
|
||||
|
||||
source.Add(new TValue(DateTime.UtcNow, 100));
|
||||
Assert.True(double.IsFinite(filter.Last.Value));
|
||||
}
|
||||
|
||||
// ── IsHot ───────────────────────────────────────────────────────────
|
||||
|
||||
[Fact]
|
||||
public void IsHot_BecomesTrueWhenReady()
|
||||
{
|
||||
@@ -36,6 +73,36 @@ public class Cheby2Tests
|
||||
Assert.True(filter.IsHot);
|
||||
}
|
||||
|
||||
// ── Update ──────────────────────────────────────────────────────────
|
||||
|
||||
[Fact]
|
||||
public void Update_ConstantInput_ConvergesToConstant()
|
||||
{
|
||||
var filter = new Cheby2(10, 5.0);
|
||||
for (int i = 0; i < 50; i++)
|
||||
{
|
||||
filter.Update(new TValue(DateTime.UtcNow, 42.0));
|
||||
}
|
||||
Assert.Equal(42.0, filter.Last.Value, 1e-4);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Update_SmoothsNoisySignal()
|
||||
{
|
||||
var filter = new Cheby2(20, 5.0);
|
||||
var gbm = new GBM(seed: 42);
|
||||
var bars = gbm.Fetch(100, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1));
|
||||
|
||||
foreach (var bar in bars)
|
||||
{
|
||||
filter.Update(new TValue(bar.Time, bar.Close));
|
||||
}
|
||||
|
||||
Assert.True(double.IsFinite(filter.Last.Value));
|
||||
}
|
||||
|
||||
// ── NaN handling ────────────────────────────────────────────────────
|
||||
|
||||
[Fact]
|
||||
public void Update_HandlesNaNSafely()
|
||||
{
|
||||
@@ -51,6 +118,18 @@ public class Cheby2Tests
|
||||
Assert.True(double.IsFinite(result.Value));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Update_HandlesInfinitySafely()
|
||||
{
|
||||
var filter = new Cheby2(10, 5.0);
|
||||
filter.Update(new TValue(DateTime.UtcNow, 100.0));
|
||||
|
||||
var result = filter.Update(new TValue(DateTime.UtcNow, double.PositiveInfinity));
|
||||
Assert.True(double.IsFinite(result.Value));
|
||||
}
|
||||
|
||||
// ── Bar Correction ──────────────────────────────────────────────────
|
||||
|
||||
[Fact]
|
||||
public void IterativeCorrections_RestoreState()
|
||||
{
|
||||
@@ -83,6 +162,42 @@ public class Cheby2Tests
|
||||
Assert.Equal(valAfterNew, valRestored, 1e-10);
|
||||
}
|
||||
|
||||
// ── Reset ───────────────────────────────────────────────────────────
|
||||
|
||||
[Fact]
|
||||
public void Reset_ClearsState()
|
||||
{
|
||||
var filter = new Cheby2(10, 5.0);
|
||||
for (int i = 0; i < 10; i++)
|
||||
{
|
||||
filter.Update(new TValue(DateTime.UtcNow, 100 + i));
|
||||
}
|
||||
Assert.True(filter.IsHot);
|
||||
|
||||
filter.Reset();
|
||||
Assert.False(filter.IsHot);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Reset_AllowsReuse()
|
||||
{
|
||||
var filter = new Cheby2(10, 5.0);
|
||||
for (int i = 0; i < 20; i++)
|
||||
{
|
||||
filter.Update(new TValue(DateTime.UtcNow, 100 + i));
|
||||
}
|
||||
var firstResult = filter.Last.Value;
|
||||
|
||||
filter.Reset();
|
||||
for (int i = 0; i < 20; i++)
|
||||
{
|
||||
filter.Update(new TValue(DateTime.UtcNow, 100 + i));
|
||||
}
|
||||
Assert.Equal(firstResult, filter.Last.Value, 1e-10);
|
||||
}
|
||||
|
||||
// ── Batch ───────────────────────────────────────────────────────────
|
||||
|
||||
[Fact]
|
||||
public void SpanBatch_MatchesIterative()
|
||||
{
|
||||
@@ -109,4 +224,125 @@ public class Cheby2Tests
|
||||
Assert.Equal(iterativeResults[i], spanResults[i], 1e-10);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Batch_TSeries_Static()
|
||||
{
|
||||
var data = _gbm.Fetch(50, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1));
|
||||
var result = Cheby2.Batch(data.Close, 10, 5.0);
|
||||
Assert.Equal(50, result.Count);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Update_EmptyTSeries_ReturnsEmpty()
|
||||
{
|
||||
var filter = new Cheby2(10, 5.0);
|
||||
var result = filter.Update(new TSeries());
|
||||
Assert.Empty(result);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Update_TSeries_ReturnsCorrectCount()
|
||||
{
|
||||
var filter = new Cheby2(10, 5.0);
|
||||
var data = _gbm.Fetch(50, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1));
|
||||
var result = filter.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) = Cheby2.Calculate(data.Close, 10, 5.0);
|
||||
Assert.Equal(50, results.Count);
|
||||
Assert.True(indicator.IsHot);
|
||||
}
|
||||
|
||||
// ── Prime ───────────────────────────────────────────────────────────
|
||||
|
||||
[Fact]
|
||||
public void Prime_WarmsUpIndicator()
|
||||
{
|
||||
var filter = new Cheby2(10, 5.0);
|
||||
var values = new double[20];
|
||||
for (int i = 0; i < 20; i++)
|
||||
{
|
||||
values[i] = 100 + i;
|
||||
}
|
||||
|
||||
filter.Prime(values);
|
||||
Assert.True(filter.IsHot);
|
||||
}
|
||||
|
||||
// ── Dispose ─────────────────────────────────────────────────────────
|
||||
|
||||
[Fact]
|
||||
public void Dispose_WithPublisher_Unsubscribes()
|
||||
{
|
||||
var source = new TSeries();
|
||||
var filter = new Cheby2(source, 10, 5.0);
|
||||
source.Add(new TValue(DateTime.UtcNow, 100));
|
||||
|
||||
filter.Dispose();
|
||||
var lastBefore = filter.Last;
|
||||
source.Add(new TValue(DateTime.UtcNow, 200));
|
||||
Assert.Equal(lastBefore, filter.Last);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Dispose_WithoutPublisher_DoesNotThrow()
|
||||
{
|
||||
var filter = new Cheby2(10, 5.0);
|
||||
filter.Update(new TValue(DateTime.UtcNow, 100));
|
||||
filter.Dispose();
|
||||
Assert.True(true); // S2699: explicit assertion for dispose-only test
|
||||
}
|
||||
|
||||
// ── Determinism ─────────────────────────────────────────────────────
|
||||
|
||||
[Fact]
|
||||
public void TwoInstances_SameInput_SameOutput()
|
||||
{
|
||||
var f1 = new Cheby2(10, 5.0);
|
||||
var f2 = new Cheby2(10, 5.0);
|
||||
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 = f1.Update(tv);
|
||||
var r2 = f2.Update(tv);
|
||||
Assert.Equal(r1.Value, r2.Value);
|
||||
}
|
||||
}
|
||||
|
||||
// ── Filter behavior ─────────────────────────────────────────────────
|
||||
|
||||
[Fact]
|
||||
public void HighAttenuation_MoreSmoothing()
|
||||
{
|
||||
var lowAtten = new Cheby2(10, 2.0);
|
||||
var highAtten = new Cheby2(10, 20.0);
|
||||
|
||||
var gbm = new GBM(seed: 42);
|
||||
var bars = gbm.Fetch(100, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1));
|
||||
|
||||
double sumDiffLow = 0, sumDiffHigh = 0;
|
||||
foreach (var bar in bars)
|
||||
{
|
||||
var tv = new TValue(bar.Time, bar.Close);
|
||||
var rLow = lowAtten.Update(tv);
|
||||
var rHigh = highAtten.Update(tv);
|
||||
sumDiffLow += Math.Abs(bar.Close - rLow.Value);
|
||||
sumDiffHigh += Math.Abs(bar.Close - rHigh.Value);
|
||||
}
|
||||
|
||||
// Both should produce finite outputs
|
||||
Assert.True(double.IsFinite(lowAtten.Last.Value));
|
||||
Assert.True(double.IsFinite(highAtten.Last.Value));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,6 +14,8 @@ public class NotchTests
|
||||
_gbm = new GBM(startPrice: 100, mu: 0.05, sigma: 0.2, seed: 123);
|
||||
}
|
||||
|
||||
// ── Constructor ──────────────────────────────────────────────────────
|
||||
|
||||
[Fact]
|
||||
public void Constructor_ValidatesInput()
|
||||
{
|
||||
@@ -21,6 +23,74 @@ public class NotchTests
|
||||
Assert.Throws<ArgumentOutOfRangeException>(() => new Notch(10, -0.5));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Constructor_Period1_Throws()
|
||||
{
|
||||
Assert.Throws<ArgumentOutOfRangeException>(() => new Notch(1));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Constructor_ZeroQ_Throws()
|
||||
{
|
||||
Assert.Throws<ArgumentOutOfRangeException>(() => new Notch(10, 0));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Constructor_SetsProperties()
|
||||
{
|
||||
var notch = new Notch(10, 2.0);
|
||||
Assert.Equal(10, notch.NotchFreq);
|
||||
Assert.Equal(2.0, notch.Bandwidth);
|
||||
Assert.Equal("Notch(10,2)", notch.Name);
|
||||
Assert.Equal(10, notch.WarmupPeriod);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Constructor_DefaultQ()
|
||||
{
|
||||
var notch = new Notch(10);
|
||||
Assert.Equal(1.0, notch.Bandwidth);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Constructor_WithPublisher_Subscribes()
|
||||
{
|
||||
var source = new TSeries();
|
||||
var notch = new Notch(source, 10, 1.0);
|
||||
|
||||
source.Add(new TValue(DateTime.UtcNow, 100));
|
||||
Assert.True(double.IsFinite(notch.Last.Value));
|
||||
}
|
||||
|
||||
// ── IsHot ───────────────────────────────────────────────────────────
|
||||
|
||||
[Fact]
|
||||
public void IsHot_BecomesTrueAfterWarmup()
|
||||
{
|
||||
var notch = new Notch(10, 1.0);
|
||||
Assert.False(notch.IsHot);
|
||||
|
||||
for (int i = 0; i < 10; i++)
|
||||
{
|
||||
notch.Update(new TValue(DateTime.UtcNow, 100));
|
||||
}
|
||||
|
||||
Assert.True(notch.IsHot);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void IsHot_FalseBeforeWarmup()
|
||||
{
|
||||
var notch = new Notch(10, 1.0);
|
||||
for (int i = 0; i < 9; i++)
|
||||
{
|
||||
notch.Update(new TValue(DateTime.UtcNow, 100));
|
||||
}
|
||||
Assert.False(notch.IsHot);
|
||||
}
|
||||
|
||||
// ── Update ──────────────────────────────────────────────────────────
|
||||
|
||||
[Fact]
|
||||
public void Calc_ReturnsValue()
|
||||
{
|
||||
@@ -29,6 +99,159 @@ public class NotchTests
|
||||
Assert.True(double.IsFinite(result.Value));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Update_BarCorrection_Works()
|
||||
{
|
||||
var notch = new Notch(10, 1.0);
|
||||
var now = DateTime.UtcNow;
|
||||
|
||||
for (int i = 0; i < 15; i++)
|
||||
{
|
||||
notch.Update(new TValue(now.AddMinutes(i), 100 + i));
|
||||
}
|
||||
|
||||
// New bar
|
||||
var result1 = notch.Update(new TValue(now.AddMinutes(15), 200), isNew: true);
|
||||
|
||||
// Correction
|
||||
notch.Update(new TValue(now.AddMinutes(15), 150), isNew: false);
|
||||
|
||||
// Restore to original value
|
||||
notch.Update(new TValue(now.AddMinutes(15), 200), isNew: false);
|
||||
var restored = notch.Last;
|
||||
|
||||
Assert.Equal(result1.Value, restored.Value, 1e-10);
|
||||
}
|
||||
|
||||
// ── NaN handling ────────────────────────────────────────────────────
|
||||
|
||||
[Fact]
|
||||
public void Update_NaN_UsesLastValid()
|
||||
{
|
||||
var notch = new Notch(10, 1.0);
|
||||
notch.Update(new TValue(DateTime.UtcNow, 100));
|
||||
|
||||
var result = notch.Update(new TValue(DateTime.UtcNow, double.NaN));
|
||||
Assert.True(double.IsFinite(result.Value));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Update_Infinity_UsesLastValid()
|
||||
{
|
||||
var notch = new Notch(10, 1.0);
|
||||
notch.Update(new TValue(DateTime.UtcNow, 100));
|
||||
|
||||
var result = notch.Update(new TValue(DateTime.UtcNow, double.PositiveInfinity));
|
||||
Assert.True(double.IsFinite(result.Value));
|
||||
}
|
||||
|
||||
// ── Reset ───────────────────────────────────────────────────────────
|
||||
|
||||
[Fact]
|
||||
public void Reset_ClearsState()
|
||||
{
|
||||
var notch = new Notch(10, 1.0);
|
||||
for (int i = 0; i < 15; i++)
|
||||
{
|
||||
notch.Update(new TValue(DateTime.UtcNow, 100 + i));
|
||||
}
|
||||
Assert.True(notch.IsHot);
|
||||
|
||||
notch.Reset();
|
||||
Assert.False(notch.IsHot);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Reset_AllowsReuse()
|
||||
{
|
||||
var notch = new Notch(10, 1.0);
|
||||
for (int i = 0; i < 20; i++)
|
||||
{
|
||||
notch.Update(new TValue(DateTime.UtcNow, 100 + i));
|
||||
}
|
||||
var firstResult = notch.Last.Value;
|
||||
|
||||
notch.Reset();
|
||||
for (int i = 0; i < 20; i++)
|
||||
{
|
||||
notch.Update(new TValue(DateTime.UtcNow, 100 + i));
|
||||
}
|
||||
Assert.Equal(firstResult, notch.Last.Value, 1e-10);
|
||||
}
|
||||
|
||||
// ── Filter behavior ─────────────────────────────────────────────────
|
||||
|
||||
[Fact]
|
||||
public void Notch_Passes_DC()
|
||||
{
|
||||
// DC input (constant value) should pass through with Gain 1
|
||||
var notch = new Notch(period: 10, q: 1.0);
|
||||
double input = 100.0;
|
||||
double output = 0;
|
||||
|
||||
// Warmup to stabilize (IIR transient)
|
||||
for (int i = 0; i < 100; i++)
|
||||
{
|
||||
output = notch.Update(new TValue(DateTime.UtcNow, input)).Value;
|
||||
}
|
||||
|
||||
Assert.Equal(input, output, precision: 6);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Notch_Attenuates_CenterFrequency()
|
||||
{
|
||||
// Period 10 means frequency is 1/10 cycles per sample.
|
||||
int period = 10;
|
||||
double q = 5.0; // High Q for sharp notch
|
||||
var notch = new Notch(period, q);
|
||||
|
||||
double omega = 2.0 * Math.PI / period;
|
||||
|
||||
double maxAmp = 0;
|
||||
for (int i = 0; i < 200; i++)
|
||||
{
|
||||
double val = Math.Sin(omega * i); // Input amplitude 1
|
||||
double outVal = notch.Update(new TValue(DateTime.UtcNow, val)).Value;
|
||||
|
||||
if (i > 50) // ignore transient
|
||||
{
|
||||
maxAmp = Math.Max(maxAmp, Math.Abs(outVal));
|
||||
}
|
||||
}
|
||||
|
||||
// At exact notch frequency, ideal is 0.
|
||||
Assert.True(maxAmp < 0.1, $"Amplitude {maxAmp} should be attenuated ( < 0.1 )");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Notch_PassesNonNotchFrequency()
|
||||
{
|
||||
// Non-notch frequency should pass through with ~unity gain
|
||||
int notchPeriod = 10;
|
||||
var notch = new Notch(notchPeriod, 1.0);
|
||||
|
||||
// Use a frequency far from the notch (period 50 instead of 10)
|
||||
double omega = 2.0 * Math.PI / 50.0;
|
||||
double maxAmp = 0;
|
||||
|
||||
for (int i = 0; i < 300; i++)
|
||||
{
|
||||
double val = Math.Sin(omega * i);
|
||||
double outVal = notch.Update(new TValue(DateTime.UtcNow, val)).Value;
|
||||
|
||||
if (i > 100) // ignore transient
|
||||
{
|
||||
maxAmp = Math.Max(maxAmp, Math.Abs(outVal));
|
||||
}
|
||||
}
|
||||
|
||||
// At non-notch frequency, output should be close to input amplitude (1.0)
|
||||
Assert.True(maxAmp > 0.7, $"Non-notch amplitude {maxAmp} should be high (> 0.7)");
|
||||
}
|
||||
|
||||
// ── Batch ───────────────────────────────────────────────────────────
|
||||
|
||||
[Fact]
|
||||
public void AllModes_ProduceSameResult()
|
||||
{
|
||||
@@ -66,47 +289,110 @@ public class NotchTests
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Notch_Passes_DC()
|
||||
public void Batch_TSeries_Static()
|
||||
{
|
||||
// DC input (constant value) should pass through with Gain 1
|
||||
var notch = new Notch(period: 10, q: 1.0);
|
||||
double input = 100.0;
|
||||
double output = 0;
|
||||
|
||||
// Warmup to stabilize (IIR transient)
|
||||
for (int i = 0; i < 100; i++)
|
||||
{
|
||||
output = notch.Update(new TValue(DateTime.UtcNow, input)).Value;
|
||||
}
|
||||
|
||||
Assert.Equal(input, output, precision: 6);
|
||||
var data = _gbm.Fetch(50, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1));
|
||||
var result = Notch.Batch(data.Close, 10, 1.0);
|
||||
Assert.Equal(50, result.Count);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Notch_Attenuates_CenterFrequency()
|
||||
public void Update_EmptyTSeries_ReturnsEmpty()
|
||||
{
|
||||
// Period 10 means frequency is 1/10 cycles per sample.
|
||||
// theta = 2*pi/10
|
||||
int period = 10;
|
||||
double q = 5.0; // High Q for sharp notch
|
||||
var notch = new Notch(period, q);
|
||||
var notch = new Notch(10, 1.0);
|
||||
var result = notch.Update(new TSeries());
|
||||
Assert.Empty(result);
|
||||
}
|
||||
|
||||
double omega = 2.0 * Math.PI / period;
|
||||
// ── Calculate ───────────────────────────────────────────────────────
|
||||
|
||||
double maxAmp = 0;
|
||||
for (int i = 0; i < 200; i++)
|
||||
[Fact]
|
||||
public void Calculate_ReturnsResultsAndIndicator()
|
||||
{
|
||||
var data = _gbm.Fetch(50, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1));
|
||||
var (results, indicator) = Notch.Calculate(data.Close, 10, 1.0);
|
||||
Assert.Equal(50, results.Count);
|
||||
Assert.True(indicator.IsHot);
|
||||
}
|
||||
|
||||
// ── Prime ───────────────────────────────────────────────────────────
|
||||
|
||||
[Fact]
|
||||
public void Prime_WarmsUpIndicator()
|
||||
{
|
||||
var notch = new Notch(10, 1.0);
|
||||
var values = new double[15];
|
||||
for (int i = 0; i < 15; i++)
|
||||
{
|
||||
double val = Math.Sin(omega * i); // Input amplitude 1
|
||||
double outVal = notch.Update(new TValue(DateTime.UtcNow, val)).Value;
|
||||
|
||||
if (i > 50) // ignore transient
|
||||
{
|
||||
maxAmp = Math.Max(maxAmp, Math.Abs(outVal));
|
||||
}
|
||||
values[i] = 100 + i;
|
||||
}
|
||||
|
||||
// At exact notch frequency, ideal is 0.
|
||||
// With Q=5, it should be very small.
|
||||
Assert.True(maxAmp < 0.1, $"Amplitude {maxAmp} should be attenuated ( < 0.1 )");
|
||||
notch.Prime(values);
|
||||
Assert.True(notch.IsHot);
|
||||
}
|
||||
|
||||
// ── Dispose ─────────────────────────────────────────────────────────
|
||||
|
||||
[Fact]
|
||||
public void Dispose_WithPublisher_Unsubscribes()
|
||||
{
|
||||
var source = new TSeries();
|
||||
var notch = new Notch(source, 10, 1.0);
|
||||
source.Add(new TValue(DateTime.UtcNow, 100));
|
||||
|
||||
notch.Dispose();
|
||||
var lastBefore = notch.Last;
|
||||
source.Add(new TValue(DateTime.UtcNow, 200));
|
||||
Assert.Equal(lastBefore, notch.Last);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Dispose_WithoutPublisher_DoesNotThrow()
|
||||
{
|
||||
var notch = new Notch(10, 1.0);
|
||||
notch.Update(new TValue(DateTime.UtcNow, 100));
|
||||
notch.Dispose();
|
||||
Assert.True(true); // S2699: explicit assertion for dispose-only test
|
||||
}
|
||||
|
||||
// ── Determinism ─────────────────────────────────────────────────────
|
||||
|
||||
[Fact]
|
||||
public void TwoInstances_SameInput_SameOutput()
|
||||
{
|
||||
var n1 = new Notch(10, 1.0);
|
||||
var n2 = new Notch(10, 1.0);
|
||||
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 = n1.Update(tv);
|
||||
var r2 = n2.Update(tv);
|
||||
Assert.Equal(r1.Value, r2.Value);
|
||||
}
|
||||
}
|
||||
|
||||
// ── Q Factor ────────────────────────────────────────────────────────
|
||||
|
||||
[Fact]
|
||||
public void DifferentQ_ProduceDifferentOutputs()
|
||||
{
|
||||
var narrowNotch = new Notch(10, 0.5);
|
||||
var wideNotch = new Notch(10, 5.0);
|
||||
|
||||
var gbm = new GBM(seed: 42);
|
||||
var bars = gbm.Fetch(50, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1));
|
||||
|
||||
double lastNarrow = 0, lastWide = 0;
|
||||
foreach (var bar in bars)
|
||||
{
|
||||
var tv = new TValue(bar.Time, bar.Close);
|
||||
lastNarrow = narrowNotch.Update(tv).Value;
|
||||
lastWide = wideNotch.Update(tv).Value;
|
||||
}
|
||||
|
||||
Assert.NotEqual(lastNarrow, lastWide, 1e-6);
|
||||
}
|
||||
}
|
||||
|
||||
+270
-15
@@ -11,6 +11,8 @@ public class SgfTests
|
||||
_gbm = new GBM(startPrice: 100, mu: 0.05, sigma: 0.2, seed: 123);
|
||||
}
|
||||
|
||||
// ── Constructor ──────────────────────────────────────────────────────
|
||||
|
||||
[Fact]
|
||||
public void Constructor_ValidatesInput()
|
||||
{
|
||||
@@ -22,6 +24,203 @@ public class SgfTests
|
||||
Assert.Equal("Sgf(9,2)", sgf.Name); // Period adjusted to odd
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Constructor_EvenPeriod_AdjustedToOdd()
|
||||
{
|
||||
var sgf = new Sgf(10, 2);
|
||||
Assert.Equal("Sgf(9,2)", sgf.Name);
|
||||
Assert.Equal(9, sgf.WarmupPeriod);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Constructor_OddPeriod_Unchanged()
|
||||
{
|
||||
var sgf = new Sgf(11, 2);
|
||||
Assert.Equal("Sgf(11,2)", sgf.Name);
|
||||
Assert.Equal(11, sgf.WarmupPeriod);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Constructor_Period1_Works()
|
||||
{
|
||||
// Period 1 should work (minimum after adjustment)
|
||||
var sgf = new Sgf(1, 0);
|
||||
Assert.NotNull(sgf);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Constructor_PolyOrder4_Works()
|
||||
{
|
||||
var sgf = new Sgf(11, 4);
|
||||
Assert.Equal("Sgf(11,4)", sgf.Name);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Constructor_DefaultPolyOrder_Is2()
|
||||
{
|
||||
var sgf = new Sgf(11);
|
||||
Assert.Equal("Sgf(11,2)", sgf.Name);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Constructor_WithPublisher_Subscribes()
|
||||
{
|
||||
var source = new TSeries();
|
||||
var sgf = new Sgf(source, 11, 2);
|
||||
|
||||
source.Add(new TValue(DateTime.UtcNow, 100));
|
||||
Assert.True(double.IsFinite(sgf.Last.Value));
|
||||
}
|
||||
|
||||
// ── IsHot / WarmupPeriod ────────────────────────────────────────────
|
||||
|
||||
[Fact]
|
||||
public void WarmupPeriod_IsCorrect()
|
||||
{
|
||||
var sgf = new Sgf(21, 2);
|
||||
Assert.Equal(21, sgf.WarmupPeriod);
|
||||
Assert.False(sgf.IsHot);
|
||||
|
||||
for (int i = 0; i < 21; i++)
|
||||
{
|
||||
sgf.Update(new TValue(DateTime.UtcNow, 100));
|
||||
}
|
||||
|
||||
Assert.True(sgf.IsHot);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void IsHot_FalseBeforeFull()
|
||||
{
|
||||
var sgf = new Sgf(11, 2);
|
||||
for (int i = 0; i < 10; i++)
|
||||
{
|
||||
sgf.Update(new TValue(DateTime.UtcNow, 100));
|
||||
}
|
||||
Assert.False(sgf.IsHot);
|
||||
}
|
||||
|
||||
// ── Update ──────────────────────────────────────────────────────────
|
||||
|
||||
[Fact]
|
||||
public void Update_ConstantInput_ReturnsConstant()
|
||||
{
|
||||
var sgf = new Sgf(5, 2);
|
||||
for (int i = 0; i < 10; i++)
|
||||
{
|
||||
sgf.Update(new TValue(DateTime.UtcNow, 42.0));
|
||||
}
|
||||
// A constant signal filtered should still be constant
|
||||
Assert.Equal(42.0, sgf.Last.Value, 1e-9);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Update_SmoothsNoisyInput()
|
||||
{
|
||||
var sgf = new Sgf(21, 2);
|
||||
var gbm = new GBM(seed: 42);
|
||||
var bars = gbm.Fetch(100, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1));
|
||||
|
||||
foreach (var bar in bars)
|
||||
{
|
||||
sgf.Update(new TValue(bar.Time, bar.Close));
|
||||
}
|
||||
|
||||
// Filtered output should be finite and not exactly equal to raw
|
||||
Assert.True(double.IsFinite(sgf.Last.Value));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Update_BarCorrection_Works()
|
||||
{
|
||||
var sgf = new Sgf(5, 2);
|
||||
var now = DateTime.UtcNow;
|
||||
|
||||
for (int i = 0; i < 10; i++)
|
||||
{
|
||||
sgf.Update(new TValue(now.AddMinutes(i), 100 + i));
|
||||
}
|
||||
|
||||
// New bar
|
||||
var result1 = sgf.Update(new TValue(now.AddMinutes(10), 200), isNew: true);
|
||||
|
||||
// Correction to same bar
|
||||
var result2 = sgf.Update(new TValue(now.AddMinutes(10), 150), isNew: false);
|
||||
|
||||
// Different corrections should yield different results
|
||||
Assert.NotEqual(result1.Value, result2.Value);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Update_PartialWindow_Works()
|
||||
{
|
||||
var sgf = new Sgf(11, 2);
|
||||
// First value before buffer is full should still produce a finite result
|
||||
var result = sgf.Update(new TValue(DateTime.UtcNow, 100));
|
||||
Assert.True(double.IsFinite(result.Value));
|
||||
}
|
||||
|
||||
// ── NaN handling ────────────────────────────────────────────────────
|
||||
|
||||
[Fact]
|
||||
public void HandlesNaN()
|
||||
{
|
||||
var sgf = new Sgf(5, 2);
|
||||
|
||||
sgf.Update(new TValue(DateTime.UtcNow, 100));
|
||||
sgf.Update(new TValue(DateTime.UtcNow, double.NaN));
|
||||
sgf.Update(new TValue(DateTime.UtcNow, 102));
|
||||
|
||||
// Should produce valid result if sufficient valid data exists within window
|
||||
Assert.True(double.IsFinite(sgf.Last.Value));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void AllNaN_InWindow_ReturnsInput()
|
||||
{
|
||||
var sgf = new Sgf(3, 1);
|
||||
sgf.Update(new TValue(DateTime.UtcNow, double.NaN));
|
||||
// With only NaN in buffer and partial window, should fallback to input
|
||||
// (wSum <= epsilon path)
|
||||
Assert.True(double.IsNaN(sgf.Last.Value) || double.IsFinite(sgf.Last.Value));
|
||||
}
|
||||
|
||||
// ── Reset ───────────────────────────────────────────────────────────
|
||||
|
||||
[Fact]
|
||||
public void Reset_ClearsState()
|
||||
{
|
||||
var sgf = new Sgf(11, 2);
|
||||
for (int i = 0; i < 20; i++)
|
||||
{
|
||||
sgf.Update(new TValue(DateTime.UtcNow, 100 + i));
|
||||
}
|
||||
Assert.True(sgf.IsHot);
|
||||
|
||||
sgf.Reset();
|
||||
Assert.False(sgf.IsHot);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Reset_AllowsReuse()
|
||||
{
|
||||
var sgf = new Sgf(5, 2);
|
||||
for (int i = 0; i < 10; i++)
|
||||
{
|
||||
sgf.Update(new TValue(DateTime.UtcNow, 100 + i));
|
||||
}
|
||||
var firstLast = sgf.Last.Value;
|
||||
|
||||
sgf.Reset();
|
||||
for (int i = 0; i < 10; i++)
|
||||
{
|
||||
sgf.Update(new TValue(DateTime.UtcNow, 100 + i));
|
||||
}
|
||||
Assert.Equal(firstLast, sgf.Last.Value, 1e-10);
|
||||
}
|
||||
|
||||
// ── Batch ───────────────────────────────────────────────────────────
|
||||
|
||||
[Fact]
|
||||
public void AllModes_ProduceSameResult()
|
||||
{
|
||||
@@ -67,30 +266,86 @@ public class SgfTests
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void HandlesNaN()
|
||||
public void Batch_TSeries_Static()
|
||||
{
|
||||
var sgf = new Sgf(5, 2);
|
||||
|
||||
sgf.Update(new TValue(DateTime.UtcNow, 100));
|
||||
sgf.Update(new TValue(DateTime.UtcNow, double.NaN));
|
||||
sgf.Update(new TValue(DateTime.UtcNow, 102));
|
||||
|
||||
// Should produce valid result if sufficient valid data exists within window
|
||||
Assert.True(double.IsFinite(sgf.Last.Value));
|
||||
var data = _gbm.Fetch(50, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1));
|
||||
var result = Sgf.Batch(data.Close, 11, 2);
|
||||
Assert.Equal(50, result.Count);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void WarmupPeriod_IsCorrect()
|
||||
public void Batch_Span_MismatchedLength_Throws()
|
||||
{
|
||||
var sgf = new Sgf(21, 2);
|
||||
Assert.Equal(21, sgf.WarmupPeriod);
|
||||
Assert.False(sgf.IsHot);
|
||||
var source = new double[10];
|
||||
var output = new double[5];
|
||||
Assert.Throws<ArgumentException>(() => Sgf.Batch(source, output, 5, 2));
|
||||
}
|
||||
|
||||
for (int i = 0; i < 21; i++)
|
||||
[Fact]
|
||||
public void Batch_Span_PolyOrderTooLarge_Throws()
|
||||
{
|
||||
var source = new double[10];
|
||||
var output = new double[10];
|
||||
Assert.Throws<ArgumentException>(() => Sgf.Batch(source, output, 5, 5));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Update_EmptyTSeries_ReturnsEmpty()
|
||||
{
|
||||
var sgf = new Sgf(11, 2);
|
||||
var result = sgf.Update(new TSeries());
|
||||
Assert.Empty(result);
|
||||
}
|
||||
|
||||
// ── Calculate ───────────────────────────────────────────────────────
|
||||
|
||||
[Fact]
|
||||
public void Calculate_ReturnsResultsAndIndicator()
|
||||
{
|
||||
var data = _gbm.Fetch(50, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1));
|
||||
var (results, indicator) = Sgf.Calculate(data.Close, 11, 2);
|
||||
Assert.Equal(50, results.Count);
|
||||
Assert.True(indicator.IsHot);
|
||||
}
|
||||
|
||||
// ── Prime ───────────────────────────────────────────────────────────
|
||||
|
||||
[Fact]
|
||||
public void Prime_WarmsUpIndicator()
|
||||
{
|
||||
var sgf = new Sgf(11, 2);
|
||||
var values = new double[20];
|
||||
for (int i = 0; i < 20; i++)
|
||||
{
|
||||
sgf.Update(new TValue(DateTime.UtcNow, 100));
|
||||
values[i] = 100 + i;
|
||||
}
|
||||
|
||||
sgf.Prime(values);
|
||||
Assert.True(sgf.IsHot);
|
||||
}
|
||||
|
||||
// ── Dispose ─────────────────────────────────────────────────────────
|
||||
|
||||
[Fact]
|
||||
public void Dispose_WithPublisher_Unsubscribes()
|
||||
{
|
||||
var source = new TSeries();
|
||||
var sgf = new Sgf(source, 5, 2);
|
||||
source.Add(new TValue(DateTime.UtcNow, 100));
|
||||
|
||||
sgf.Dispose();
|
||||
|
||||
var lastBefore = sgf.Last;
|
||||
source.Add(new TValue(DateTime.UtcNow, 200));
|
||||
Assert.Equal(lastBefore, sgf.Last);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Dispose_WithoutPublisher_DoesNotThrow()
|
||||
{
|
||||
var sgf = new Sgf(5, 2);
|
||||
sgf.Update(new TValue(DateTime.UtcNow, 100));
|
||||
sgf.Dispose();
|
||||
Assert.True(true); // S2699: explicit assertion for dispose-only test
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user