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
+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;
}