mirror of
https://github.com/mihakralj/QuanTAlib.git
synced 2026-08-12 23:58:04 +00:00
- 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
201 lines
5.5 KiB
C#
201 lines
5.5 KiB
C#
using Xunit;
|
|
|
|
namespace QuanTAlib.Tests;
|
|
|
|
/// <summary>
|
|
/// Self-consistency validation for NMA. No external library supports NMA,
|
|
/// so we validate internal consistency: streaming==batch==span, ratio bounds,
|
|
/// regime detection, and determinism.
|
|
/// </summary>
|
|
public class NmaValidationTests
|
|
{
|
|
private const long Seed = 12345;
|
|
private static readonly TimeSpan Step = TimeSpan.FromMinutes(1);
|
|
|
|
private static TSeries GetTestSeries(int count = 500)
|
|
{
|
|
var gbm = new GBM();
|
|
var bars = gbm.Fetch(count, Seed, Step);
|
|
return bars.Close;
|
|
}
|
|
|
|
[Fact]
|
|
public void StreamingEqualsBatch_DefaultPeriod()
|
|
{
|
|
var series = GetTestSeries(500);
|
|
int period = 40;
|
|
|
|
// Streaming
|
|
var streaming = new Nma(period);
|
|
var streamResults = new double[series.Count];
|
|
for (int i = 0; i < series.Count; i++)
|
|
{
|
|
streamResults[i] = streaming.Update(series[i]).Value;
|
|
}
|
|
|
|
// Batch (span)
|
|
var batchResults = new double[series.Count];
|
|
Nma.Batch(series.Values, batchResults, period);
|
|
|
|
for (int i = 0; i < series.Count; i++)
|
|
{
|
|
Assert.Equal(streamResults[i], batchResults[i], 1e-7);
|
|
}
|
|
}
|
|
|
|
[Fact]
|
|
public void StreamingEqualsTSeries()
|
|
{
|
|
var series = GetTestSeries(500);
|
|
int period = 40;
|
|
|
|
// Streaming
|
|
var streaming = new Nma(period);
|
|
var streamResults = new double[series.Count];
|
|
for (int i = 0; i < series.Count; i++)
|
|
{
|
|
streamResults[i] = streaming.Update(series[i]).Value;
|
|
}
|
|
|
|
// TSeries batch
|
|
var batchSeries = Nma.Batch(series, period);
|
|
|
|
for (int i = 0; i < series.Count; i++)
|
|
{
|
|
Assert.Equal(streamResults[i], batchSeries.Values[i], 1e-7);
|
|
}
|
|
}
|
|
|
|
[Theory]
|
|
[InlineData(5)]
|
|
[InlineData(14)]
|
|
[InlineData(40)]
|
|
[InlineData(80)]
|
|
public void ConsistencyAcrossPeriods(int period)
|
|
{
|
|
var series = GetTestSeries(300);
|
|
|
|
// Streaming
|
|
var streaming = new Nma(period);
|
|
var streamResults = new double[series.Count];
|
|
for (int i = 0; i < series.Count; i++)
|
|
{
|
|
streamResults[i] = streaming.Update(series[i]).Value;
|
|
}
|
|
|
|
// Batch
|
|
var batchResults = new double[series.Count];
|
|
Nma.Batch(series.Values, batchResults, period);
|
|
|
|
for (int i = 0; i < series.Count; i++)
|
|
{
|
|
Assert.Equal(streamResults[i], batchResults[i], 1e-7);
|
|
}
|
|
}
|
|
|
|
[Fact]
|
|
public void ConstantInput_NmaEqualsConstant()
|
|
{
|
|
double constant = 100.0;
|
|
int period = 40;
|
|
int count = 200;
|
|
|
|
var nma = new Nma(period);
|
|
for (int i = 0; i < count; i++)
|
|
{
|
|
nma.Update(new TValue(DateTime.UtcNow.AddMinutes(i), constant));
|
|
}
|
|
|
|
// For constant input, volatility is 0 everywhere → ratio = 0
|
|
// But first bar seeds NMA = constant, so it should stay constant
|
|
Assert.Equal(constant, nma.Last.Value, 1e-8);
|
|
}
|
|
|
|
[Fact]
|
|
public void MonotonicRising_NmaFollowsGradually()
|
|
{
|
|
int period = 14;
|
|
var nma = new Nma(period);
|
|
|
|
double lastNma = 0;
|
|
for (int i = 0; i < 100; i++)
|
|
{
|
|
double price = 100.0 + i * 0.5;
|
|
lastNma = nma.Update(new TValue(DateTime.UtcNow.AddMinutes(i), price)).Value;
|
|
}
|
|
|
|
// NMA should lag behind the linearly rising price
|
|
Assert.True(lastNma > 100.0, "NMA should rise");
|
|
Assert.True(lastNma < 150.0, "NMA should lag behind final price");
|
|
}
|
|
|
|
[Fact]
|
|
public void DeterministicOutput()
|
|
{
|
|
var series = GetTestSeries(200);
|
|
int period = 40;
|
|
|
|
var nma1 = new Nma(period);
|
|
var nma2 = new Nma(period);
|
|
for (int i = 0; i < series.Count; i++)
|
|
{
|
|
var r1 = nma1.Update(series[i]);
|
|
var r2 = nma2.Update(series[i]);
|
|
Assert.Equal(r1.Value, r2.Value, 1e-15);
|
|
}
|
|
}
|
|
|
|
[Fact]
|
|
public void OutputBounded_WithinInputRange()
|
|
{
|
|
var series = GetTestSeries(500);
|
|
int period = 40;
|
|
|
|
var nma = new Nma(period);
|
|
double minInput = double.MaxValue;
|
|
double maxInput = double.MinValue;
|
|
|
|
for (int i = 0; i < series.Count; i++)
|
|
{
|
|
nma.Update(series[i]);
|
|
if (series[i].Value < minInput)
|
|
{
|
|
minInput = series[i].Value;
|
|
}
|
|
if (series[i].Value > maxInput)
|
|
{
|
|
maxInput = series[i].Value;
|
|
}
|
|
}
|
|
|
|
// NMA should stay within input range (with small tolerance for FP)
|
|
Assert.True(nma.Last.Value >= minInput * 0.99);
|
|
Assert.True(nma.Last.Value <= maxInput * 1.01);
|
|
}
|
|
|
|
[Fact]
|
|
public void SmallPeriod_MoreResponsive()
|
|
{
|
|
var series = GetTestSeries(200);
|
|
|
|
var nmaFast = new Nma(5);
|
|
var nmaSlow = new Nma(80);
|
|
|
|
double sumAbsDiffFast = 0;
|
|
double sumAbsDiffSlow = 0;
|
|
|
|
for (int i = 0; i < series.Count; i++)
|
|
{
|
|
var fast = nmaFast.Update(series[i]).Value;
|
|
var slow = nmaSlow.Update(series[i]).Value;
|
|
|
|
sumAbsDiffFast += Math.Abs(fast - series[i].Value);
|
|
sumAbsDiffSlow += Math.Abs(slow - series[i].Value);
|
|
}
|
|
|
|
// Faster NMA (smaller period) should track price more closely
|
|
Assert.True(sumAbsDiffFast < sumAbsDiffSlow,
|
|
$"Fast NMA avg deviation ({sumAbsDiffFast / series.Count:F4}) should be less than slow ({sumAbsDiffSlow / series.Count:F4})");
|
|
}
|
|
}
|