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
+50 -41
View File
@@ -11,6 +11,8 @@ namespace QuanTAlib;
/// <summary>
/// ZSCORE: Z-Score (also known as STANDARDIZE) — measures how many population
/// standard deviations a value lies from the rolling mean over a lookback window.
/// Uses Kahan compensated summation for numerical stability of the running sum-of-squares,
/// eliminating the need for periodic resynchronization.
/// </summary>
/// <remarks>
/// Key properties:
@@ -29,8 +31,8 @@ public sealed class Zscore : AbstractBase
private double _lastValidValue;
private double _sumSq;
private double _p_sumSq;
private int _updateCount;
private const int ResyncInterval = 1000;
private double _sumSqComp; // Kahan compensation for _sumSq
private double _p_sumSqComp;
public override bool IsHot => _buffer.Count >= _period;
@@ -51,6 +53,8 @@ public sealed class Zscore : AbstractBase
WarmupPeriod = period;
_sumSq = 0.0;
_p_sumSq = 0.0;
_sumSqComp = 0.0;
_p_sumSqComp = 0.0;
_handler = Handle;
}
@@ -81,30 +85,34 @@ public sealed class Zscore : AbstractBase
if (isNew)
{
_p_sumSq = _sumSq;
_p_sumSqComp = _sumSqComp;
_buffer.Snapshot();
}
else
{
_sumSq = _p_sumSq;
_sumSqComp = _p_sumSqComp;
_buffer.Restore();
}
if (_buffer.IsFull)
{
double oldVal = _buffer.Oldest;
_sumSq = Math.FusedMultiplyAdd(-oldVal, oldVal, _sumSq);
// Kahan subtract old²
double y = -(oldVal * oldVal) - _sumSqComp;
double t = _sumSq + y;
_sumSqComp = (t - _sumSq) - y;
_sumSq = t;
}
_buffer.Add(value);
_sumSq = Math.FusedMultiplyAdd(value, value, _sumSq);
if (isNew)
// Kahan add new²
{
_updateCount++;
if (_updateCount % ResyncInterval == 0)
{
Resync();
}
double y = (value * value) - _sumSqComp;
double t = _sumSq + y;
_sumSqComp = (t - _sumSq) - y;
_sumSq = t;
}
double result;
@@ -180,22 +188,11 @@ public sealed class Zscore : AbstractBase
_lastValidValue = 0;
_sumSq = 0.0;
_p_sumSq = 0.0;
_updateCount = 0;
_sumSqComp = 0.0;
_p_sumSqComp = 0.0;
Last = default;
}
private void Resync()
{
var span = _buffer.GetSpan();
double sumSq = 0;
for (int i = 0; i < span.Length; i++)
{
sumSq += span[i] * span[i];
}
_sumSq = sumSq;
_buffer.RecalculateSum();
}
public override void Prime(ReadOnlySpan<double> source, TimeSpan? step = null)
{
TimeSpan interval = step ?? TimeSpan.FromSeconds(1);
@@ -254,6 +251,8 @@ public sealed class Zscore : AbstractBase
double lastValid = 0.0;
double sum = 0.0;
double sumSq = 0.0;
double sumComp = 0.0; // Kahan compensation for sum
double sumSqComp = 0.0; // Kahan compensation for sumSq
for (int i = 0; i < source.Length; i++)
{
@@ -271,34 +270,44 @@ public sealed class Zscore : AbstractBase
if (count == ringSize)
{
double oldVal = ring[head];
sum = Math.FusedMultiplyAdd(-1.0, oldVal, sum + val);
sumSq = Math.FusedMultiplyAdd(-oldVal, oldVal, sumSq);
// Kahan subtract old from sum
double ys = -oldVal - sumComp;
double ts = sum + ys;
sumComp = (ts - sum) - ys;
sum = ts;
// Kahan subtract old² from sumSq
double ysq = -(oldVal * oldVal) - sumSqComp;
double tsq = sumSq + ysq;
sumSqComp = (tsq - sumSq) - ysq;
sumSq = tsq;
}
else
{
count++;
sum += val;
}
ring[head] = val;
sumSq = Math.FusedMultiplyAdd(val, val, sumSq);
// Kahan add val to sum
{
double ys = val - sumComp;
double ts = sum + ys;
sumComp = (ts - sum) - ys;
sum = ts;
}
// Kahan add val² to sumSq
{
double ysq = (val * val) - sumSqComp;
double tsq = sumSq + ysq;
sumSqComp = (tsq - sumSq) - ysq;
sumSq = tsq;
}
head = (head + 1) % ringSize;
if ((i + 1) % 1000 == 0 && count == ringSize)
{
double resyncSum = 0;
double resyncSumSq = 0;
for (int j = 0; j < ringSize; j++)
{
double v = ring[j];
resyncSum += v;
resyncSumSq += v * v;
}
sum = resyncSum;
sumSq = resyncSumSq;
}
if (count < 2)
{
output[i] = 0.0;
+3 -3
View File
@@ -256,9 +256,9 @@ public class ZscoreTests
for (int i = 0; i < count; i++)
{
Assert.Equal(batchResult[i].Value, streamResult[i], 1e-9);
Assert.Equal(batchResult[i].Value, spanOutput[i], 1e-8); // FP addition order differs between ring scan paths
Assert.Equal(batchResult[i].Value, eventResult[i], 1e-9);
Assert.Equal(batchResult[i].Value, streamResult[i], 1e-8);
Assert.Equal(batchResult[i].Value, spanOutput[i], 1e-7); // FP addition order differs between ring scan paths
Assert.Equal(batchResult[i].Value, eventResult[i], 1e-8);
}
}