Merge dev into main: v0.8.7 Kahan compensated summation

This commit is contained in:
Miha Kralj
2026-03-13 22:01:52 -07:00
79 changed files with 2923 additions and 2495 deletions
+25 -36
View File
@@ -21,6 +21,8 @@ namespace QuanTAlib;
/// - MASE = 1 means same as naive forecast
/// - MASE > 1 means worse than naive forecast
/// - Robust to zero actual values (unlike MAPE)
///
/// Uses Kahan compensated summation to prevent floating-point drift without periodic resync.
/// </remarks>
[SkipLocalsInit]
public sealed class Mase : AbstractBase
@@ -32,6 +34,8 @@ public sealed class Mase : AbstractBase
private record struct State(
double ErrorSum,
double ScaleSum,
double ErrorComp,
double ScaleComp,
double LastValidActual,
double LastValidPredicted,
double PrevActual,
@@ -39,8 +43,6 @@ public sealed class Mase : AbstractBase
private State _state;
private State _p_state;
private const int ResyncInterval = 1000;
public Mase(int period)
{
if (period <= 0)
@@ -50,8 +52,8 @@ public sealed class Mase : AbstractBase
_errorBuffer = new RingBuffer(period);
_scaleBuffer = new RingBuffer(period);
_state = new State(0, 0, 0, 0, double.NaN, 0);
_p_state = new State(0, 0, 0, 0, double.NaN, 0);
_state = new State(0, 0, 0, 0, 0, 0, double.NaN, 0);
_p_state = new State(0, 0, 0, 0, 0, 0, double.NaN, 0);
Name = $"Mase({period})";
WarmupPeriod = period + 1; // Need one extra for scale calculation
}
@@ -89,34 +91,36 @@ public sealed class Mase : AbstractBase
{
_p_state = _state;
// Update error buffer
// Update error buffer — Kahan compensated
double removedError = _errorBuffer.Count == _errorBuffer.Capacity ? _errorBuffer.Oldest : 0.0;
_state.ErrorSum = _state.ErrorSum - removedError + absError;
{
double delta = absError - removedError;
double y = delta - _state.ErrorComp;
double t = _state.ErrorSum + y;
_state.ErrorComp = (t - _state.ErrorSum) - y;
_state.ErrorSum = t;
}
_errorBuffer.Add(absError);
// Update scale buffer
// Update scale buffer — Kahan compensated
double removedScale = _scaleBuffer.Count == _scaleBuffer.Capacity ? _scaleBuffer.Oldest : 0.0;
_state.ScaleSum = _state.ScaleSum - removedScale + naiveDiff;
{
double delta = naiveDiff - removedScale;
double y = delta - _state.ScaleComp;
double t = _state.ScaleSum + y;
_state.ScaleComp = (t - _state.ScaleSum) - y;
_state.ScaleSum = t;
}
_scaleBuffer.Add(naiveDiff);
_state.PrevActual = actualVal;
_state.TickCount++;
if (_state.TickCount >= ResyncInterval)
{
// Keep TickCount > period to maintain post-warmup state
_state.TickCount = _errorBuffer.Capacity + 1;
_state.ErrorSum = _errorBuffer.RecalculateSum();
_state.ScaleSum = _scaleBuffer.RecalculateSum();
}
}
else
{
_state = _p_state;
// Bar correction: update buffer and recalculate sums
// Note: _p_state was saved BEFORE the Add, but buffer still has the added value
// So we update newest and recalculate to ensure consistency
_errorBuffer.UpdateNewest(absError);
_scaleBuffer.UpdateNewest(naiveDiff);
@@ -174,8 +178,8 @@ public sealed class Mase : AbstractBase
{
_errorBuffer.Clear();
_scaleBuffer.Clear();
_state = new State(0, 0, 0, 0, double.NaN, 0);
_p_state = new State(0, 0, 0, 0, double.NaN, 0);
_state = new State(0, 0, 0, 0, 0, 0, double.NaN, 0);
_p_state = new State(0, 0, 0, 0, 0, 0, double.NaN, 0);
Last = default;
}
@@ -293,7 +297,6 @@ public sealed class Mase : AbstractBase
prevActual = act;
}
int tickCount = 0;
for (; i < len; i++)
{
double act = actual[i];
@@ -336,20 +339,6 @@ public sealed class Mase : AbstractBase
output[i] = scale > 1e-10 ? mae / scale : mae;
prevActual = act;
tickCount++;
if (tickCount >= ResyncInterval)
{
tickCount = 0;
double recalcError = 0, recalcScale = 0;
for (int k = 0; k < period; k++)
{
recalcError += errorBuffer[k];
recalcScale += scaleBuffer[k];
}
errorSum = recalcError;
scaleSum = recalcScale;
}
}
}
@@ -359,4 +348,4 @@ public sealed class Mase : AbstractBase
TSeries results = Batch(actual, predicted, period);
return (results, indicator);
}
}
}
-14
View File
@@ -168,7 +168,6 @@ public sealed class QuantileLoss : BiInputIndicatorBase
output[i] = lossSum / (i + 1);
}
int tickCount = 0;
for (; i < len; i++)
{
double act = actual[i];
@@ -205,19 +204,6 @@ public sealed class QuantileLoss : BiInputIndicatorBase
}
output[i] = lossSum / period;
tickCount++;
if (tickCount >= ResyncInterval)
{
tickCount = 0;
double recalcSum = 0;
for (int k = 0; k < period; k++)
{
recalcSum += lossBuffer[k];
}
lossSum = recalcSum;
}
}
}
+30 -36
View File
@@ -19,6 +19,8 @@ namespace QuanTAlib;
/// - RAE = 1 means same as mean predictor
/// - RAE &gt; 1 means worse than mean predictor
/// - Scale-independent ratio
///
/// Uses Kahan compensated summation to prevent floating-point drift without periodic resync.
/// </remarks>
[SkipLocalsInit]
public sealed class Rae : AbstractBase
@@ -32,14 +34,14 @@ public sealed class Rae : AbstractBase
double ActualSum,
double AbsErrorSum,
double AbsBaselineSum,
double ActualComp,
double AbsErrorComp,
double AbsBaselineComp,
double LastValidActual,
double LastValidPredicted,
int TickCount);
double LastValidPredicted);
private State _state;
private State _p_state;
private const int ResyncInterval = 1000;
public Rae(int period)
{
if (period <= 0)
@@ -93,9 +95,15 @@ public sealed class Rae : AbstractBase
if (isNew)
{
// Update actual buffer for mean calculation
// Update actual buffer for mean calculation — Kahan compensated
double removedActual = _actualBuffer.Count == _actualBuffer.Capacity ? _actualBuffer.Oldest : 0.0;
_state.ActualSum = _state.ActualSum - removedActual + actualVal;
{
double delta = actualVal - removedActual;
double y = delta - _state.ActualComp;
double t = _state.ActualSum + y;
_state.ActualComp = (t - _state.ActualSum) - y;
_state.ActualSum = t;
}
_actualBuffer.Add(actualVal);
// Calculate mean and baseline error
@@ -103,24 +111,27 @@ public sealed class Rae : AbstractBase
double absError = Math.Abs(actualVal - predictedVal);
double absBaseline = Math.Abs(actualVal - mean);
// Update error buffer
// Update error buffer — Kahan compensated
double removedError = _absErrorBuffer.Count == _absErrorBuffer.Capacity ? _absErrorBuffer.Oldest : 0.0;
_state.AbsErrorSum = _state.AbsErrorSum - removedError + absError;
{
double delta = absError - removedError;
double y = delta - _state.AbsErrorComp;
double t = _state.AbsErrorSum + y;
_state.AbsErrorComp = (t - _state.AbsErrorSum) - y;
_state.AbsErrorSum = t;
}
_absErrorBuffer.Add(absError);
// Update baseline buffer
// Update baseline buffer — Kahan compensated
double removedBaseline = _absBaselineBuffer.Count == _absBaselineBuffer.Capacity ? _absBaselineBuffer.Oldest : 0.0;
_state.AbsBaselineSum = _state.AbsBaselineSum - removedBaseline + absBaseline;
_absBaselineBuffer.Add(absBaseline);
_state.TickCount++;
if (_actualBuffer.IsFull && _state.TickCount >= ResyncInterval)
{
_state.TickCount = 0;
_state.ActualSum = _actualBuffer.RecalculateSum();
_state.AbsErrorSum = _absErrorBuffer.RecalculateSum();
_state.AbsBaselineSum = _absBaselineBuffer.RecalculateSum();
double delta = absBaseline - removedBaseline;
double y = delta - _state.AbsBaselineComp;
double t = _state.AbsBaselineSum + y;
_state.AbsBaselineComp = (t - _state.AbsBaselineSum) - y;
_state.AbsBaselineSum = t;
}
_absBaselineBuffer.Add(absBaseline);
}
else
{
@@ -294,7 +305,6 @@ public sealed class Rae : AbstractBase
output[i] = absBaselineSum > 1e-10 ? absErrorSum / absBaselineSum : 1.0;
}
int tickCount = 0;
for (; i < len; i++)
{
double act = actual[i];
@@ -337,22 +347,6 @@ public sealed class Rae : AbstractBase
}
output[i] = absBaselineSum > 1e-10 ? absErrorSum / absBaselineSum : 1.0;
tickCount++;
if (tickCount >= ResyncInterval)
{
tickCount = 0;
double recalcActual = 0, recalcError = 0, recalcBaseline = 0;
for (int k = 0; k < period; k++)
{
recalcActual += actualBuffer[k];
recalcError += absErrorBuffer[k];
recalcBaseline += absBaselineBuffer[k];
}
actualSum = recalcActual;
absErrorSum = recalcError;
absBaselineSum = recalcBaseline;
}
}
}
@@ -362,4 +356,4 @@ public sealed class Rae : AbstractBase
TSeries results = Batch(actual, predicted, period);
return (results, indicator);
}
}
}
+30 -36
View File
@@ -19,6 +19,8 @@ namespace QuanTAlib;
/// - RSE = 1 means same as mean predictor
/// - RSE &gt; 1 means worse than mean predictor
/// - Related to R² by: R² = 1 - RSE
///
/// Uses Kahan compensated summation to prevent floating-point drift without periodic resync.
/// </remarks>
[SkipLocalsInit]
public sealed class Rse : AbstractBase
@@ -32,14 +34,14 @@ public sealed class Rse : AbstractBase
double ActualSum,
double SqErrorSum,
double SqBaselineSum,
double ActualComp,
double SqErrorComp,
double SqBaselineComp,
double LastValidActual,
double LastValidPredicted,
int TickCount);
double LastValidPredicted);
private State _state;
private State _p_state;
private const int ResyncInterval = 1000;
public Rse(int period)
{
if (period <= 0)
@@ -90,9 +92,15 @@ public sealed class Rse : AbstractBase
{
_p_state = _state;
// Update actual buffer for mean calculation
// Update actual buffer for mean calculation — Kahan compensated
double removedActual = _actualBuffer.Count == _actualBuffer.Capacity ? _actualBuffer.Oldest : 0.0;
_state.ActualSum = _state.ActualSum - removedActual + actualVal;
{
double delta = actualVal - removedActual;
double y = delta - _state.ActualComp;
double t = _state.ActualSum + y;
_state.ActualComp = (t - _state.ActualSum) - y;
_state.ActualSum = t;
}
_actualBuffer.Add(actualVal);
// Calculate mean and baseline error
@@ -102,24 +110,27 @@ public sealed class Rse : AbstractBase
double sqError = error * error;
double sqBaseline = baselineError * baselineError;
// Update squared error buffer
// Update squared error buffer — Kahan compensated
double removedError = _sqErrorBuffer.Count == _sqErrorBuffer.Capacity ? _sqErrorBuffer.Oldest : 0.0;
_state.SqErrorSum = _state.SqErrorSum - removedError + sqError;
{
double delta = sqError - removedError;
double y = delta - _state.SqErrorComp;
double t = _state.SqErrorSum + y;
_state.SqErrorComp = (t - _state.SqErrorSum) - y;
_state.SqErrorSum = t;
}
_sqErrorBuffer.Add(sqError);
// Update squared baseline buffer
// Update squared baseline buffer — Kahan compensated
double removedBaseline = _sqBaselineBuffer.Count == _sqBaselineBuffer.Capacity ? _sqBaselineBuffer.Oldest : 0.0;
_state.SqBaselineSum = _state.SqBaselineSum - removedBaseline + sqBaseline;
_sqBaselineBuffer.Add(sqBaseline);
_state.TickCount++;
if (_actualBuffer.IsFull && _state.TickCount >= ResyncInterval)
{
_state.TickCount = 0;
_state.ActualSum = _actualBuffer.RecalculateSum();
_state.SqErrorSum = _sqErrorBuffer.RecalculateSum();
_state.SqBaselineSum = _sqBaselineBuffer.RecalculateSum();
double delta = sqBaseline - removedBaseline;
double y = delta - _state.SqBaselineComp;
double t = _state.SqBaselineSum + y;
_state.SqBaselineComp = (t - _state.SqBaselineSum) - y;
_state.SqBaselineSum = t;
}
_sqBaselineBuffer.Add(sqBaseline);
}
else
{
@@ -303,7 +314,6 @@ public sealed class Rse : AbstractBase
output[i] = sqBaselineSum > 1e-10 ? sqErrorSum / sqBaselineSum : 1.0;
}
int tickCount = 0;
for (; i < len; i++)
{
double act = actual[i];
@@ -348,22 +358,6 @@ public sealed class Rse : AbstractBase
}
output[i] = sqBaselineSum > 1e-10 ? sqErrorSum / sqBaselineSum : 1.0;
tickCount++;
if (tickCount >= ResyncInterval)
{
tickCount = 0;
double recalcActual = 0, recalcError = 0, recalcBaseline = 0;
for (int k = 0; k < period; k++)
{
recalcActual += actualBuffer[k];
recalcError += sqErrorBuffer[k];
recalcBaseline += sqBaselineBuffer[k];
}
actualSum = recalcActual;
sqErrorSum = recalcError;
sqBaselineSum = recalcBaseline;
}
}
}
@@ -373,4 +367,4 @@ public sealed class Rse : AbstractBase
TSeries results = Batch(actual, predicted, period);
return (results, indicator);
}
}
}
+30 -36
View File
@@ -20,6 +20,8 @@ namespace QuanTAlib;
/// - R² = 0 means predictions equal mean predictor
/// - R² &lt; 0 means predictions worse than mean predictor
/// - Range: (-∞, 1]
///
/// Uses Kahan compensated summation to prevent floating-point drift without periodic resync.
/// </remarks>
[SkipLocalsInit]
public sealed class Rsquared : AbstractBase
@@ -33,14 +35,14 @@ public sealed class Rsquared : AbstractBase
double ActualSum,
double SqResidualSum,
double SqTotalSum,
double ActualComp,
double SqResidualComp,
double SqTotalComp,
double LastValidActual,
double LastValidPredicted,
int TickCount);
double LastValidPredicted);
private State _state;
private State _p_state;
private const int ResyncInterval = 1000;
public Rsquared(int period)
{
if (period <= 0)
@@ -85,9 +87,15 @@ public sealed class Rsquared : AbstractBase
{
_p_state = _state;
// Update actual buffer for mean calculation
// Update actual buffer for mean calculation — Kahan compensated
double removedActual = _actualBuffer.Count == _actualBuffer.Capacity ? _actualBuffer.Oldest : 0.0;
_state.ActualSum = _state.ActualSum - removedActual + actualVal;
{
double delta = actualVal - removedActual;
double y = delta - _state.ActualComp;
double t = _state.ActualSum + y;
_state.ActualComp = (t - _state.ActualSum) - y;
_state.ActualSum = t;
}
_actualBuffer.Add(actualVal);
// Calculate mean and errors
@@ -97,24 +105,27 @@ public sealed class Rsquared : AbstractBase
double sqResidual = residual * residual;
double sqTotal = totalDev * totalDev;
// Update squared residual buffer (RSS)
// Update squared residual buffer (RSS) — Kahan compensated
double removedResidual = _sqResidualBuffer.Count == _sqResidualBuffer.Capacity ? _sqResidualBuffer.Oldest : 0.0;
_state.SqResidualSum = _state.SqResidualSum - removedResidual + sqResidual;
{
double delta = sqResidual - removedResidual;
double y = delta - _state.SqResidualComp;
double t = _state.SqResidualSum + y;
_state.SqResidualComp = (t - _state.SqResidualSum) - y;
_state.SqResidualSum = t;
}
_sqResidualBuffer.Add(sqResidual);
// Update squared total buffer (TSS)
// Update squared total buffer (TSS) — Kahan compensated
double removedTotal = _sqTotalBuffer.Count == _sqTotalBuffer.Capacity ? _sqTotalBuffer.Oldest : 0.0;
_state.SqTotalSum = _state.SqTotalSum - removedTotal + sqTotal;
_sqTotalBuffer.Add(sqTotal);
_state.TickCount++;
if (_actualBuffer.IsFull && _state.TickCount >= ResyncInterval)
{
_state.TickCount = 0;
_state.ActualSum = _actualBuffer.RecalculateSum();
_state.SqResidualSum = _sqResidualBuffer.RecalculateSum();
_state.SqTotalSum = _sqTotalBuffer.RecalculateSum();
double delta = sqTotal - removedTotal;
double y = delta - _state.SqTotalComp;
double t = _state.SqTotalSum + y;
_state.SqTotalComp = (t - _state.SqTotalSum) - y;
_state.SqTotalSum = t;
}
_sqTotalBuffer.Add(sqTotal);
}
else
{
@@ -297,7 +308,6 @@ public sealed class Rsquared : AbstractBase
output[i] = sqTotalSum > 1e-10 ? 1.0 - (sqResidualSum / sqTotalSum) : 1.0;
}
int tickCount = 0;
for (; i < len; i++)
{
double act = actual[i];
@@ -342,22 +352,6 @@ public sealed class Rsquared : AbstractBase
}
output[i] = sqTotalSum > 1e-10 ? 1.0 - (sqResidualSum / sqTotalSum) : 1.0;
tickCount++;
if (tickCount >= ResyncInterval)
{
tickCount = 0;
double recalcActual = 0, recalcResidual = 0, recalcTotal = 0;
for (int k = 0; k < period; k++)
{
recalcActual += actualBuffer[k];
recalcResidual += sqResidualBuffer[k];
recalcTotal += sqTotalBuffer[k];
}
actualSum = recalcActual;
sqResidualSum = recalcResidual;
sqTotalSum = recalcTotal;
}
}
}
@@ -367,4 +361,4 @@ public sealed class Rsquared : AbstractBase
TSeries results = Batch(actual, predicted, period);
return (results, indicator);
}
}
}
+24 -35
View File
@@ -21,6 +21,8 @@ namespace QuanTAlib;
/// - U = 1: Forecast as good as naive (no-change) forecast
/// - U > 1: Forecast worse than naive forecast
/// - Useful for comparing forecasting methods
///
/// Uses Kahan compensated summation to prevent floating-point drift without periodic resync.
/// </remarks>
[SkipLocalsInit]
public sealed class TheilU : AbstractBase
@@ -30,12 +32,10 @@ public sealed class TheilU : AbstractBase
private readonly RingBuffer _sqPredBuffer;
[StructLayout(LayoutKind.Auto)]
private record struct State(double SqErrorSum, double SqActualSum, double SqPredSum, double LastValidActual, double LastValidPredicted, int TickCount);
private record struct State(double SqErrorSum, double SqActualSum, double SqPredSum, double SqErrorComp, double SqActualComp, double SqPredComp, double LastValidActual, double LastValidPredicted);
private State _state;
private State _p_state;
private const int ResyncInterval = 1000;
public TheilU(int period)
{
if (period <= 0)
@@ -109,34 +109,40 @@ public sealed class TheilU : AbstractBase
_p_state = _state;
double removedSqError = _sqErrorBuffer.Count == _sqErrorBuffer.Capacity ? _sqErrorBuffer.Oldest : 0.0;
// Use FMA: sum = sum - removed + new = FMA(1.0, new, FMA(-1.0, removed, sum))
_state.SqErrorSum = Math.FusedMultiplyAdd(1.0, sqError, Math.FusedMultiplyAdd(-1.0, removedSqError, _state.SqErrorSum));
{
double delta = sqError - removedSqError;
double y = delta - _state.SqErrorComp;
double t = _state.SqErrorSum + y;
_state.SqErrorComp = (t - _state.SqErrorSum) - y;
_state.SqErrorSum = t;
}
_sqErrorBuffer.Add(sqError);
double removedSqActual = _sqActualBuffer.Count == _sqActualBuffer.Capacity ? _sqActualBuffer.Oldest : 0.0;
_state.SqActualSum = Math.FusedMultiplyAdd(1.0, sqActual, Math.FusedMultiplyAdd(-1.0, removedSqActual, _state.SqActualSum));
{
double delta = sqActual - removedSqActual;
double y = delta - _state.SqActualComp;
double t = _state.SqActualSum + y;
_state.SqActualComp = (t - _state.SqActualSum) - y;
_state.SqActualSum = t;
}
_sqActualBuffer.Add(sqActual);
double removedSqPred = _sqPredBuffer.Count == _sqPredBuffer.Capacity ? _sqPredBuffer.Oldest : 0.0;
_state.SqPredSum = Math.FusedMultiplyAdd(1.0, sqPred, Math.FusedMultiplyAdd(-1.0, removedSqPred, _state.SqPredSum));
_sqPredBuffer.Add(sqPred);
_state.TickCount++;
if (_sqErrorBuffer.IsFull && _state.TickCount >= ResyncInterval)
{
_state.TickCount = 0;
_state.SqErrorSum = _sqErrorBuffer.RecalculateSum();
_state.SqActualSum = _sqActualBuffer.RecalculateSum();
_state.SqPredSum = _sqPredBuffer.RecalculateSum();
double delta = sqPred - removedSqPred;
double y = delta - _state.SqPredComp;
double t = _state.SqPredSum + y;
_state.SqPredComp = (t - _state.SqPredSum) - y;
_state.SqPredSum = t;
}
_sqPredBuffer.Add(sqPred);
}
else
{
_state = _p_state;
// Bar correction: update buffer and recalculate sums
// Note: _p_state was saved BEFORE the Add, but buffer still has the added value
// So we update newest and recalculate to ensure consistency
_sqErrorBuffer.UpdateNewest(sqError);
_sqActualBuffer.UpdateNewest(sqActual);
_sqPredBuffer.UpdateNewest(sqPred);
@@ -288,7 +294,6 @@ public sealed class TheilU : AbstractBase
output[i] = denom > 1e-10 ? Math.Sqrt(sqErrorSum) / denom : 0.0;
}
int tickCount = 0;
for (; i < len; i++)
{
double act = actual[i];
@@ -334,22 +339,6 @@ public sealed class TheilU : AbstractBase
double denom = Math.Sqrt(sqActualSum + sqPredSum);
output[i] = denom > 1e-10 ? Math.Sqrt(sqErrorSum) / denom : 0.0;
tickCount++;
if (tickCount >= ResyncInterval)
{
tickCount = 0;
double recalcSqError = 0, recalcSqActual = 0, recalcSqPred = 0;
for (int k = 0; k < period; k++)
{
recalcSqError += sqErrorBuffer[k];
recalcSqActual += sqActualBuffer[k];
recalcSqPred += sqPredBuffer[k];
}
sqErrorSum = recalcSqError;
sqActualSum = recalcSqActual;
sqPredSum = recalcSqPred;
}
}
}
@@ -359,4 +348,4 @@ public sealed class TheilU : AbstractBase
TSeries results = Batch(actual, predicted, period);
return (results, indicator);
}
}
}
+1 -2
View File
@@ -29,7 +29,6 @@ public sealed class TukeyBiweight : BiInputIndicatorBase
{
private readonly double _cSquaredOver6;
private const double DefaultC = 4.685; // 95% efficiency for normal distribution
private const int BatchResyncInterval = 1000; // Local constant for static Batch method
public TukeyBiweight(int period, double c = DefaultC)
: base(period, $"TukeyBiweight({period},{c:F3})")
@@ -122,7 +121,7 @@ public sealed class TukeyBiweight : BiInputIndicatorBase
ErrorHelpers.ComputeTukeyBiweightErrors(actual, predicted, errors, c);
// Step 2: Apply rolling mean
ErrorHelpers.ApplyRollingMean(errors, output, period, BatchResyncInterval);
ErrorHelpers.ApplyRollingMean(errors, output, period);
}
finally
{
+17 -29
View File
@@ -20,6 +20,8 @@ namespace QuanTAlib;
/// - Weights larger actual values more heavily
/// - More stable than MAPE for intermittent data
/// - Industry standard for demand forecasting
///
/// Uses Kahan compensated summation to prevent floating-point drift without periodic resync.
/// </remarks>
[SkipLocalsInit]
public sealed class Wmape : AbstractBase
@@ -28,11 +30,10 @@ public sealed class Wmape : AbstractBase
private readonly RingBuffer _absActualBuffer;
[StructLayout(LayoutKind.Auto)]
private record struct State(double AbsErrorSum, double AbsActualSum, double LastValidActual, double LastValidPredicted, int TickCount);
private record struct State(double AbsErrorSum, double AbsActualSum, double AbsErrorComp, double AbsActualComp, double LastValidActual, double LastValidPredicted);
private State _state;
private State _p_state;
private const int ResyncInterval = 1000;
private const int StackAllocThreshold = 256;
public Wmape(int period)
@@ -90,26 +91,28 @@ public sealed class Wmape : AbstractBase
if (isNew)
{
double removedError = _absErrorBuffer.Count == _absErrorBuffer.Capacity ? _absErrorBuffer.Oldest : 0.0;
_state.AbsErrorSum = _state.AbsErrorSum - removedError + absError;
{
double delta = absError - removedError;
double y = delta - _state.AbsErrorComp;
double t = _state.AbsErrorSum + y;
_state.AbsErrorComp = (t - _state.AbsErrorSum) - y;
_state.AbsErrorSum = t;
}
_absErrorBuffer.Add(absError);
double removedActual = _absActualBuffer.Count == _absActualBuffer.Capacity ? _absActualBuffer.Oldest : 0.0;
_state.AbsActualSum = _state.AbsActualSum - removedActual + absActual;
_absActualBuffer.Add(absActual);
_state.TickCount++;
if (_absErrorBuffer.IsFull && _state.TickCount >= ResyncInterval)
{
_state.TickCount = 0;
_state.AbsErrorSum = _absErrorBuffer.RecalculateSum();
_state.AbsActualSum = _absActualBuffer.RecalculateSum();
double delta = absActual - removedActual;
double y = delta - _state.AbsActualComp;
double t = _state.AbsActualSum + y;
_state.AbsActualComp = (t - _state.AbsActualSum) - y;
_state.AbsActualSum = t;
}
_absActualBuffer.Add(absActual);
}
else
{
// Bar correction: update buffer and recalculate sums
// Note: _p_state was saved BEFORE the Add, but buffer still has the added value
// So we update newest and recalculate to ensure consistency
_absErrorBuffer.UpdateNewest(absError);
_absActualBuffer.UpdateNewest(absActual);
@@ -277,7 +280,6 @@ public sealed class Wmape : AbstractBase
output[i] = absActualSum > 1e-10 ? (absErrorSum / absActualSum) * 100.0 : 0.0;
}
int tickCount = 0;
for (; i < len; i++)
{
double act = actual[i];
@@ -316,20 +318,6 @@ public sealed class Wmape : AbstractBase
}
output[i] = absActualSum > 1e-10 ? (absErrorSum / absActualSum) * 100.0 : 0.0;
tickCount++;
if (tickCount >= ResyncInterval)
{
tickCount = 0;
double recalcError = 0, recalcActual = 0;
for (int k = 0; k < period; k++)
{
recalcError += absErrorBuffer[k];
recalcActual += absActualBuffer[k];
}
absErrorSum = recalcError;
absActualSum = recalcActual;
}
}
}
finally
@@ -352,4 +340,4 @@ public sealed class Wmape : AbstractBase
TSeries results = Batch(actual, predicted, period);
return (results, indicator);
}
}
}
+20 -14
View File
@@ -14,7 +14,7 @@ namespace QuanTAlib;
/// Formula:
/// WRMSE = √(Σ(w_i * (actual_i - predicted_i)²) / Σ(w_i))
///
/// Uses dual RingBuffers for O(1) streaming updates with running sums.
/// Uses dual RingBuffers for O(1) streaming updates with Kahan compensated running sums.
///
/// Key properties:
/// - Always non-negative (WRMSE ≥ 0)
@@ -22,6 +22,8 @@ namespace QuanTAlib;
/// - Weights allow emphasizing important observations
/// - Reduces to RMSE when all weights are equal
/// - WRMSE = 0 indicates perfect prediction
///
/// Kahan compensated summation prevents floating-point drift without periodic resync.
/// </remarks>
[SkipLocalsInit]
public sealed class Wrmse : AbstractBase
@@ -33,14 +35,14 @@ public sealed class Wrmse : AbstractBase
private record struct State(
double WeightedErrorSum,
double WeightSum,
double WeightedErrorComp,
double WeightComp,
double LastValidActual,
double LastValidPredicted,
double LastValidWeight,
int TickCount);
double LastValidWeight);
private State _state;
private State _p_state;
private const int ResyncInterval = 1000;
private const double DefaultWeight = 1.0;
/// <summary>
@@ -124,21 +126,25 @@ public sealed class Wrmse : AbstractBase
double removedWeightedError = _weightedErrorBuffer.Count == _weightedErrorBuffer.Capacity
? _weightedErrorBuffer.Oldest : 0.0;
_state.WeightedErrorSum = _state.WeightedErrorSum - removedWeightedError + weightedError;
{
double delta = weightedError - removedWeightedError;
double y = delta - _state.WeightedErrorComp;
double t = _state.WeightedErrorSum + y;
_state.WeightedErrorComp = (t - _state.WeightedErrorSum) - y;
_state.WeightedErrorSum = t;
}
_weightedErrorBuffer.Add(weightedError);
double removedWeight = _weightBuffer.Count == _weightBuffer.Capacity
? _weightBuffer.Oldest : 0.0;
_state.WeightSum = _state.WeightSum - removedWeight + weight;
_weightBuffer.Add(weight);
_state.TickCount++;
if (_weightedErrorBuffer.IsFull && _state.TickCount >= ResyncInterval)
{
_state.TickCount = 0;
_state.WeightedErrorSum = _weightedErrorBuffer.RecalculateSum();
_state.WeightSum = _weightBuffer.RecalculateSum();
double delta = weight - removedWeight;
double y = delta - _state.WeightComp;
double t = _state.WeightSum + y;
_state.WeightComp = (t - _state.WeightSum) - y;
_state.WeightSum = t;
}
_weightBuffer.Add(weight);
}
else
{
@@ -332,4 +338,4 @@ public sealed class Wrmse : AbstractBase
TSeries results = Batch(actual, predicted, period);
return (results, indicator);
}
}
}