mirror of
https://github.com/mihakralj/QuanTAlib.git
synced 2026-08-16 09:38:05 +00:00
Merge dev into main: v0.8.7 Kahan compensated summation
This commit is contained in:
@@ -20,9 +20,9 @@ namespace QuanTAlib;
|
||||
public sealed class Ema : AbstractBase
|
||||
{
|
||||
[StructLayout(LayoutKind.Auto)]
|
||||
private record struct State(double Ema, double E, bool IsHot, bool IsCompensated, int TickCount)
|
||||
private record struct State(double Ema, double E, bool IsHot, bool IsCompensated)
|
||||
{
|
||||
public static State New() => new() { Ema = 0, E = 1.0, IsHot = false, IsCompensated = false, TickCount = 0 };
|
||||
public static State New() => new() { Ema = 0, E = 1.0, IsHot = false, IsCompensated = false };
|
||||
}
|
||||
|
||||
private readonly double _alpha;
|
||||
@@ -32,12 +32,6 @@ public sealed class Ema : AbstractBase
|
||||
private double _lastValidValue;
|
||||
private double _p_lastValidValue;
|
||||
|
||||
/// <summary>
|
||||
/// Interval for periodic resync to prevent floating-point drift accumulation.
|
||||
/// After this many updates, the EMA state is recalculated from a checkpoint.
|
||||
/// </summary>
|
||||
private const int ResyncInterval = 10000;
|
||||
|
||||
/// <summary>
|
||||
/// Creates EMA with specified period.
|
||||
/// Alpha = 2 / (period + 1)
|
||||
@@ -286,7 +280,7 @@ public sealed class Ema : AbstractBase
|
||||
|
||||
/// <summary>
|
||||
/// Core EMA calculation with bias compensation and NaN handling.
|
||||
/// Uses FMA for precision and includes periodic resync for long streams.
|
||||
/// Uses FMA for precision. IIR filters are inherently self-correcting.
|
||||
/// </summary>
|
||||
[MethodImpl(MethodImplOptions.AggressiveOptimization)]
|
||||
private static void CalculateCore(ReadOnlySpan<double> source, Span<double> output, double alpha, ref State state, ref double lastValidValue)
|
||||
@@ -319,7 +313,6 @@ public sealed class Ema : AbstractBase
|
||||
}
|
||||
|
||||
output[i] = state.Ema / (1.0 - state.E);
|
||||
state.TickCount++;
|
||||
}
|
||||
if (state.E <= COMPENSATOR_THRESHOLD)
|
||||
{
|
||||
@@ -389,17 +382,6 @@ public sealed class Ema : AbstractBase
|
||||
state.Ema = Math.FusedMultiplyAdd(state.Ema, decay, alpha * v3);
|
||||
Unsafe.Add(ref outRef, i + 3) = state.Ema;
|
||||
|
||||
state.TickCount += 4;
|
||||
|
||||
// Periodic resync to prevent floating-point drift
|
||||
if (state.TickCount >= ResyncInterval)
|
||||
{
|
||||
state.TickCount = 0;
|
||||
// For EMA, resync means recalculating from a known good state
|
||||
// Since we don't store history, we accept the current state as truth
|
||||
// The drift is typically < 1e-14 per operation, so after 10000 ops
|
||||
// it's still well within double precision tolerance
|
||||
}
|
||||
}
|
||||
|
||||
// Scalar remainder
|
||||
@@ -417,7 +399,6 @@ public sealed class Ema : AbstractBase
|
||||
|
||||
state.Ema = Math.FusedMultiplyAdd(state.Ema, decay, alpha * val);
|
||||
Unsafe.Add(ref outRef, i) = state.Ema;
|
||||
state.TickCount++;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -308,7 +308,7 @@ ema.Prime(historicalPrices); // Ready for live data
|
||||
### State Structure
|
||||
|
||||
```csharp
|
||||
private record struct State(double Ema, double E, bool IsHot, bool IsCompensated, int TickCount);
|
||||
private record struct State(double Ema, double E, bool IsHot, bool IsCompensated);
|
||||
```
|
||||
|
||||
| Field | Size | Purpose |
|
||||
@@ -317,9 +317,8 @@ private record struct State(double Ema, double E, bool IsHot, bool IsCompensated
|
||||
| `E` | 8 bytes | Compensator factor $(1-\alpha)^n$ |
|
||||
| `IsHot` | 1 byte | Warmup complete flag |
|
||||
| `IsCompensated` | 1 byte | True when E < 1e-10 |
|
||||
| `TickCount` | 4 bytes | Bars processed |
|
||||
|
||||
**Total state:** ~32 bytes per instance. No buffers required regardless of period.
|
||||
**Total state:** ~18 bytes per instance. No buffers required regardless of period. IIR filters are inherently self-correcting and do not require periodic resynchronization.
|
||||
|
||||
### FMA Optimization
|
||||
|
||||
|
||||
@@ -19,7 +19,7 @@ namespace QuanTAlib;
|
||||
public sealed class Rema : AbstractBase
|
||||
{
|
||||
[StructLayout(LayoutKind.Auto)]
|
||||
private record struct State(double Rema, double PrevRema, double E, bool IsHot, bool IsCompensated, int TickCount, bool IsInitialized)
|
||||
private record struct State(double Rema, double PrevRema, double E, bool IsHot, bool IsCompensated, bool IsInitialized)
|
||||
{
|
||||
public static State New() => new()
|
||||
{
|
||||
@@ -28,7 +28,6 @@ public sealed class Rema : AbstractBase
|
||||
E = 1.0,
|
||||
IsHot = false,
|
||||
IsCompensated = false,
|
||||
TickCount = 0,
|
||||
IsInitialized = false
|
||||
};
|
||||
}
|
||||
@@ -41,7 +40,6 @@ public sealed class Rema : AbstractBase
|
||||
private double _lastValidValue;
|
||||
private double _p_lastValidValue;
|
||||
|
||||
private const int ResyncInterval = 10000;
|
||||
private const double COVERAGE_THRESHOLD = 0.05;
|
||||
private const double COMPENSATOR_THRESHOLD = 1e-10;
|
||||
|
||||
@@ -229,7 +227,6 @@ public sealed class Rema : AbstractBase
|
||||
state.Rema = input;
|
||||
state.PrevRema = input;
|
||||
state.IsInitialized = true;
|
||||
state.TickCount = 1;
|
||||
state.E *= decay;
|
||||
|
||||
if (state.E <= COVERAGE_THRESHOLD)
|
||||
@@ -256,7 +253,6 @@ public sealed class Rema : AbstractBase
|
||||
// When lambda=0: REMA = reg_component (pure momentum)
|
||||
state.Rema = Math.FusedMultiplyAdd(lambda, emaComponent - regComponent, regComponent);
|
||||
state.PrevRema = prevRema;
|
||||
state.TickCount++;
|
||||
|
||||
if (!state.IsCompensated)
|
||||
{
|
||||
@@ -318,7 +314,6 @@ public sealed class Rema : AbstractBase
|
||||
state.Rema = val;
|
||||
state.PrevRema = val;
|
||||
state.IsInitialized = true;
|
||||
state.TickCount = 1;
|
||||
state.E *= decay;
|
||||
|
||||
if (state.E <= COVERAGE_THRESHOLD)
|
||||
@@ -336,7 +331,6 @@ public sealed class Rema : AbstractBase
|
||||
double regComponent = state.Rema + (state.Rema - state.PrevRema);
|
||||
state.Rema = Math.FusedMultiplyAdd(lambda, emaComponent - regComponent, regComponent);
|
||||
state.PrevRema = prevRema;
|
||||
state.TickCount++;
|
||||
|
||||
if (!state.IsCompensated)
|
||||
{
|
||||
@@ -365,10 +359,6 @@ public sealed class Rema : AbstractBase
|
||||
|
||||
Unsafe.Add(ref outRef, i) = result;
|
||||
|
||||
if (state.TickCount >= ResyncInterval)
|
||||
{
|
||||
state.TickCount = 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -121,7 +121,7 @@ REMA is inherently recursive due to state dependency on previous two values. SIM
|
||||
| **Throughput (Streaming)** | ~2 ns/bar | Single Update() call |
|
||||
| **Allocations (Hot Path)** | 0 bytes | Verified via BenchmarkDotNet |
|
||||
| **Complexity** | O(1) | Two FMA operations per bar |
|
||||
| **State Size** | 48 bytes | REMA, PrevRema, E, flags, counter |
|
||||
| **State Size** | 44 bytes | REMA, PrevRema, E, flags |
|
||||
|
||||
### Quality Metrics
|
||||
|
||||
|
||||
@@ -627,11 +627,11 @@ public class RemaTests
|
||||
[Fact]
|
||||
public void Rema_AllModes_ProduceSameResult_AfterResyncInterval()
|
||||
{
|
||||
// This guards against implementation drift between CalculateCore (batch/span)
|
||||
// and Update(TValue) (streaming/eventing) when internal counters wrap/reset.
|
||||
// Guards against implementation drift between CalculateCore (batch/span)
|
||||
// and Update(TValue) (streaming/eventing) over long runs.
|
||||
int period = 10;
|
||||
double lambda = 0.5;
|
||||
int count = 12050; // > ResyncInterval (10,000)
|
||||
int count = 12050; // Long-running consistency check
|
||||
|
||||
var gbm = new GBM(startPrice: 100, mu: 0.05, sigma: 0.2, seed: 321);
|
||||
var bars = gbm.Fetch(count, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1));
|
||||
|
||||
@@ -23,9 +23,9 @@ namespace QuanTAlib;
|
||||
public sealed class Rgma : AbstractBase
|
||||
{
|
||||
[StructLayout(LayoutKind.Auto)]
|
||||
private record struct State(double E, bool IsHot, bool IsInitialized, int TickCount)
|
||||
private record struct State(double E, bool IsHot, bool IsInitialized)
|
||||
{
|
||||
public static State New() => new() { E = 1.0, IsHot = false, IsInitialized = false, TickCount = 0 };
|
||||
public static State New() => new() { E = 1.0, IsHot = false, IsInitialized = false };
|
||||
}
|
||||
|
||||
private readonly int _passes;
|
||||
@@ -45,7 +45,6 @@ public sealed class Rgma : AbstractBase
|
||||
private bool _disposed;
|
||||
|
||||
private const double COVERAGE_THRESHOLD = 0.05;
|
||||
private const int ResyncInterval = 10000;
|
||||
private const int StackAllocThreshold = 512;
|
||||
public override bool IsHot => _state.IsHot;
|
||||
|
||||
@@ -272,7 +271,6 @@ public sealed class Rgma : AbstractBase
|
||||
{
|
||||
filters.Fill(input);
|
||||
state.IsInitialized = true;
|
||||
state.TickCount = 1;
|
||||
state.E *= decay;
|
||||
if (state.E <= COVERAGE_THRESHOLD)
|
||||
{
|
||||
@@ -289,17 +287,12 @@ public sealed class Rgma : AbstractBase
|
||||
filters[i] = Math.FusedMultiplyAdd(alpha, filters[i - 1] - filters[i], filters[i]);
|
||||
}
|
||||
|
||||
state.TickCount++;
|
||||
state.E *= decay;
|
||||
if (!state.IsHot && state.E <= COVERAGE_THRESHOLD)
|
||||
{
|
||||
state.IsHot = true;
|
||||
}
|
||||
|
||||
if (state.TickCount >= ResyncInterval)
|
||||
{
|
||||
state.TickCount = 0;
|
||||
}
|
||||
|
||||
return filters[^1];
|
||||
}
|
||||
@@ -332,7 +325,6 @@ public sealed class Rgma : AbstractBase
|
||||
{
|
||||
filters.Fill(x);
|
||||
state.IsInitialized = true;
|
||||
state.TickCount = 1;
|
||||
state.E *= decay;
|
||||
if (state.E <= COVERAGE_THRESHOLD)
|
||||
{
|
||||
@@ -349,17 +341,12 @@ public sealed class Rgma : AbstractBase
|
||||
filters[p] = Math.FusedMultiplyAdd(alpha, filters[p - 1] - filters[p], filters[p]);
|
||||
}
|
||||
|
||||
state.TickCount++;
|
||||
state.E *= decay;
|
||||
if (!state.IsHot && state.E <= COVERAGE_THRESHOLD)
|
||||
{
|
||||
state.IsHot = true;
|
||||
}
|
||||
|
||||
if (state.TickCount >= ResyncInterval)
|
||||
{
|
||||
state.TickCount = 0;
|
||||
}
|
||||
|
||||
y = filters[^1];
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user