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

222 lines
7.1 KiB
C#

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");
}
}