mirror of
https://github.com/mihakralj/QuanTAlib.git
synced 2026-08-22 20:48:04 +00:00
test: setup common stability and robustness properties tracking
This commit is contained in:
@@ -354,7 +354,7 @@ public class ZtestTests
|
||||
|
||||
for (int i = 0; i < count; i++)
|
||||
{
|
||||
Assert.Equal(batchResult[i].Value, spanOutput[i], 1e-4); // t-stat magnifies FP drift (values ~6000)
|
||||
Assert.Equal(batchResult[i].Value, spanOutput[i], 5e-4); // t-stat magnifies FP drift (values ~15000)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+114
-63
@@ -29,13 +29,16 @@ public sealed class Ztest : AbstractBase
|
||||
private readonly RingBuffer _buffer;
|
||||
private readonly TValuePublishedHandler _handler;
|
||||
private double _lastValidValue;
|
||||
|
||||
[StructLayout(LayoutKind.Auto)]
|
||||
private record struct State(double LastValidTStat, 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 one-sample t-test indicator.
|
||||
/// </summary>
|
||||
/// <param name="period">Lookback period (default 30, must be >= 2)</param>
|
||||
/// <param name="mu0">Hypothesized population mean (default 0.0)</param>
|
||||
public Ztest(int period = 30, double mu0 = 0.0)
|
||||
@@ -50,11 +53,14 @@ public sealed class Ztest : AbstractBase
|
||||
_buffer = new RingBuffer(period);
|
||||
Name = $"Ztest({period},{mu0:G})";
|
||||
WarmupPeriod = period;
|
||||
_s = new State(0.0, 0.0);
|
||||
_ps = _s;
|
||||
_sumSq = 0.0;
|
||||
_p_sumSq = 0.0;
|
||||
_handler = Handle;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a rolling one-sample t-test 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 30)</param>
|
||||
/// <param name="mu0">Hypothesized population mean (default 0.0)</param>
|
||||
@@ -66,16 +72,6 @@ public sealed class Ztest : 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))
|
||||
@@ -87,11 +83,37 @@ public sealed class Ztest : 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)
|
||||
{
|
||||
@@ -99,27 +121,19 @@ public sealed class Ztest : 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 first: 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;
|
||||
}
|
||||
|
||||
// Bessel correction: sample variance = popVariance * n / (n - 1)
|
||||
double sampleStdDev = Math.Sqrt(popVariance * n / (n - 1));
|
||||
// which is numerator / (n - 1)
|
||||
double sampleVariance = numerator / (n - 1);
|
||||
double sampleStdDev = Math.Sqrt(sampleVariance);
|
||||
double standardError = sampleStdDev / Math.Sqrt(n);
|
||||
|
||||
if (standardError > 1e-10)
|
||||
@@ -132,7 +146,6 @@ public sealed class Ztest : AbstractBase
|
||||
}
|
||||
}
|
||||
|
||||
_s = new State(result, _lastValidValue);
|
||||
Last = new TValue(input.Time, result);
|
||||
PubEvent(Last, isNew);
|
||||
return Last;
|
||||
@@ -140,17 +153,30 @@ public sealed class Ztest : 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, _mu0);
|
||||
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)]
|
||||
@@ -160,11 +186,24 @@ public sealed class Ztest : 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);
|
||||
@@ -221,6 +260,8 @@ public sealed class Ztest : 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++)
|
||||
{
|
||||
@@ -235,45 +276,55 @@ public sealed class Ztest : 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;
|
||||
}
|
||||
|
||||
// Bessel correction: sample variance = popVariance * n / (n - 1)
|
||||
double sampleStdDev = Math.Sqrt(popVariance * n / (n - 1));
|
||||
double sampleVariance = numerator / (n - 1);
|
||||
double sampleStdDev = Math.Sqrt(sampleVariance);
|
||||
double standardError = sampleStdDev / Math.Sqrt(n);
|
||||
|
||||
if (standardError > 1e-10)
|
||||
|
||||
@@ -15,7 +15,7 @@
|
||||
- Parameterized by `period` (default 30), `mu0` (default 0.0).
|
||||
- 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 testable statistical properties.
|
||||
|
||||
> "The purpose of hypothesis testing is not to prove what we believe, but to measure what we observe." — Adapted from R.A. Fisher
|
||||
|
||||
@@ -91,21 +91,12 @@ The indicators answer different questions:
|
||||
|
||||
### Operation Count (Streaming Mode)
|
||||
|
||||
Z-Test computes a rolling mean and standard deviation for O(1) hypothesis testing per bar.
|
||||
|
||||
| Operation | Count | Cost (cycles) | Subtotal |
|
||||
| :--- | :---: | :---: | :---: |
|
||||
| O(1) StdDev computation | 1 | 28 cy | ~28 cy |
|
||||
| Compute Z = (x - mu) / (sigma / sqrt(N)) | 1 | 5 cy | ~5 cy |
|
||||
| NaN guard (sigma = 0 guard) | 1 | 2 cy | ~2 cy |
|
||||
| **Total** | **O(1)** | — | **~35 cy** |
|
||||
|
||||
O(1) per update. Z-statistic is a trivial transformation of the running mean and standard deviation already computed by StdDev.
|
||||
ZTEST uses a rolling window with running sums and periodic resynchronization.
|
||||
|
||||
| Operation | Complexity | Notes |
|
||||
|-----------|-----------|-------|
|
||||
| Update (streaming) | $O(n)$ | Full window scan for sum/sumSq |
|
||||
| Batch (span) | $O(N \cdot p)$ | N data points, p period |
|
||||
| Update (streaming) | $O(1)$ amortized | Running sum/sumSq maintenance; periodic full resync every 1000 updates |
|
||||
| Batch (span) | $O(N)$ | Single pass over source with O(1) ring maintenance per element |
|
||||
| Memory | $O(p)$ | RingBuffer + scalar state |
|
||||
| Allocations per update | 0 | Zero-allocation hot path |
|
||||
|
||||
|
||||
Reference in New Issue
Block a user