Files
QuanTAlib/lib/cycles/sine/Sine.Validation.Tests.cs
T
Miha Kralj 951842acca Add validation tests for various volume and momentum indicators
- Introduced Massi validation tests to ensure mathematical properties hold for the Mass Index indicator.
- Added Va validation tests for Volume Accumulation, checking for finite outputs and correct accumulation behavior.
- Implemented Vf validation tests for Volume Force, verifying outputs for rising and falling prices, and ensuring batch and streaming results match.
- Created Vo validation tests for Volume Oscillator, confirming behavior with constant, increasing, and decreasing volumes.
- Developed Vroc validation tests for Volume Rate of Change, validating outputs for constant volume and changes in volume.
- Updated project file to include new momentum indicators (MACD and RSI) in the compilation.
2026-02-12 19:43:09 -08:00

122 lines
3.9 KiB
C#

using Xunit;
namespace QuanTAlib.Tests;
/// <summary>
/// Validation tests for Ehlers Sine Wave indicator.
/// Sine is Ehlers' proprietary cycle indicator not commonly implemented in trading libraries
/// (TA-Lib, Skender, Tulip), so validation is done against mathematical properties
/// and known theoretical results based on the original algorithm.
/// </summary>
public class SineValidationTests
{
[Fact]
public void Validation_OutputRange_NegativeOneToOne()
{
// Sine wave output should be in [-1, 1]
var sine = new Sine();
var gbm = new GBM(seed: 42);
var bars = gbm.Fetch(500, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1));
foreach (var bar in bars)
{
sine.Update(new TValue(bar.Time, bar.Close));
if (sine.IsHot)
{
double val = sine.Last.Value;
Assert.True(val >= -1.0 && val <= 1.0,
$"Sine value {val} is outside expected range [-1, 1]");
}
}
}
[Fact]
public void Validation_ConstantSeries_Bounded()
{
// For a constant price series, there is no real cycle — output should remain bounded
var sine = new Sine();
for (int i = 0; i < 200; i++)
{
sine.Update(new TValue(DateTime.UtcNow.AddSeconds(i), 100.0));
}
// Constant series may not produce exactly zero due to filter initialization artifacts
// but output should remain within the bounded range [-1, 1]
Assert.True(sine.Last.Value >= -1.0 && sine.Last.Value <= 1.0,
$"Constant series should produce bounded sine output, got {sine.Last.Value}");
}
[Fact]
public void Validation_SinusoidInput_DetectsCycle()
{
// Feed a known sinusoidal signal and verify output oscillates
var sine = new Sine(hpPeriod: 40, ssfPeriod: 10);
var values = new List<double>();
for (int i = 0; i < 300; i++)
{
double price = 100.0 + 5.0 * Math.Sin(2.0 * Math.PI * i / 20.0);
sine.Update(new TValue(DateTime.UtcNow.AddSeconds(i), price));
if (sine.IsHot)
{
values.Add(sine.Last.Value);
}
}
// The output should oscillate: check that it crosses zero at least once
bool hasCrossedZero = false;
for (int i = 1; i < values.Count; i++)
{
if ((values[i - 1] >= 0 && values[i] < 0) || (values[i - 1] < 0 && values[i] >= 0))
{
hasCrossedZero = true;
break;
}
}
Assert.True(hasCrossedZero, "Sine should oscillate (cross zero) on sinusoidal input");
}
[Fact]
public void Validation_FiniteOutputs()
{
var sine = new Sine();
var gbm = new GBM(seed: 99);
var bars = gbm.Fetch(300, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1));
foreach (var bar in bars)
{
sine.Update(new TValue(bar.Time, bar.Close));
Assert.True(double.IsFinite(sine.Last.Value),
$"Sine produced non-finite value: {sine.Last.Value}");
}
}
[Fact]
public void Validation_DifferentPeriods_ProduceDifferentResults()
{
var sine1 = new Sine(hpPeriod: 20, ssfPeriod: 5);
var sine2 = new Sine(hpPeriod: 80, ssfPeriod: 20);
var gbm = new GBM(seed: 42);
var bars = gbm.Fetch(300, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1));
bool foundDifference = false;
foreach (var bar in bars)
{
sine1.Update(new TValue(bar.Time, bar.Close));
sine2.Update(new TValue(bar.Time, bar.Close));
if (sine1.IsHot && sine2.IsHot &&
Math.Abs(sine1.Last.Value - sine2.Last.Value) > 1e-6)
{
foundDifference = true;
}
}
Assert.True(foundDifference, "Different HP/SSF periods should produce different results");
}
}