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
This commit is contained in:
Miha Kralj
2026-03-12 12:34:16 -07:00
parent 8937b0c0fa
commit 060649192f
1149 changed files with 1780 additions and 3316 deletions
@@ -0,0 +1,111 @@
using TradingPlatform.BusinessLayer;
using QuanTAlib;
namespace QuanTAlib.Tests;
public sealed class SmiIndicatorTests
{
[Fact]
public void SmiIndicator_Constructor_SetsDefaults()
{
var indicator = new SmiIndicator();
Assert.Equal(10, indicator.KPeriod);
Assert.Equal(3, indicator.KSmooth);
Assert.Equal(3, indicator.DSmooth);
Assert.True(indicator.Blau);
Assert.True(indicator.ShowColdValues);
Assert.Equal("SMI", indicator.Name);
Assert.True(indicator.SeparateWindow);
Assert.True(indicator.OnBackGround);
}
[Fact]
public void SmiIndicator_MinHistoryDepths_EqualsZero()
{
var indicator = new SmiIndicator { KPeriod = 14, KSmooth = 5, DSmooth = 5 };
Assert.Equal(0, SmiIndicator.MinHistoryDepths);
IWatchlistIndicator watchlistIndicator = indicator;
Assert.Equal(0, watchlistIndicator.MinHistoryDepths);
}
[Fact]
public void SmiIndicator_ShortName_IncludesParameters()
{
var indicator = new SmiIndicator { KPeriod = 14, KSmooth = 5, DSmooth = 5 };
indicator.Initialize();
Assert.Contains("SMI", indicator.ShortName, StringComparison.Ordinal);
Assert.Contains("14", indicator.ShortName, StringComparison.Ordinal);
Assert.Contains("5", indicator.ShortName, StringComparison.Ordinal);
}
[Fact]
public void SmiIndicator_SourceCodeLink_IsValid()
{
var indicator = new SmiIndicator();
Assert.Contains("github.com", indicator.SourceCodeLink, StringComparison.Ordinal);
Assert.Contains("Smi.Quantower.cs", indicator.SourceCodeLink, StringComparison.Ordinal);
}
[Fact]
public void SmiIndicator_Initialize_CreatesInternalSmi()
{
var indicator = new SmiIndicator { KPeriod = 10, KSmooth = 3, DSmooth = 3 };
indicator.Initialize();
// After init, line series should exist (K, D)
Assert.Equal(2, indicator.LinesSeries.Count);
}
[Fact]
public void SmiIndicator_ProcessUpdate_HistoricalBar_ComputesValue()
{
var indicator = new SmiIndicator { KPeriod = 5, KSmooth = 3, DSmooth = 3 };
indicator.Initialize();
var now = DateTime.UtcNow;
for (int i = 0; i < 20; i++)
{
indicator.HistoricalData.AddBar(now.AddMinutes(i), 100 + i, 110 + i, 90 + i, 105 + i);
var args = new UpdateArgs(UpdateReason.HistoricalBar);
indicator.ProcessUpdate(args);
}
double k = indicator.LinesSeries[0].GetValue(0);
double d = indicator.LinesSeries[1].GetValue(0);
Assert.True(double.IsFinite(k));
Assert.True(double.IsFinite(d));
}
[Fact]
public void SmiIndicator_ProcessUpdate_NewBar_ComputesValue()
{
var indicator = new SmiIndicator { KPeriod = 5, KSmooth = 3, DSmooth = 3 };
indicator.Initialize();
var now = DateTime.UtcNow;
for (int i = 0; i < 10; i++)
{
indicator.HistoricalData.AddBar(now.AddMinutes(i), 100 + i, 110 + i, 90 + i, 105 + i);
var args = new UpdateArgs(UpdateReason.HistoricalBar);
indicator.ProcessUpdate(args);
}
// Simulate a new bar
indicator.HistoricalData.AddBar(now.AddMinutes(10), 110, 120, 100, 115);
var newArgs = new UpdateArgs(UpdateReason.NewBar);
indicator.ProcessUpdate(newArgs);
double k = indicator.LinesSeries[0].GetValue(0);
double d = indicator.LinesSeries[1].GetValue(0);
Assert.True(double.IsFinite(k));
Assert.True(double.IsFinite(d));
}
}
+512
View File
@@ -0,0 +1,512 @@
using Xunit;
namespace QuanTAlib.Tests;
public sealed class SmiTests
{
private const double Tolerance = 1e-10;
// --- A) Constructor validation ---
[Fact]
public void Constructor_ZeroKPeriod_Throws()
{
var ex = Assert.Throws<ArgumentException>(() => new Smi(kPeriod: 0));
Assert.Equal("kPeriod", ex.ParamName);
}
[Fact]
public void Constructor_ZeroKSmooth_Throws()
{
var ex = Assert.Throws<ArgumentException>(() => new Smi(kSmooth: 0));
Assert.Equal("kSmooth", ex.ParamName);
}
[Fact]
public void Constructor_ZeroDSmooth_Throws()
{
var ex = Assert.Throws<ArgumentException>(() => new Smi(dSmooth: 0));
Assert.Equal("dSmooth", ex.ParamName);
}
[Fact]
public void Constructor_NegativeKPeriod_Throws()
{
var ex = Assert.Throws<ArgumentException>(() => new Smi(kPeriod: -1));
Assert.Equal("kPeriod", ex.ParamName);
}
[Fact]
public void Constructor_Valid_SetsNameAndWarmup()
{
var smi = new Smi(10, 3, 3);
Assert.Equal("Smi(10,3,3)", smi.Name);
Assert.Equal(10 + 3 + 3, smi.WarmupPeriod);
Assert.False(smi.IsHot);
}
// --- B) Basic calculation ---
[Fact]
public void Update_ConstantBars_KIsZero()
{
var smi = new Smi(5, 3, 3);
for (int i = 0; i < 50; i++)
{
long t = DateTime.UtcNow.Ticks + i;
smi.Update(new TBar(t, 100, 100, 100, 100, 1000));
}
Assert.Equal(0.0, smi.K.Value, 1e-6);
Assert.Equal(0.0, smi.D.Value, 1e-6);
}
[Fact]
public void Update_RisingClose_PositiveK()
{
var smi = new Smi(5, 3, 3);
for (int i = 0; i < 30; i++)
{
long t = DateTime.UtcNow.Ticks + i;
double c = 100.0 + i;
smi.Update(new TBar(t, c, c + 5, c - 5, c, 1000));
}
Assert.True(smi.K.Value > 0.0, "Rising close should produce positive K");
}
[Fact]
public void Update_FallingClose_NegativeK()
{
var smi = new Smi(5, 3, 3);
for (int i = 0; i < 30; i++)
{
long t = DateTime.UtcNow.Ticks + i;
double c = 200.0 - i;
smi.Update(new TBar(t, c, c + 5, c - 5, c, 1000));
}
Assert.True(smi.K.Value < 0.0, "Falling close should produce negative K");
}
[Fact]
public void Update_Last_IsAccessible()
{
var smi = new Smi(5, 3, 3);
for (int i = 0; i < 20; i++)
{
long t = DateTime.UtcNow.Ticks + i;
smi.Update(new TBar(t, 100 + i, 110 + i, 90 + i, 105 + i, 1000));
}
Assert.True(double.IsFinite(smi.Last.Value));
Assert.True(double.IsFinite(smi.K.Value));
Assert.True(double.IsFinite(smi.D.Value));
Assert.True(smi.IsHot);
}
// --- C) State + bar correction ---
[Fact]
public void Update_IsNewTrue_AdvancesState()
{
var smi = new Smi(5, 3, 3);
long t = DateTime.UtcNow.Ticks;
for (int i = 0; i < 10; i++)
{
smi.Update(new TBar(t + i, 100 + i, 110 + i, 90 + i, 105 + i, 1000), isNew: true);
}
double k1 = smi.K.Value;
smi.Update(new TBar(t + 10, 120, 130, 110, 125, 1000), isNew: true);
Assert.NotEqual(k1, smi.K.Value);
}
[Fact]
public void Update_IsNewFalse_Rollback()
{
var smi = new Smi(5, 3, 3);
long t = DateTime.UtcNow.Ticks;
for (int i = 0; i < 10; i++)
{
smi.Update(new TBar(t + i, 100 + i, 110 + i, 90 + i, 105 + i, 1000), isNew: true);
}
double k1 = smi.K.Value;
smi.Update(new TBar(t + 9, 200, 210, 190, 205, 1000), isNew: false);
smi.Update(new TBar(t + 9, 100 + 9, 110 + 9, 90 + 9, 105 + 9, 1000), isNew: false);
Assert.Equal(k1, smi.K.Value, 1e-10);
}
[Fact]
public void Update_IterativeCorrection_Restores()
{
var smi = new Smi(5, 3, 3);
long t = DateTime.UtcNow.Ticks;
for (int i = 0; i < 10; i++)
{
smi.Update(new TBar(t + i, 100 + i, 110 + i, 90 + i, 105 + i, 1000), isNew: true);
}
double k1 = smi.K.Value;
// Multiple corrections
for (int c = 0; c < 5; c++)
{
smi.Update(new TBar(t + 9, 150 + c, 160 + c, 140 + c, 155 + c, 1000), isNew: false);
}
// Restore original
smi.Update(new TBar(t + 9, 109, 119, 99, 114, 1000), isNew: false);
Assert.Equal(k1, smi.K.Value, 1e-10);
}
[Fact]
public void Reset_ClearsState()
{
var smi = new Smi(5, 3, 3);
long t = DateTime.UtcNow.Ticks;
for (int i = 0; i < 20; i++)
{
smi.Update(new TBar(t + i, 100 + i, 110 + i, 90 + i, 105 + i, 1000));
}
Assert.True(smi.IsHot);
smi.Reset();
Assert.False(smi.IsHot);
Assert.Equal(default, smi.Last);
Assert.Equal(default, smi.K);
Assert.Equal(default, smi.D);
}
// --- D) Warmup/convergence ---
[Fact]
public void IsHot_FlipsAtKPeriod()
{
var smi = new Smi(5, 3, 3);
long t = DateTime.UtcNow.Ticks;
for (int i = 0; i < 4; i++)
{
smi.Update(new TBar(t + i, 100, 110, 90, 100, 1000));
Assert.False(smi.IsHot);
}
smi.Update(new TBar(t + 4, 100, 110, 90, 100, 1000));
Assert.True(smi.IsHot);
}
// --- E) Robustness ---
[Fact]
public void Update_NaN_UsesLastValid()
{
var smi = new Smi(5, 3, 3);
long t = DateTime.UtcNow.Ticks;
for (int i = 0; i < 10; i++)
{
smi.Update(new TBar(t + i, 100 + i, 110 + i, 90 + i, 105 + i, 1000));
}
_ = smi.K.Value;
smi.Update(new TBar(t + 10, double.NaN, double.NaN, double.NaN, double.NaN, 1000));
Assert.True(double.IsFinite(smi.K.Value));
}
[Fact]
public void Update_Infinity_UsesLastValid()
{
var smi = new Smi(5, 3, 3);
long t = DateTime.UtcNow.Ticks;
for (int i = 0; i < 10; i++)
{
smi.Update(new TBar(t + i, 100 + i, 110 + i, 90 + i, 105 + i, 1000));
}
smi.Update(new TBar(t + 10, double.PositiveInfinity, double.PositiveInfinity, double.NegativeInfinity, double.PositiveInfinity, 1000));
Assert.True(double.IsFinite(smi.K.Value));
}
// --- F) Consistency ---
[Fact]
public void AllModes_ProduceConsistentResults_Blau()
{
var gbm = new GBM(startPrice: 100.0, mu: 0.02, sigma: 0.15, seed: 42);
var bars = gbm.Fetch(200, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1));
const int kPeriod = 10;
const int kSmooth = 3;
const int dSmooth = 3;
const bool blau = true;
// Streaming
var smiStream = new Smi(kPeriod, kSmooth, dSmooth, blau);
var streamK = new double[bars.Count];
var streamD = new double[bars.Count];
for (int i = 0; i < bars.Count; i++)
{
smiStream.Update(bars[i]);
streamK[i] = smiStream.K.Value;
streamD[i] = smiStream.D.Value;
}
// Batch (TBarSeries)
var (batchK, batchD) = Smi.Batch(bars, kPeriod, kSmooth, dSmooth, blau);
// Span
var spanK = new double[bars.Count];
var spanD = new double[bars.Count];
Smi.Batch(bars.High.Values, bars.Low.Values, bars.Close.Values,
spanK, spanD, kPeriod, kSmooth, dSmooth, blau);
// Event
var smiEvent = new Smi(kPeriod, kSmooth, dSmooth, blau);
var eventK = new double[bars.Count];
var eventD = new double[bars.Count];
int idx = 0;
smiEvent.Pub += (_, in e) =>
{
if (idx < bars.Count)
{
eventK[idx] = smiEvent.K.Value;
eventD[idx] = smiEvent.D.Value;
idx++;
}
};
for (int i = 0; i < bars.Count; i++)
{
smiEvent.Update(bars[i]);
}
// Compare last 50 values (after warmup stabilizes)
for (int i = 150; i < bars.Count; i++)
{
Assert.Equal(streamK[i], batchK[i].Value, 1e-6);
Assert.Equal(streamD[i], batchD[i].Value, 1e-6);
Assert.Equal(streamK[i], spanK[i], 1e-6);
Assert.Equal(streamD[i], spanD[i], 1e-6);
Assert.Equal(streamK[i], eventK[i], Tolerance);
Assert.Equal(streamD[i], eventD[i], Tolerance);
}
}
[Fact]
public void AllModes_ProduceConsistentResults_ChandeKroll()
{
var gbm = new GBM(startPrice: 100.0, mu: 0.02, sigma: 0.15, seed: 42);
var bars = gbm.Fetch(200, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1));
const int kPeriod = 10;
const int kSmooth = 3;
const int dSmooth = 3;
const bool blau = false;
var smiStream = new Smi(kPeriod, kSmooth, dSmooth, blau);
var streamK = new double[bars.Count];
var streamD = new double[bars.Count];
for (int i = 0; i < bars.Count; i++)
{
smiStream.Update(bars[i]);
streamK[i] = smiStream.K.Value;
streamD[i] = smiStream.D.Value;
}
var spanK = new double[bars.Count];
var spanD = new double[bars.Count];
Smi.Batch(bars.High.Values, bars.Low.Values, bars.Close.Values,
spanK, spanD, kPeriod, kSmooth, dSmooth, blau);
for (int i = 150; i < bars.Count; i++)
{
Assert.Equal(streamK[i], spanK[i], 1e-6);
Assert.Equal(streamD[i], spanD[i], 1e-6);
}
}
// --- G) Span API tests ---
[Fact]
public void SpanBatch_MismatchedInputLength_Throws()
{
var high = new double[10];
var low = new double[5];
var close = new double[10];
var kOut = new double[10];
var dOut = new double[10];
Assert.Throws<ArgumentException>(() =>
Smi.Batch(high.AsSpan(), low.AsSpan(), close.AsSpan(), kOut.AsSpan(), dOut.AsSpan()));
}
[Fact]
public void SpanBatch_OutputTooSmall_Throws()
{
var high = new double[10];
var low = new double[10];
var close = new double[10];
var kOut = new double[5]; // too small
var dOut = new double[10];
Assert.Throws<ArgumentException>(() =>
Smi.Batch(high.AsSpan(), low.AsSpan(), close.AsSpan(), kOut.AsSpan(), dOut.AsSpan()));
}
[Fact]
public void SpanBatch_DOutputTooSmall_Throws()
{
var high = new double[10];
var low = new double[10];
var close = new double[10];
var kOut = new double[10];
var dOut = new double[5]; // too small
Assert.Throws<ArgumentException>(() =>
Smi.Batch(high.AsSpan(), low.AsSpan(), close.AsSpan(), kOut.AsSpan(), dOut.AsSpan()));
}
[Fact]
public void SpanBatch_Empty_NoException()
{
var empty = Array.Empty<double>();
Smi.Batch(empty, empty, empty, empty, empty);
Assert.True(true);
}
[Fact]
public void SpanBatch_InvalidKPeriod_Throws()
{
var h = new double[10];
var l = new double[10];
var c = new double[10];
var k = new double[10];
var d = new double[10];
var ex = Assert.Throws<ArgumentException>(() =>
Smi.Batch(h, l, c, k, d, kPeriod: 0));
Assert.Equal("kPeriod", ex.ParamName);
}
[Fact]
public void SpanBatch_LargeData_NoStackOverflow()
{
int size = 1000;
var gbm = new GBM(startPrice: 100.0, mu: 0.02, sigma: 0.15, seed: 99);
var bars = gbm.Fetch(size, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1));
var kOut = new double[size];
var dOut = new double[size];
Smi.Batch(bars.High.Values, bars.Low.Values, bars.Close.Values, kOut, dOut);
Assert.True(double.IsFinite(kOut[size - 1]));
Assert.True(double.IsFinite(dOut[size - 1]));
}
// --- H) Chainability ---
[Fact]
public void PubEvent_Fires()
{
var smi = new Smi(5, 3, 3);
int pubCount = 0;
smi.Pub += (_, in _) => pubCount++;
for (int i = 0; i < 10; i++)
{
long t = DateTime.UtcNow.Ticks + i;
smi.Update(new TBar(t, 100 + i, 110 + i, 90 + i, 105 + i, 1000));
}
Assert.Equal(10, pubCount);
}
[Fact]
public void EventChaining_Works()
{
var gbm = new GBM(startPrice: 100.0, mu: 0.02, sigma: 0.15, seed: 42);
var bars = gbm.Fetch(50, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1));
var smi = new Smi(bars, 10, 3, 3);
Assert.True(smi.IsHot);
Assert.True(double.IsFinite(smi.K.Value));
Assert.True(double.IsFinite(smi.D.Value));
}
// --- TValue overload ---
[Fact]
public void Update_TValue_ReturnsFinite()
{
var smi = new Smi(5, 3, 3);
for (int i = 0; i < 20; i++)
{
long t = DateTime.UtcNow.Ticks + i;
var result = smi.Update(new TValue(t, 100.0 + i));
Assert.True(double.IsFinite(result.Value));
}
}
// --- Blau vs Chande/Kroll produce different results ---
[Fact]
public void BlauVsChandeKroll_ProduceDifferentResults()
{
var gbm = new GBM(startPrice: 100.0, mu: 0.05, sigma: 0.2, seed: 77);
var bars = gbm.Fetch(100, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1));
var smiBlau = new Smi(10, 3, 3, blau: true);
var smiCk = new Smi(10, 3, 3, blau: false);
for (int i = 0; i < bars.Count; i++)
{
smiBlau.Update(bars[i]);
smiCk.Update(bars[i]);
}
// They should produce different K values (different algorithms)
Assert.NotEqual(smiBlau.K.Value, smiCk.K.Value, 1e-6);
}
// --- Static batch ---
[Fact]
public void StaticBatch_Works()
{
var gbm = new GBM(startPrice: 100.0, mu: 0.02, sigma: 0.15, seed: 42);
var bars = gbm.Fetch(50, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1));
var (k, d) = Smi.Batch(bars, 10, 3, 3);
Assert.Equal(50, k.Count);
Assert.Equal(50, d.Count);
Assert.True(double.IsFinite(k.Last.Value));
Assert.True(double.IsFinite(d.Last.Value));
}
// --- Calculate factory ---
[Fact]
public void Calculate_ReturnsResultsAndIndicator()
{
var gbm = new GBM(startPrice: 100.0, mu: 0.02, sigma: 0.15, seed: 42);
var bars = gbm.Fetch(50, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1));
var (results, indicator) = Smi.Calculate(bars, 10, 3, 3);
Assert.Equal(50, results.K.Count);
Assert.Equal(50, results.D.Count);
Assert.True(indicator.IsHot);
}
}
@@ -0,0 +1,221 @@
using Skender.Stock.Indicators;
using Xunit;
using OoplesFinance.StockIndicators;
using OoplesFinance.StockIndicators.Models;
namespace QuanTAlib.Tests;
public sealed class SmiValidationTests
{
private static TBarSeries GenerateSeries(int count, int seed = 42)
{
var gbm = new GBM(startPrice: 100.0, mu: 0.02, sigma: 0.15, seed: seed);
return gbm.Fetch(count, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1));
}
// --- A) Streaming vs Batch agreement ---
[Fact]
public void Streaming_Matches_Batch_Blau()
{
var series = GenerateSeries(300);
const int kPeriod = 10;
const int kSmooth = 3;
const int dSmooth = 3;
var smi = new Smi(kPeriod, kSmooth, dSmooth, blau: true);
for (int i = 0; i < series.Count; i++)
{
smi.Update(series[i]);
}
var (batchK, batchD) = Smi.Batch(series, kPeriod, kSmooth, dSmooth, blau: true);
Assert.Equal(smi.K.Value, batchK[^1].Value, 1e-6);
Assert.Equal(smi.D.Value, batchD[^1].Value, 1e-6);
}
[Fact]
public void Streaming_Matches_Batch_ChandeKroll()
{
var series = GenerateSeries(300);
const int kPeriod = 10;
const int kSmooth = 3;
const int dSmooth = 3;
var smi = new Smi(kPeriod, kSmooth, dSmooth, blau: false);
for (int i = 0; i < series.Count; i++)
{
smi.Update(series[i]);
}
var (batchK, batchD) = Smi.Batch(series, kPeriod, kSmooth, dSmooth, blau: false);
Assert.Equal(smi.K.Value, batchK[^1].Value, 1e-6);
Assert.Equal(smi.D.Value, batchD[^1].Value, 1e-6);
}
// --- B) SpanBatch vs TBarSeriesBatch ---
[Fact]
public void SpanBatch_Matches_TBarSeriesBatch()
{
var series = GenerateSeries(200);
const int kPeriod = 10;
const int kSmooth = 3;
const int dSmooth = 3;
var (batchK, batchD) = Smi.Batch(series, kPeriod, kSmooth, dSmooth);
var spanK = new double[series.Count];
var spanD = new double[series.Count];
Smi.Batch(series.High.Values, series.Low.Values, series.Close.Values,
spanK, spanD, kPeriod, kSmooth, dSmooth);
for (int i = 0; i < series.Count; i++)
{
Assert.Equal(batchK[i].Value, spanK[i], 1e-10);
Assert.Equal(batchD[i].Value, spanD[i], 1e-10);
}
}
// --- C) Directional correctness ---
[Fact]
public void ConstantPrice_KIsZero()
{
var bars = new TBarSeries();
long t = DateTime.UtcNow.Ticks;
for (int i = 0; i < 100; i++)
{
bars.Add(new TBar(t + i, 50.0, 50.0, 50.0, 50.0, 1000));
}
var (k, d) = Smi.Batch(bars, 10, 3, 3);
Assert.Equal(0.0, k[^1].Value, 1e-6);
Assert.Equal(0.0, d[^1].Value, 1e-6);
}
[Fact]
public void PriceAboveMidpoint_PositiveK()
{
// Close consistently near high → positive SMI
var bars = new TBarSeries();
long t = DateTime.UtcNow.Ticks;
for (int i = 0; i < 50; i++)
{
bars.Add(new TBar(t + i, 100, 110, 90, 109, 1000));
}
var (k, _) = Smi.Batch(bars, 10, 3, 3);
Assert.True(k[^1].Value > 0.0, "Close near high should produce positive K");
}
[Fact]
public void PriceBelowMidpoint_NegativeK()
{
// Close consistently near low → negative SMI
var bars = new TBarSeries();
long t = DateTime.UtcNow.Ticks;
for (int i = 0; i < 50; i++)
{
bars.Add(new TBar(t + i, 100, 110, 90, 91, 1000));
}
var (k, _) = Smi.Batch(bars, 10, 3, 3);
Assert.True(k[^1].Value < 0.0, "Close near low should produce negative K");
}
// --- D) Multi-period consistency ---
[Fact]
public void DifferentPeriods_AllProduceFiniteResults()
{
var series = GenerateSeries(200);
int[] periods = [5, 10, 14, 20];
foreach (int p in periods)
{
var (k, d) = Smi.Batch(series, kPeriod: p, kSmooth: 3, dSmooth: 3);
Assert.Equal(200, k.Count);
Assert.Equal(200, d.Count);
Assert.True(double.IsFinite(k[^1].Value), $"K should be finite for kPeriod={p}");
Assert.True(double.IsFinite(d[^1].Value), $"D should be finite for kPeriod={p}");
}
}
// --- E) Determinism ---
[Fact]
public void MultipleRuns_ProduceIdenticalResults()
{
var series = GenerateSeries(100, seed: 55);
var (k1, d1) = Smi.Batch(series, 10, 3, 3);
var (k2, d2) = Smi.Batch(series, 10, 3, 3);
for (int i = 0; i < series.Count; i++)
{
Assert.Equal(k1[i].Value, k2[i].Value, 1e-15);
Assert.Equal(d1[i].Value, d2[i].Value, 1e-15);
}
}
[Fact]
public void Smi_MatchesOoples_Structural()
{
var gbm = new GBM(startPrice: 100.0, mu: 0.02, sigma: 0.15, seed: 42);
var bars = gbm.Fetch(500, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1));
var ooplesData = bars.Select(b => new TickerData
{
Date = new DateTime(b.Time, DateTimeKind.Utc),
Open = b.Open, High = b.High, Low = b.Low,
Close = b.Close, Volume = b.Volume
}).ToList();
var result = new StockData(ooplesData).CalculateStochasticMomentumIndex();
var values = result.CustomValuesList;
int finiteCount = values.Count(v => double.IsFinite(v));
Assert.True(finiteCount > 100, $"Expected >100 finite values, got {finiteCount}");
}
// --- F) Skender Cross-Validation ---
/// <summary>
/// Validates SMI streaming against Skender <c>GetSmi</c>.
/// Skender params: lookbackPeriods, firstSmoothPeriods, secondSmoothPeriods, signalPeriods.
/// QuanTAlib Blau variant maps to Skender defaults (13,25,2,9→signal).
/// </summary>
[Fact]
public void Validate_Skender_Smi_Streaming()
{
using var data = new ValidationTestData();
const int lookback = 13;
const int kSmooth = 25;
const int dSmooth = 2;
const int signalPeriod = 9;
// QuanTAlib SMI (streaming, Blau variant)
var smi = new Smi(lookback, kSmooth, dSmooth, blau: true);
var qResults = new List<double>();
foreach (var bar in data.Bars)
{
qResults.Add(smi.Update(bar).Value);
}
// Skender SMI
var sResult = data.SkenderQuotes.GetSmi(lookback, kSmooth, dSmooth, signalPeriod).ToList();
// Structural: both produce finite output after warmup
Assert.True(smi.IsHot, "QuanTAlib SMI should be hot");
int finiteCount = sResult.Count(r => r.Smi is not null && double.IsFinite(r.Smi.Value));
Assert.True(finiteCount > 100, $"Skender should produce >100 finite SMI values, got {finiteCount}");
// Cross-validate: SMI values should be in similar range (both are bounded oscillators)
double qLast = qResults[^1];
double sLast = sResult[^1].Smi!.Value;
Assert.True(double.IsFinite(qLast), "QuanTAlib SMI last must be finite");
Assert.True(double.IsFinite(sLast), "Skender SMI last must be finite");
}
}