Files
QuanTAlib/lib/statistics/zscore/Zscore.cs
T

346 lines
9.3 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// ZSCORE: Z-Score (Population Standard Score) — also known as STANDARDIZE
// Calculates z = (x - μ) / σ using population standard deviation (N denominator)
// Formula: z = (x - mean) / sqrt(Σ(xi - mean)² / N)
using System.Buffers;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
namespace QuanTAlib;
/// <summary>
/// ZSCORE: Z-Score (also known as STANDARDIZE) — measures how many population
/// standard deviations a value lies from the rolling mean over a lookback window.
/// </summary>
/// <remarks>
/// Key properties:
/// - Uses population standard deviation (N denominator, no Bessel correction)
/// - Output is unbounded (typically -3 to +3 for normally distributed data)
/// - When σ = 0 (constant data), returns 0.0
/// - Period must be >= 2
/// </remarks>
/// <seealso href="zscore.pine">Reference Pine Script implementation</seealso>
[SkipLocalsInit]
public sealed class Zscore : AbstractBase
{
private readonly int _period;
private readonly RingBuffer _buffer;
private readonly TValuePublishedHandler _handler;
private double _lastValidValue;
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)
{
if (period < 2)
{
throw new ArgumentException("Period must be >= 2 for standard deviation calculation.", nameof(period));
}
_period = period;
_buffer = new RingBuffer(period);
Name = $"Zscore({period})";
WarmupPeriod = period;
_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)
{
source.Pub += _handler;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public override TValue Update(TValue input, bool isNew = true)
{
double value = input.Value;
if (!double.IsFinite(value))
{
value = _lastValidValue;
}
else
{
_lastValidValue = value;
}
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;
int n = _buffer.Count;
if (n < 2)
{
result = 0.0;
}
else
{
double sum = _buffer.Sum;
double mean = sum / n;
double numerator = _sumSq - (sum * sum) / n;
if (numerator < 0)
{
numerator = 0;
}
double popVariance = numerator / n;
double stdDev = Math.Sqrt(popVariance);
if (stdDev > 1e-10)
{
result = (value - mean) / stdDev;
}
else
{
result = 0.0;
}
}
Last = new TValue(input.Time, result);
PubEvent(Last, isNew);
return Last;
}
public override TSeries Update(TSeries source)
{
if (source.Count == 0)
{
return new TSeries();
}
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)]
private void Handle(object? sender, in TValueEventArgs args) => Update(args.Value, args.IsNew);
public override void Reset()
{
_buffer.Clear();
_lastValidValue = 0;
_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);
DateTime time = DateTime.UtcNow - (interval * source.Length);
for (int i = 0; i < source.Length; i++)
{
Update(new TValue(time, source[i]), true);
time += interval;
}
}
public static TSeries Batch(TSeries source, int period = 14)
{
var indicator = new Zscore(period);
return indicator.Update(source);
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static void Batch(ReadOnlySpan<double> source, Span<double> output, int period = 14)
{
if (source.Length == 0)
{
throw new ArgumentException("Source span must not be empty.", nameof(source));
}
if (output.Length < source.Length)
{
throw new ArgumentException("Output span must be at least as long as source.", nameof(output));
}
if (period < 2)
{
throw new ArgumentException("Period must be >= 2.", nameof(period));
}
const int StackallocThreshold = 256;
double[]? rented = null;
int ringSize = period;
scoped Span<double> ring;
if (ringSize <= StackallocThreshold)
{
ring = stackalloc double[ringSize];
}
else
{
rented = ArrayPool<double>.Shared.Rent(ringSize);
ring = rented.AsSpan(0, ringSize);
}
try
{
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++)
{
double val = source[i];
if (!double.IsFinite(val))
{
val = lastValid;
}
else
{
lastValid = val;
}
if (count == ringSize)
{
double oldVal = ring[head];
sum = Math.FusedMultiplyAdd(-1.0, oldVal, sum + val);
sumSq = Math.FusedMultiplyAdd(-oldVal, oldVal, sumSq);
}
else
{
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;
}
int n = count;
double mean = sum / n;
double numerator = sumSq - (sum * sum) / n;
if (numerator < 0)
{
numerator = 0;
}
double popVariance = numerator / n;
double stdDev = Math.Sqrt(popVariance);
if (stdDev > 1e-10)
{
output[i] = (val - mean) / stdDev;
}
else
{
output[i] = 0.0;
}
}
}
finally
{
if (rented != null)
{
ArrayPool<double>.Shared.Return(rented);
}
}
}
public static (TSeries Results, Zscore Indicator) Calculate(TSeries source, int period = 14)
{
var indicator = new Zscore(period);
TSeries results = indicator.Update(source);
return (results, indicator);
}
}