Files
QuanTAlib/lib/trends_FIR/hwma/Hwma.Validation.Tests.cs
T
2026-02-10 21:33:16 -08:00

219 lines
7.0 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
namespace QuanTAlib.Tests;
/// <summary>
/// Validation tests for HWMA (Holt-Winters Moving Average).
/// Note: HWMA is not available in most external libraries (TA-Lib, Skender, etc.),
/// so we validate against our own PineScript reference implementation and mathematical properties.
/// </summary>
public class HwmaValidationTests
{
private const double Tolerance = 1e-9;
[Fact]
public void Hwma_MatchesPineScriptReference()
{
// Test that our implementation matches the PineScript reference
// hwma.pine formulas:
// α = 2/(period+1), β = 1/period, γ = 1/period
// F = α × source + (1-α) × (prevF + prevV + 0.5 × prevA)
// V = β × (F - prevF) + (1-β) × (prevV + prevA)
// A = γ × (V - prevV) + (1-γ) × prevA
// output = F + V + 0.5 × A
var series = new TSeries();
var gbm = new GBM(startPrice: 100.0, mu: 0.05, sigma: 0.2, seed: 42);
for (int i = 0; i < 50; i++)
{
var bar = gbm.Next(isNew: true);
series.Add(bar.Time, bar.Close);
}
int period = 10;
var hwma = new Hwma(period);
var results = hwma.Update(series);
// Manual calculation
double alpha = 2.0 / (period + 1.0);
double beta = 1.0 / period;
double gamma = 1.0 / period;
double F = series[0].Value;
double V = 0;
double A = 0;
for (int i = 1; i < series.Count; i++)
{
double prevF = F;
double prevV = V;
double prevA = A;
F = alpha * series[i].Value + (1 - alpha) * (prevF + prevV + 0.5 * prevA);
V = beta * (F - prevF) + (1 - beta) * (prevV + prevA);
A = gamma * (V - prevV) + (1 - gamma) * prevA;
}
double expected = F + V + 0.5 * A;
Assert.Equal(expected, results.Last.Value, Tolerance);
}
[Fact]
public void Hwma_SmoothingFactorFormulas()
{
// Verify smoothing factors are calculated correctly from period
// α = 2/(period+1), β = γ = 1/period
int period = 10;
double expectedAlpha = 2.0 / (period + 1.0); // 2/11 ≈ 0.1818
double expectedBeta = 1.0 / period; // 0.1
double expectedGamma = 1.0 / period; // 0.1
Assert.Equal(2.0 / 11.0, expectedAlpha, Tolerance);
Assert.Equal(0.1, expectedBeta, Tolerance);
Assert.Equal(0.1, expectedGamma, Tolerance);
}
[Fact]
public void Hwma_ConsistentAcrossModes()
{
var gbm = new GBM(startPrice: 100.0, mu: 0.05, sigma: 0.2, seed: 42);
var series = new TSeries();
for (int i = 0; i < 100; i++)
{
var bar = gbm.Next(isNew: true);
series.Add(bar.Time, bar.Close);
}
int period = 10;
// Batch
var batchResults = Hwma.Batch(series, period);
// Streaming
var streaming = new Hwma(period);
var streamingResults = new TSeries();
foreach (var item in series)
{
streamingResults.Add(streaming.Update(item));
}
// Span
double[] input = series.Values.ToArray();
double[] spanOutput = new double[input.Length];
Hwma.Batch(input.AsSpan(), spanOutput.AsSpan(), period);
// All should match
for (int i = 0; i < series.Count; i++)
{
Assert.Equal(batchResults[i].Value, streamingResults[i].Value, Tolerance);
Assert.Equal(batchResults[i].Value, spanOutput[i], Tolerance);
}
}
[Fact]
public void Hwma_DifferentPeriodsProduceDifferentResults()
{
var gbm = new GBM(startPrice: 100.0, mu: 0.05, sigma: 0.2, seed: 42);
var series = new TSeries();
for (int i = 0; i < 50; i++)
{
var bar = gbm.Next(isNew: true);
series.Add(bar.Time, bar.Close);
}
var hwma5 = new Hwma(5);
var hwma10 = new Hwma(10);
var hwma20 = new Hwma(20);
var results5 = hwma5.Update(series);
var results10 = hwma10.Update(series);
var results20 = hwma20.Update(series);
// Different periods should produce different results
Assert.NotEqual(results5.Last.Value, results10.Last.Value);
Assert.NotEqual(results10.Last.Value, results20.Last.Value);
}
[Fact]
public void Hwma_ConstantInput_ReturnsConstant()
{
var hwma = new Hwma(10);
const double constantValue = 100.0;
for (int i = 0; i < 20; i++)
{
var result = hwma.Update(new TValue(DateTime.UtcNow, constantValue));
Assert.Equal(constantValue, result.Value, Tolerance);
}
}
[Fact]
public void Hwma_TripleExponentialSmoothing_Property()
{
// HWMA should exhibit the triple exponential smoothing behavior:
// - Level (F) tracks the current value
// - Velocity (V) tracks the trend/slope
// - Acceleration (A) tracks the change in trend
// For a linear trend, HWMA should converge to track it closely
var hwma = new Hwma(10);
// Linear uptrend: 100, 101, 102, ..., 119
for (int i = 0; i < 20; i++)
{
double price = 100 + i;
hwma.Update(new TValue(DateTime.UtcNow, price));
}
// After 20 points of linear trend, HWMA should be close to current value
double lastPrice = 119;
double hwmaValue = hwma.Last.Value;
// Should be within 5% for a well-adapted filter
Assert.True(Math.Abs(hwmaValue - lastPrice) / lastPrice < 0.05);
}
[Fact]
public void Hwma_VelocityTracking_Uptrend()
{
// In a consistent uptrend, HWMA should be ahead of simple EMA
// because it accounts for velocity
var hwma = new Hwma(10);
var ema = new Ema(10);
// Generate uptrend
for (int i = 0; i < 30; i++)
{
double price = 100 + i * 2; // Strong uptrend
hwma.Update(new TValue(DateTime.UtcNow, price));
ema.Update(new TValue(DateTime.UtcNow, price));
}
// HWMA should be closer to current price than EMA in uptrend
// (or even ahead due to velocity/acceleration extrapolation)
double currentPrice = 100 + 29 * 2; // 158
double hwmaDiff = Math.Abs(hwma.Last.Value - currentPrice);
double emaDiff = Math.Abs(ema.Last.Value - currentPrice);
// HWMA should track better than or equal to EMA in trends
Assert.True(hwmaDiff <= emaDiff * 1.5); // Allow some margin
}
[Fact]
public void Hwma_AlphaBetaGamma_CustomValues()
{
// Test explicit alpha/beta/gamma constructor produces valid results
var hwma = new Hwma(0.3, 0.2, 0.1);
var gbm = new GBM(startPrice: 100.0, mu: 0.05, sigma: 0.2, seed: 42);
for (int i = 0; i < 30; i++)
{
var bar = gbm.Next(isNew: true);
hwma.Update(new TValue(bar.Time, bar.Close));
}
Assert.True(double.IsFinite(hwma.Last.Value));
Assert.True(hwma.Last.Value > 0);
}
}