v0.8.7: Replace periodic ResyncInterval with Kahan compensated summation

Comprehensive refactor across all indicators replacing the periodic
ResyncInterval-based drift correction (every 1000 ticks recalculate
from scratch) with Kahan compensated summation for running sums.

Key changes:
- Remove ResyncInterval constants and TickCount fields from all State records
- Add Kahan compensation fields (SumComp, SumSqComp, etc.) to State records
- Replace naive sum += val - removed with Kahan delta pattern
- Remove Resync()/RecalculateSum() methods that did O(N) recalculation
- Update batch/SIMD paths to use Kahan compensation instead of resync loops
- IIR filters (EMA, REMA, RGMA) simplified: inherently self-correcting
- Version bump to 0.8.7
- Build system: README version stamping via Directory.Build.props
- Minor doc/test tolerance adjustments for new numerical characteristics

Affected modules: channels, core, cycles, dynamics, errors, momentum,
oscillators, statistics, trends_FIR, trends_IIR, volatility, volume
This commit is contained in:
Miha Kralj
2026-03-13 22:01:31 -07:00
parent c75135ab14
commit 67ad6f0cba
79 changed files with 2923 additions and 2495 deletions
+17 -71
View File
@@ -25,17 +25,11 @@ namespace QuanTAlib;
public sealed class Evwma : ITValuePublisher
{
[StructLayout(LayoutKind.Auto)]
private record struct State(double SumVol, double Result, int Index, int Head, int Count, int SyncCounter)
private record struct State(double SumVol, double SumVolComp, double Result, int Index, int Head, int Count)
{
public static State New() => new() { SumVol = 0, Result = double.NaN, Index = 0, Head = 0, Count = 0, SyncCounter = 0 };
public static State New() => new() { SumVol = 0, SumVolComp = 0, Result = double.NaN, Index = 0, Head = 0, Count = 0 };
}
/// <summary>
/// Resync interval to limit floating-point drift in running volume sum.
/// Full recalculation every N bars.
/// </summary>
private const int ResyncInterval = 1000;
private readonly int _period;
private readonly double[] _volBuffer;
private State _state;
@@ -118,25 +112,6 @@ public sealed class Evwma : ITValuePublisher
return lastValid;
}
/// <summary>
/// Recalculates running volume sum from buffer to eliminate accumulated floating-point drift.
/// </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private void ResyncRunningTotals(ref State s)
{
double sumVol = 0;
for (int i = 0; i < _period; i++)
{
double v = _volBuffer[i];
if (v > 0)
{
sumVol += v;
}
}
s.SumVol = sumVol;
}
[MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)]
public TValue Update(TBar input, bool isNew = true)
@@ -203,6 +178,8 @@ public sealed class Evwma : ITValuePublisher
_lastValidVolume = _p_lastValidVolume;
// Restore buffer value at head position
_volBuffer[s.Head] = _p_bufferVol;
// Reset Kahan compensation on re-entry
s.SumVolComp = 0;
}
// Get valid values
@@ -210,16 +187,13 @@ public sealed class Evwma : ITValuePublisher
double currentVol = GetValidValue(volume, ref _lastValidVolume);
currentVol = Math.Max(0.0, currentVol);
// Remove oldest volume from circular buffer
// Kahan-compensated delta update for SumVol
double oldVol = _volBuffer[s.Head];
if (s.Count >= _period)
{
s.SumVol -= oldVol;
}
// Add current volume to running sum
s.SumVol += currentVol;
double delta = currentVol - (s.Count >= _period ? oldVol : 0);
double y = delta - s.SumVolComp;
double t = s.SumVol + y;
s.SumVolComp = (t - s.SumVol) - y;
s.SumVol = t;
// Store in circular buffer
_volBuffer[s.Head] = currentVol;
@@ -234,14 +208,6 @@ public sealed class Evwma : ITValuePublisher
{
s.Count++;
}
// Periodic resync to limit floating-point drift
s.SyncCounter++;
if (s.SyncCounter >= ResyncInterval && s.Count >= _period)
{
s.SyncCounter = 0;
ResyncRunningTotals(ref s);
}
}
// EVWMA calculation
@@ -384,6 +350,7 @@ public sealed class Evwma : ITValuePublisher
volBuffer.Clear();
double sumVol = 0;
double sumVolComp = 0;
double result = double.NaN;
double lastValidPrice = 0;
double lastValidVolume = 0;
@@ -408,8 +375,6 @@ public sealed class Evwma : ITValuePublisher
}
}
int syncCounter = 0;
for (int i = 0; i < len; i++)
{
// Get valid values with NaN substitution
@@ -426,16 +391,13 @@ public sealed class Evwma : ITValuePublisher
lastValidVolume = volume[i];
}
// Remove oldest volume from circular buffer
// Kahan-compensated delta update for SumVol
double oldVol = volBuffer[head];
if (count >= period)
{
sumVol -= oldVol;
}
// Add current volume to running sum
sumVol += currentVol;
double delta = currentVol - (count >= period ? oldVol : 0);
double y = delta - sumVolComp;
double t = sumVol + y;
sumVolComp = (t - sumVol) - y;
sumVol = t;
// Store in circular buffer
volBuffer[head] = currentVol;
@@ -448,22 +410,6 @@ public sealed class Evwma : ITValuePublisher
count++;
}
// Periodic resync to limit floating-point drift
syncCounter++;
if (syncCounter >= ResyncInterval && count >= period)
{
syncCounter = 0;
sumVol = 0;
for (int j = 0; j < period; j++)
{
double vj = volBuffer[j];
if (vj > 0)
{
sumVol += vj;
}
}
}
// EVWMA calculation
if (double.IsNaN(result))
{
+33 -61
View File
@@ -23,17 +23,11 @@ namespace QuanTAlib;
public sealed class Vwma : ITValuePublisher
{
[StructLayout(LayoutKind.Auto)]
private record struct State(double SumPV, double SumVol, int Index, int Head, int Count, int SyncCounter)
private record struct State(double SumPV, double SumVol, double SumPVComp, double SumVolComp, int Index, int Head, int Count)
{
public static State New() => new() { SumPV = 0, SumVol = 0, Index = 0, Head = 0, Count = 0, SyncCounter = 0 };
public static State New() => new() { SumPV = 0, SumVol = 0, SumPVComp = 0, SumVolComp = 0, Index = 0, Head = 0, Count = 0 };
}
/// <summary>
/// Resync interval to limit floating-point drift in running sums.
/// Full recalculation every N bars.
/// </summary>
private const int ResyncInterval = 1000;
private readonly int _period;
private readonly double[] _priceBuffer;
private readonly double[] _volBuffer;
@@ -222,18 +216,23 @@ public sealed class Vwma : ITValuePublisher
double oldPrice = _priceBuffer[s.Head];
double oldVol = _volBuffer[s.Head];
if (s.Count >= _period && oldVol > 0)
{
s.SumPV = Math.FusedMultiplyAdd(-oldPrice, oldVol, s.SumPV);
s.SumVol -= oldVol;
}
// Compute net deltas for Kahan compensation
double pvRemove = (s.Count >= _period && oldVol > 0) ? oldPrice * oldVol : 0.0;
double pvAdd = currentVol > 0 ? currentPrice * currentVol : 0.0;
double volRemove = (s.Count >= _period && oldVol > 0) ? oldVol : 0.0;
double volAdd = currentVol > 0 ? currentVol : 0.0;
// Add new values
if (currentVol > 0)
{
s.SumPV = Math.FusedMultiplyAdd(currentPrice, currentVol, s.SumPV);
s.SumVol += currentVol;
}
// Kahan compensated SumPV
double pvDelta = pvAdd - pvRemove - s.SumPVComp;
double pvNewSum = s.SumPV + pvDelta;
s.SumPVComp = (pvNewSum - s.SumPV) - pvDelta;
s.SumPV = pvNewSum;
// Kahan compensated SumVol
double volDelta = volAdd - volRemove - s.SumVolComp;
double volNewSum = s.SumVol + volDelta;
s.SumVolComp = (volNewSum - s.SumVol) - volDelta;
s.SumVol = volNewSum;
// Store in circular buffer
_priceBuffer[s.Head] = currentPrice;
@@ -249,14 +248,6 @@ public sealed class Vwma : ITValuePublisher
{
s.Count++;
}
// Periodic resync to limit floating-point drift
s.SyncCounter++;
if (s.SyncCounter >= ResyncInterval && s.Count >= _period)
{
s.SyncCounter = 0;
ResyncRunningTotals(ref s);
}
}
// Calculate VWMA
@@ -389,7 +380,9 @@ public sealed class Vwma : ITValuePublisher
volBuffer.Clear();
double sumPV = 0;
double sumPVComp = 0;
double sumVol = 0;
double sumVolComp = 0;
double lastValidPrice = 0;
double lastValidVolume = 0;
int head = 0;
@@ -413,8 +406,6 @@ public sealed class Vwma : ITValuePublisher
}
}
int syncCounter = 0;
for (int i = 0; i < len; i++)
{
// Get valid values with NaN substitution
@@ -430,22 +421,23 @@ public sealed class Vwma : ITValuePublisher
lastValidVolume = volume[i];
}
// Remove old values from circular buffer
// Kahan-compensated delta updates for SumPV and SumVol
double oldPrice = priceBuffer[head];
double oldVol = volBuffer[head];
if (count >= period && oldVol > 0)
{
sumPV = Math.FusedMultiplyAdd(-oldPrice, oldVol, sumPV);
sumVol -= oldVol;
}
double newPV = currentVol > 0 ? currentPrice * currentVol : 0;
double oldPV = (count >= period && oldVol > 0) ? oldPrice * oldVol : 0;
double deltaPV = newPV - oldPV;
double yPV = deltaPV - sumPVComp;
double tPV = sumPV + yPV;
sumPVComp = (tPV - sumPV) - yPV;
sumPV = tPV;
// Add new values
if (currentVol > 0)
{
sumPV = Math.FusedMultiplyAdd(currentPrice, currentVol, sumPV);
sumVol += currentVol;
}
double deltaVol = (currentVol > 0 ? currentVol : 0) - (count >= period && oldVol > 0 ? oldVol : 0);
double yVol = deltaVol - sumVolComp;
double tVol = sumVol + yVol;
sumVolComp = (tVol - sumVol) - yVol;
sumVol = tVol;
// Store in circular buffer
priceBuffer[head] = currentPrice;
@@ -459,26 +451,6 @@ public sealed class Vwma : ITValuePublisher
count++;
}
// Periodic resync to limit floating-point drift
syncCounter++;
if (syncCounter >= ResyncInterval && count >= period)
{
syncCounter = 0;
// Recalculate sums from buffer
sumPV = 0;
sumVol = 0;
for (int j = 0; j < period; j++)
{
double pj = priceBuffer[j];
double vj = volBuffer[j];
if (vj > 0)
{
sumPV = Math.FusedMultiplyAdd(pj, vj, sumPV);
sumVol += vj;
}
}
}
// Calculate VWMA
output[i] = sumVol > double.Epsilon ? sumPV / sumVol : currentPrice;
}