Files
QuanTAlib/lib/trends_FIR/hwma/Hwma.Validation.Tests.cs
T

219 lines
7.0 KiB
C#
Raw Normal View History

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];
2026-02-10 21:33:16 -08:00
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);
}
}