namespace QuanTAlib.Tests; public class MpeTests { private const double Precision = 1e-10; [Fact] public void Constructor_ValidatesInput() { Assert.Throws(() => new Mpe(0)); Assert.Throws(() => new Mpe(-1)); var mpe = new Mpe(10); Assert.NotNull(mpe); } [Fact] public void Calc_ReturnsValue() { var mpe = new Mpe(10); var result = mpe.Update(100.0, 90.0); Assert.True(double.IsFinite(result.Value)); Assert.Equal(result.Value, mpe.Last.Value); } [Fact] public void ZeroError_ReturnsZero() { var mpe = new Mpe(5); for (int i = 0; i < 5; i++) { mpe.Update(100.0, 100.0); } Assert.Equal(0.0, mpe.Last.Value, Precision); } [Fact] public void UnderPrediction_ReturnsPositive() { // MPE: 100 * (actual - predicted) / actual // When actual > predicted, result is positive var mpe = new Mpe(1); var result = mpe.Update(100.0, 80.0); // MPE = 100 * (100 - 80) / 100 = 20% Assert.Equal(20.0, result.Value, Precision); } [Fact] public void OverPrediction_ReturnsNegative() { // When actual < predicted, result is negative var mpe = new Mpe(1); var result = mpe.Update(100.0, 120.0); // MPE = 100 * (100 - 120) / 100 = -20% Assert.Equal(-20.0, result.Value, Precision); } [Fact] public void Period1_ReturnsCurrentError() { var mpe = new Mpe(1); // actual=100, predicted=90 -> MPE = 100 * (100-90)/100 = 10% var r1 = mpe.Update(100.0, 90.0); Assert.Equal(10.0, r1.Value, Precision); // actual=100, predicted=110 -> MPE = 100 * (100-110)/100 = -10% var r2 = mpe.Update(100.0, 110.0); Assert.Equal(-10.0, r2.Value, Precision); } [Fact] public void KnownValues_CalculatesCorrectly() { var mpe = new Mpe(3); // actual=100, predicted=90 -> MPE = 10% mpe.Update(100.0, 90.0); // actual=100, predicted=110 -> MPE = -10% mpe.Update(100.0, 110.0); // actual=100, predicted=100 -> MPE = 0% mpe.Update(100.0, 100.0); // Average: (10 + (-10) + 0) / 3 = 0% Assert.Equal(0.0, mpe.Last.Value, Precision); } [Fact] public void BiasDetection_PositiveBiasAverage() { var mpe = new Mpe(3); // Consistently under-predicting mpe.Update(100.0, 95.0); // +5% mpe.Update(100.0, 90.0); // +10% mpe.Update(100.0, 85.0); // +15% // Average: (5 + 10 + 15) / 3 = 10% Assert.Equal(10.0, mpe.Last.Value, Precision); Assert.True(mpe.Last.Value > 0); // Positive bias } [Fact] public void BiasDetection_NegativeBiasAverage() { var mpe = new Mpe(3); // Consistently over-predicting mpe.Update(100.0, 105.0); // -5% mpe.Update(100.0, 110.0); // -10% mpe.Update(100.0, 115.0); // -15% // Average: (-5 + -10 + -15) / 3 = -10% Assert.Equal(-10.0, mpe.Last.Value, Precision); Assert.True(mpe.Last.Value < 0); // Negative bias } [Fact] public void NaN_Input_UsesLastValidValue() { var mpe = new Mpe(5); mpe.Update(100.0, 90.0); mpe.Update(100.0, 95.0); var resultAfterNaN = mpe.Update(double.NaN, 90.0); Assert.True(double.IsFinite(resultAfterNaN.Value)); } [Fact] public void Infinity_Input_UsesLastValidValue() { var mpe = new Mpe(5); mpe.Update(100.0, 90.0); var resultAfterPosInf = mpe.Update(double.PositiveInfinity, 90.0); Assert.True(double.IsFinite(resultAfterPosInf.Value)); var resultAfterNegInf = mpe.Update(100.0, double.NegativeInfinity); Assert.True(double.IsFinite(resultAfterNegInf.Value)); } [Fact] public void ZeroActual_HandledGracefully() { var mpe = new Mpe(5); mpe.Update(100.0, 90.0); var result = mpe.Update(0.0, 10.0); Assert.True(double.IsFinite(result.Value)); } [Fact] public void IsHot_BecomesTrueWhenBufferFull() { var mpe = new Mpe(5); Assert.False(mpe.IsHot); for (int i = 1; i <= 4; i++) { mpe.Update(100.0, 90.0 + i); Assert.False(mpe.IsHot); } mpe.Update(100.0, 95.0); Assert.True(mpe.IsHot); } [Fact] public void Reset_ClearsState() { var mpe = new Mpe(10); mpe.Update(100.0, 90.0); mpe.Update(100.0, 95.0); mpe.Reset(); Assert.Equal(0, mpe.Last.Value); Assert.False(mpe.IsHot); } [Fact] public void IsNew_False_UpdatesCurrentBar() { var mpe = new Mpe(5); mpe.Update(100.0, 90.0); double valueBefore = mpe.Last.Value; mpe.Update(100.0, 95.0, isNew: false); double valueAfter = mpe.Last.Value; Assert.NotEqual(valueBefore, valueAfter); } [Fact] public void IterativeCorrections_RestoreToOriginalState() { var mpe = new Mpe(5); var gbm = new GBM(startPrice: 100.0, mu: 0.02, sigma: 0.1); for (int i = 0; i < 10; i++) { var bar = gbm.Next(isNew: true); mpe.Update(bar.Close, bar.Close * 0.95, isNew: true); } double stateAfterTen = mpe.Last.Value; var lastBar = gbm.Next(isNew: false); double lastActual = lastBar.Close; double lastPredicted = lastBar.Close * 0.95; for (int i = 0; i < 5; i++) { var bar = gbm.Next(isNew: false); mpe.Update(bar.Close, bar.Close * 0.9, isNew: false); } mpe.Update(lastActual, lastPredicted, isNew: false); Assert.Equal(stateAfterTen, mpe.Last.Value, 1e-6); } [Fact] public void BatchCalc_MatchesIterativeCalc() { var mpeIterative = new Mpe(10); var gbm = new GBM(startPrice: 100.0, mu: 0.02, sigma: 0.1, seed: 42); var actualSeries = new TSeries(); var predictedSeries = new TSeries(); for (int i = 0; i < 100; i++) { var bar = gbm.Next(isNew: true); actualSeries.Add(bar.Time, bar.Close); predictedSeries.Add(bar.Time, bar.Close * 0.95); } var iterativeResults = new List(); for (int i = 0; i < actualSeries.Count; i++) { iterativeResults.Add(mpeIterative.Update(actualSeries[i], predictedSeries[i]).Value); } var batchResults = Mpe.Batch(actualSeries, predictedSeries, 10); Assert.Equal(iterativeResults.Count, batchResults.Count); for (int i = 0; i < iterativeResults.Count; i++) { Assert.Equal(iterativeResults[i], batchResults[i].Value, Precision); } } [Fact] public void SpanBatch_ValidatesInput() { double[] actual = [100, 100, 100]; double[] predicted = [90, 95, 100]; double[] output = new double[3]; double[] wrongSizeOutput = new double[2]; Assert.Throws(() => Mpe.Batch(actual.AsSpan(), predicted.AsSpan(), wrongSizeOutput.AsSpan(), 3)); Assert.Throws(() => Mpe.Batch(actual.AsSpan(), predicted.AsSpan(), output.AsSpan(), 0)); } [Fact] public void SpanBatch_MatchesTSeriesBatch() { var gbm = new GBM(startPrice: 100.0, mu: 0.02, sigma: 0.1, seed: 42); var actualSeries = new TSeries(); var predictedSeries = new TSeries(); double[] actualArr = new double[100]; double[] predictedArr = new double[100]; double[] output = new double[100]; for (int i = 0; i < 100; i++) { var bar = gbm.Next(isNew: true); actualArr[i] = bar.Close; predictedArr[i] = bar.Close * 0.95; actualSeries.Add(bar.Time, bar.Close); predictedSeries.Add(bar.Time, bar.Close * 0.95); } var tseriesResult = Mpe.Batch(actualSeries, predictedSeries, 10); Mpe.Batch(actualArr.AsSpan(), predictedArr.AsSpan(), output.AsSpan(), 10); for (int i = 0; i < 100; i++) { Assert.Equal(tseriesResult[i].Value, output[i], Precision); } } [Fact] public void SpanBatch_HandlesNaN() { double[] actual = [100, 100, double.NaN, 100, 100]; double[] predicted = [90, 95, 92, double.NaN, 95]; double[] output = new double[5]; Mpe.Batch(actual.AsSpan(), predicted.AsSpan(), output.AsSpan(), 3); foreach (var val in output) { Assert.True(double.IsFinite(val), $"Expected finite value but got {val}"); } } [Fact] public void Calculate_MismatchedLengths_ThrowsException() { var actual = new TSeries(); var predicted = new TSeries(); actual.Add(DateTime.UtcNow.Ticks, 100); actual.Add(DateTime.UtcNow.Ticks + 1, 100); predicted.Add(DateTime.UtcNow.Ticks, 90); Assert.Throws(() => Mpe.Batch(actual, predicted, 5)); } [Fact] public void Name_IsSetCorrectly() { var mpe = new Mpe(14); Assert.Equal("Mpe(14)", mpe.Name); } [Fact] public void WarmupPeriod_IsSetCorrectly() { var mpe = new Mpe(20); Assert.Equal(20, mpe.WarmupPeriod); } [Fact] public void DifferenceFromMape_SignPreserved() { // MPE preserves sign, MAPE takes absolute value var mpe = new Mpe(2); var mape = new Mape(2); // Under-prediction: both should be positive mpe.Update(100.0, 90.0); // +10% mape.Update(100.0, 90.0); // +10% // Over-prediction: MPE negative, MAPE positive mpe.Update(100.0, 110.0); // -10% mape.Update(100.0, 110.0); // +10% // MPE average: (10 + (-10)) / 2 = 0 // MAPE average: (10 + 10) / 2 = 10 Assert.Equal(0.0, mpe.Last.Value, Precision); Assert.Equal(10.0, mape.Last.Value, Precision); } [Fact] public void SlidingWindow_Works() { var mpe = new Mpe(3); mpe.Update(100.0, 90.0); // +10% mpe.Update(100.0, 95.0); // +5% mpe.Update(100.0, 100.0); // 0% // Average: (10 + 5 + 0) / 3 = 5% Assert.Equal(5.0, mpe.Last.Value, Precision); mpe.Update(100.0, 105.0); // -5% // Window now: +5%, 0%, -5% // Average: (5 + 0 + (-5)) / 3 = 0% Assert.Equal(0.0, mpe.Last.Value, Precision); mpe.Update(100.0, 110.0); // -10% // Window now: 0%, -5%, -10% // Average: (0 + (-5) + (-10)) / 3 = -5% Assert.Equal(-5.0, mpe.Last.Value, Precision); } }