test: setup common stability and robustness properties tracking

This commit is contained in:
Miha Kralj
2026-02-27 12:50:05 -08:00
parent 4ab3a7fb53
commit 769a923a24
287 changed files with 1314 additions and 867 deletions
@@ -139,4 +139,4 @@ public sealed class ZscoreValidationTests
int finiteCount = values.Count(v => double.IsFinite(v));
Assert.True(finiteCount > 100, $"Expected >100 finite values, got {finiteCount}");
}
}
}
+111 -61
View File
@@ -27,13 +27,16 @@ public sealed class Zscore : AbstractBase
private readonly RingBuffer _buffer;
private readonly TValuePublishedHandler _handler;
private double _lastValidValue;
[StructLayout(LayoutKind.Auto)]
private record struct State(double LastValidZScore, double LastValidValue);
private State _s, _ps;
private double _sumSq;
private double _p_sumSq;
private int _updateCount;
private const int ResyncInterval = 1000;
public override bool IsHot => _buffer.Count >= _period;
/// <summary>
/// Initializes a rolling z-score indicator.
/// </summary>
/// <param name="period">Lookback period (default 14, must be >= 2)</param>
public Zscore(int period = 14)
{
@@ -46,11 +49,14 @@ public sealed class Zscore : AbstractBase
_buffer = new RingBuffer(period);
Name = $"Zscore({period})";
WarmupPeriod = period;
_s = new State(0.0, 0.0);
_ps = _s;
_sumSq = 0.0;
_p_sumSq = 0.0;
_handler = Handle;
}
/// <summary>
/// Initializes a rolling z-score indicator and subscribes it to a source publisher.
/// </summary>
/// <param name="source">Source indicator for event-based chaining</param>
/// <param name="period">Lookback period (default 14)</param>
public Zscore(ITValuePublisher source, int period = 14) : this(period)
@@ -61,16 +67,6 @@ public sealed class Zscore : AbstractBase
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public override TValue Update(TValue input, bool isNew = true)
{
if (isNew)
{
_ps = _s;
}
else
{
_s = _ps;
_lastValidValue = _s.LastValidValue;
}
double value = input.Value;
if (!double.IsFinite(value))
@@ -82,11 +78,37 @@ public sealed class Zscore : AbstractBase
_lastValidValue = value;
}
_buffer.Add(value, isNew);
if (isNew)
{
_p_sumSq = _sumSq;
_buffer.Snapshot();
}
else
{
_sumSq = _p_sumSq;
_buffer.Restore();
}
if (_buffer.IsFull)
{
double oldVal = _buffer.Oldest;
_sumSq = Math.FusedMultiplyAdd(-oldVal, oldVal, _sumSq);
}
_buffer.Add(value);
_sumSq = Math.FusedMultiplyAdd(value, value, _sumSq);
if (isNew)
{
_updateCount++;
if (_updateCount % ResyncInterval == 0)
{
Resync();
}
}
double result;
ReadOnlySpan<double> data = _buffer.GetSpan();
int n = data.Length;
int n = _buffer.Count;
if (n < 2)
{
@@ -94,25 +116,16 @@ public sealed class Zscore : AbstractBase
}
else
{
double sum = 0.0;
double sumSq = 0.0;
for (int i = 0; i < n; i++)
{
double v = data[i];
sum += v;
sumSq += v * v;
}
double sum = _buffer.Sum;
double mean = sum / n;
// Population variance: E[X²] - (E[X])²
double popVariance = (sumSq / n) - (mean * mean);
if (popVariance < 0.0)
double numerator = _sumSq - (sum * sum) / n;
if (numerator < 0)
{
popVariance = 0.0;
numerator = 0;
}
double popVariance = numerator / n;
double stdDev = Math.Sqrt(popVariance);
if (stdDev > 1e-10)
@@ -125,7 +138,6 @@ public sealed class Zscore : AbstractBase
}
}
_s = new State(result, _lastValidValue);
Last = new TValue(input.Time, result);
PubEvent(Last, isNew);
return Last;
@@ -133,17 +145,30 @@ public sealed class Zscore : AbstractBase
public override TSeries Update(TSeries source)
{
var result = new TSeries(source.Count);
ReadOnlySpan<double> values = source.Values;
ReadOnlySpan<long> times = source.Times;
for (int i = 0; i < source.Count; i++)
if (source.Count == 0)
{
var tv = Update(new TValue(new DateTime(times[i], DateTimeKind.Utc), values[i]), true);
result.Add(tv, true);
return new TSeries();
}
return result;
int len = source.Count;
var t = new List<long>(len);
var v = new List<double>(len);
CollectionsMarshal.SetCount(t, len);
CollectionsMarshal.SetCount(v, len);
var tSpan = CollectionsMarshal.AsSpan(t);
var vSpan = CollectionsMarshal.AsSpan(v);
Batch(source.Values, vSpan, _buffer.Capacity);
source.Times.CopyTo(tSpan);
int primeStart = Math.Max(0, len - _buffer.Capacity);
for (int i = primeStart; i < len; i++)
{
Update(source[i]);
}
return new TSeries(t, v);
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
@@ -153,11 +178,24 @@ public sealed class Zscore : AbstractBase
{
_buffer.Clear();
_lastValidValue = 0;
_s = new State(0.0, 0.0);
_ps = _s;
_sumSq = 0.0;
_p_sumSq = 0.0;
_updateCount = 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);
@@ -214,6 +252,8 @@ public sealed class Zscore : AbstractBase
int head = 0;
int count = 0;
double lastValid = 0.0;
double sum = 0.0;
double sumSq = 0.0;
for (int i = 0; i < source.Length; i++)
{
@@ -228,43 +268,53 @@ public sealed class Zscore : AbstractBase
lastValid = val;
}
if (count < ringSize)
if (count == ringSize)
{
ring[count] = val;
count++;
double oldVal = ring[head];
sum = Math.FusedMultiplyAdd(-1.0, oldVal, sum + val);
sumSq = Math.FusedMultiplyAdd(-oldVal, oldVal, sumSq);
}
else
{
ring[head] = val;
count++;
sum += val;
}
ring[head] = val;
sumSq = Math.FusedMultiplyAdd(val, val, sumSq);
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;
continue;
}
double sum = 0.0;
double sumSq = 0.0;
int n = count;
for (int j = 0; j < n; j++)
{
double v = ring[j];
sum += v;
sumSq += v * v;
}
double mean = sum / n;
double popVariance = (sumSq / n) - (mean * mean);
if (popVariance < 0.0)
double numerator = sumSq - (sum * sum) / n;
if (numerator < 0)
{
popVariance = 0.0;
numerator = 0;
}
double popVariance = numerator / n;
double stdDev = Math.Sqrt(popVariance);
if (stdDev > 1e-10)
+1 -1
View File
@@ -15,7 +15,7 @@
- Parameterized by `period` (default 14).
- Output range: Unbounded.
- Requires `period` bars of warmup before first valid output (IsHot = true).
- Validated against TA-Lib, Skender, and Tulip reference implementations where available.
- Validated against manual computation, PineScript parity, and statistical invariants.
> "How far from normal is this?" — Every risk manager, every day.