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
+31 -44
View File
@@ -34,18 +34,17 @@ public sealed class Ghla : AbstractBase
private record struct State(
double HighSum,
double LowSum,
double HighSumComp,
double LowSumComp,
int Trend,
double LastValidHigh,
double LastValidLow,
double LastValidClose,
int TickCount
double LastValidClose
);
private State _s;
private State _ps;
private const int ResyncInterval = 1000;
/// <summary>
/// Creates GHLA with specified SMA period.
/// </summary>
@@ -61,7 +60,7 @@ public sealed class Ghla : AbstractBase
_lowBuffer = new RingBuffer(period);
Name = $"Ghla({period})";
WarmupPeriod = period;
_s = new State(0, 0, 0, 0, 0, 0, 0);
_s = default;
_ps = _s;
}
@@ -178,7 +177,7 @@ public sealed class Ghla : AbstractBase
{
_highBuffer.Clear();
_lowBuffer.Clear();
_s = new State(0, 0, 0, 0, 0, 0, 0);
_s = default;
_ps = _s;
Last = default;
}
@@ -291,33 +290,32 @@ public sealed class Ghla : AbstractBase
// Update running SMA sums via ring buffers
if (isNew)
{
// High buffer
// High buffer — Kahan compensated
double highRemoved = _highBuffer.Count == _highBuffer.Capacity ? _highBuffer.Oldest : 0.0;
s.HighSum = s.HighSum - highRemoved + high;
double hDelta = high - highRemoved - s.HighSumComp;
double hNewSum = s.HighSum + hDelta;
s.HighSumComp = (hNewSum - s.HighSum) - hDelta;
s.HighSum = hNewSum;
_highBuffer.Add(high);
// Low buffer
// Low buffer — Kahan compensated
double lowRemoved = _lowBuffer.Count == _lowBuffer.Capacity ? _lowBuffer.Oldest : 0.0;
s.LowSum = s.LowSum - lowRemoved + low;
double lDelta = low - lowRemoved - s.LowSumComp;
double lNewSum = s.LowSum + lDelta;
s.LowSumComp = (lNewSum - s.LowSum) - lDelta;
s.LowSum = lNewSum;
_lowBuffer.Add(low);
// Periodic resync to limit floating-point drift
s.TickCount++;
if (_highBuffer.IsFull && s.TickCount >= ResyncInterval)
{
s.TickCount = 0;
s.HighSum = _highBuffer.RecalculateSum();
s.LowSum = _lowBuffer.RecalculateSum();
}
}
else
{
// Bar correction: update newest value in both buffers
_highBuffer.UpdateNewest(high);
s.HighSum = _highBuffer.Sum;
s.HighSumComp = 0;
_lowBuffer.UpdateNewest(low);
s.LowSum = _lowBuffer.Sum;
s.LowSumComp = 0;
}
// Compute SMAs
@@ -390,7 +388,9 @@ public sealed class Ghla : AbstractBase
try
{
double highSum = 0;
double highSumComp = 0;
double lowSum = 0;
double lowSumComp = 0;
double lastValidHigh = 0;
double lastValidLow = 0;
double lastValidClose = 0;
@@ -398,7 +398,6 @@ public sealed class Ghla : AbstractBase
int lowIdx = 0;
int filled = 0;
int trend = 0;
int tickCount = 0;
// Seed lastValid values
for (int k = 0; k < len; k++)
@@ -459,12 +458,14 @@ public sealed class Ghla : AbstractBase
c = lastValidClose;
}
// Update high buffer
if (filled >= period)
// Kahan-compensated update for high buffer
{
highSum -= highBuf[highIdx];
double deltaH = h - (filled >= period ? highBuf[highIdx] : 0);
double yH = deltaH - highSumComp;
double tH = highSum + yH;
highSumComp = (tH - highSum) - yH;
highSum = tH;
}
highSum += h;
highBuf[highIdx] = h;
highIdx++;
if (highIdx >= period)
@@ -472,12 +473,14 @@ public sealed class Ghla : AbstractBase
highIdx = 0;
}
// Update low buffer
if (filled >= period)
// Kahan-compensated update for low buffer
{
lowSum -= lowBuf[lowIdx];
double deltaL = l - (filled >= period ? lowBuf[lowIdx] : 0);
double yL = deltaL - lowSumComp;
double tL = lowSum + yL;
lowSumComp = (tL - lowSum) - yL;
lowSum = tL;
}
lowSum += l;
lowBuf[lowIdx] = l;
lowIdx++;
if (lowIdx >= period)
@@ -490,22 +493,6 @@ public sealed class Ghla : AbstractBase
filled++;
}
// Resync
tickCount++;
if (filled >= period && tickCount >= ResyncInterval)
{
tickCount = 0;
double recalcH = 0;
double recalcL = 0;
for (int k = 0; k < period; k++)
{
recalcH += highBuf[k];
recalcL += lowBuf[k];
}
highSum = recalcH;
lowSum = recalcL;
}
double smaH = highSum / filled;
double smaL = lowSum / filled;