mirror of
https://github.com/mihakralj/QuanTAlib.git
synced 2026-08-12 15:48:05 +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
224 lines
7.4 KiB
C#
224 lines
7.4 KiB
C#
using Xunit.Abstractions;
|
|
|
|
namespace QuanTAlib.Tests;
|
|
|
|
public sealed class McnmaValidationTests : IDisposable
|
|
{
|
|
private readonly ValidationTestData _testData;
|
|
private readonly ITestOutputHelper _output;
|
|
private bool _disposed;
|
|
|
|
public McnmaValidationTests(ITestOutputHelper output)
|
|
{
|
|
_output = output;
|
|
_testData = new ValidationTestData();
|
|
}
|
|
|
|
public void Dispose()
|
|
{
|
|
Dispose(true);
|
|
}
|
|
|
|
private void Dispose(bool disposing)
|
|
{
|
|
if (_disposed)
|
|
{
|
|
return;
|
|
}
|
|
|
|
_disposed = true;
|
|
|
|
if (disposing)
|
|
{
|
|
_testData?.Dispose();
|
|
}
|
|
}
|
|
|
|
[Fact]
|
|
public void Validate_ManualTemaComposition_Batch()
|
|
{
|
|
// Manual 6-EMA with first-value seeding (matches Pine exactly)
|
|
int[] periods = { 5, 10, 14, 20, 50 };
|
|
|
|
foreach (var period in periods)
|
|
{
|
|
var mcnma = new Mcnma(period);
|
|
var qResult = mcnma.Update(_testData.Data);
|
|
|
|
double alpha = 2.0 / (period + 1);
|
|
double decay = 1.0 - alpha;
|
|
double e1 = 0, e2 = 0, e3 = 0, e4 = 0, e5 = 0, e6 = 0;
|
|
bool init = false;
|
|
var manualResults = new List<double>();
|
|
|
|
for (int i = 0; i < _testData.Data.Count; i++)
|
|
{
|
|
double val = _testData.Data[i].Value;
|
|
if (!init)
|
|
{
|
|
e1 = e2 = e3 = e4 = e5 = e6 = val;
|
|
init = true;
|
|
manualResults.Add(val);
|
|
continue;
|
|
}
|
|
|
|
e1 = Math.FusedMultiplyAdd(e1, decay, alpha * val);
|
|
e2 = Math.FusedMultiplyAdd(e2, decay, alpha * e1);
|
|
e3 = Math.FusedMultiplyAdd(e3, decay, alpha * e2);
|
|
double tema1 = 3.0 * e1 - 3.0 * e2 + e3;
|
|
|
|
e4 = Math.FusedMultiplyAdd(e4, decay, alpha * tema1);
|
|
e5 = Math.FusedMultiplyAdd(e5, decay, alpha * e4);
|
|
e6 = Math.FusedMultiplyAdd(e6, decay, alpha * e5);
|
|
double tema2 = 3.0 * e4 - 3.0 * e5 + e6;
|
|
|
|
manualResults.Add(2.0 * tema1 - tema2);
|
|
}
|
|
|
|
for (int i = 0; i < qResult.Count; i++)
|
|
{
|
|
Assert.Equal(manualResults[i], qResult[i].Value, 1e-9);
|
|
}
|
|
}
|
|
_output.WriteLine("MCNMA Batch(TSeries) validated successfully against manual 6-EMA composition");
|
|
}
|
|
|
|
[Fact]
|
|
public void Validate_StreamingVsBatch_Consistency()
|
|
{
|
|
int[] periods = { 5, 10, 14, 20 };
|
|
|
|
foreach (var period in periods)
|
|
{
|
|
var batchResult = Mcnma.Batch(_testData.Data, period);
|
|
|
|
var streaming = new Mcnma(period);
|
|
for (int i = 0; i < _testData.Data.Count; i++)
|
|
{
|
|
streaming.Update(_testData.Data[i]);
|
|
}
|
|
|
|
int start = Math.Max(0, _testData.Data.Count - 100);
|
|
for (int i = start; i < _testData.Data.Count; i++)
|
|
{
|
|
Assert.Equal(batchResult[i].Value, batchResult[i].Value, 1e-9);
|
|
}
|
|
}
|
|
_output.WriteLine("MCNMA Streaming vs Batch validated successfully");
|
|
}
|
|
|
|
[Fact]
|
|
public void Validate_SpanVsStreaming_Consistency()
|
|
{
|
|
int[] periods = { 5, 10, 14, 20 };
|
|
double[] sourceData = _testData.RawData.ToArray();
|
|
|
|
foreach (var period in periods)
|
|
{
|
|
double[] spanOutput = new double[sourceData.Length];
|
|
Mcnma.Batch(sourceData.AsSpan(), spanOutput.AsSpan(), period);
|
|
|
|
var streaming = new Mcnma(period);
|
|
for (int i = 0; i < sourceData.Length; i++)
|
|
{
|
|
var val = streaming.Update(new TValue(DateTime.UtcNow, sourceData[i]));
|
|
Assert.Equal(val.Value, spanOutput[i], 1e-9);
|
|
}
|
|
}
|
|
_output.WriteLine("MCNMA Span vs Streaming validated successfully");
|
|
}
|
|
|
|
[Fact]
|
|
public void Validate_ConstantInput_ConvergesToInput()
|
|
{
|
|
// With constant input, all EMAs converge to the constant.
|
|
// TEMA(const) = 3*const - 3*const + const = const
|
|
// MCNMA = 2*const - const = const
|
|
const double constantValue = 42.0;
|
|
const int period = 10;
|
|
|
|
var mcnma = new Mcnma(period);
|
|
double lastResult = 0;
|
|
|
|
for (int i = 0; i < 200; i++)
|
|
{
|
|
var result = mcnma.Update(new TValue(DateTime.UtcNow, constantValue));
|
|
lastResult = result.Value;
|
|
}
|
|
|
|
Assert.Equal(constantValue, lastResult, 1e-6);
|
|
_output.WriteLine("MCNMA constant input convergence validated successfully");
|
|
}
|
|
|
|
[Fact]
|
|
public void Validate_Against_ManualFormula()
|
|
{
|
|
// Manual 6-EMA with first-value seeding (matches Pine exactly)
|
|
int[] periods = { 5, 10, 14, 20 };
|
|
|
|
foreach (var period in periods)
|
|
{
|
|
var mcnma = new Mcnma(period);
|
|
double alpha = 2.0 / (period + 1);
|
|
double decay = 1.0 - alpha;
|
|
double e1 = 0, e2 = 0, e3 = 0, e4 = 0, e5 = 0, e6 = 0;
|
|
bool init = false;
|
|
|
|
for (int i = 0; i < _testData.Data.Count; i++)
|
|
{
|
|
var item = _testData.Data[i];
|
|
var qVal = mcnma.Update(item);
|
|
double val = item.Value;
|
|
|
|
if (!init)
|
|
{
|
|
e1 = e2 = e3 = e4 = e5 = e6 = val;
|
|
init = true;
|
|
Assert.Equal(val, qVal.Value, ValidationHelper.DefaultTolerance);
|
|
continue;
|
|
}
|
|
|
|
e1 = Math.FusedMultiplyAdd(e1, decay, alpha * val);
|
|
e2 = Math.FusedMultiplyAdd(e2, decay, alpha * e1);
|
|
e3 = Math.FusedMultiplyAdd(e3, decay, alpha * e2);
|
|
double tema1 = 3.0 * e1 - 3.0 * e2 + e3;
|
|
|
|
e4 = Math.FusedMultiplyAdd(e4, decay, alpha * tema1);
|
|
e5 = Math.FusedMultiplyAdd(e5, decay, alpha * e4);
|
|
e6 = Math.FusedMultiplyAdd(e6, decay, alpha * e5);
|
|
double tema2 = 3.0 * e4 - 3.0 * e5 + e6;
|
|
|
|
double manualVal = 2.0 * tema1 - tema2;
|
|
Assert.Equal(manualVal, qVal.Value, ValidationHelper.DefaultTolerance);
|
|
}
|
|
}
|
|
_output.WriteLine("MCNMA validated successfully against manual 6-EMA formula");
|
|
}
|
|
|
|
[Fact]
|
|
public void Validate_NaN_Robustness()
|
|
{
|
|
const int period = 10;
|
|
var mcnma = new Mcnma(period);
|
|
|
|
for (int i = 0; i < 20; i++)
|
|
{
|
|
mcnma.Update(new TValue(DateTime.UtcNow, 100.0 + i));
|
|
}
|
|
|
|
var nanResult = mcnma.Update(new TValue(DateTime.UtcNow, double.NaN));
|
|
Assert.True(double.IsFinite(nanResult.Value), "MCNMA should handle NaN with last-valid substitution");
|
|
|
|
var infResult = mcnma.Update(new TValue(DateTime.UtcNow, double.PositiveInfinity));
|
|
Assert.True(double.IsFinite(infResult.Value), "MCNMA should handle Infinity with last-valid substitution");
|
|
|
|
var negInfResult = mcnma.Update(new TValue(DateTime.UtcNow, double.NegativeInfinity));
|
|
Assert.True(double.IsFinite(negInfResult.Value), "MCNMA should handle -Infinity with last-valid substitution");
|
|
|
|
var resumeResult = mcnma.Update(new TValue(DateTime.UtcNow, 125.0));
|
|
Assert.True(double.IsFinite(resumeResult.Value), "MCNMA should resume cleanly after invalid inputs");
|
|
|
|
_output.WriteLine("MCNMA NaN/Infinity robustness validated successfully");
|
|
}
|
|
}
|