// NORMDIST: Normal Distribution CDF // Applies the Gaussian CDF Φ(z) = 0.5*(1+erf(z/√2)) to a z-score normalized // price series over a rolling lookback window. // Pipeline: Rolling mean+stddev → z-score → parameter adjustment → erf approximation → CDF. using System.Runtime.CompilerServices; using System.Runtime.InteropServices; namespace QuanTAlib; /// /// NORMDIST: Normal Distribution CDF /// Computes Φ(z; μ, σ) = 0.5*(1+erf((z-μ)/(σ*√2))) applied to a z-score normalized /// price series over a rolling lookback window. /// /// /// Key properties: /// - Output always in [0, 1] /// - Rolling window computes mean and population stddev for z-score normalization /// - Default μ=0, σ=1 gives standard-normal CDF of the price's z-score relative to the window /// - Increasing σ compresses the S-curve; shifting μ moves the midpoint away from the rolling mean /// - erf approximation: Abramowitz & Stegun 7.1.25 (3-term), max error ~2.5e-5 /// - Fewer than 2 valid values in window → output 0.5 (uncertainty) /// - NaN/Infinity inputs use last-valid-value substitution /// [SkipLocalsInit] public sealed class Normdist : AbstractBase { private readonly int _period; private readonly double _mu; private readonly double _invSigmaSqrt2; // precomputed: 1 / (sigma * sqrt(2)) private readonly RingBuffer _buffer; [StructLayout(LayoutKind.Auto)] private record struct State(double LastValid); private State _state, _p_state; public override bool IsHot => _buffer.Count >= _period; /// /// Initializes a new Normdist indicator. /// /// Mean shift μ applied after z-score (default 0.0) /// Scale σ > 0 applied after z-score (default 1.0) /// Lookback window for rolling z-score normalization (default 14) public Normdist(double mu = 0.0, double sigma = 1.0, int period = 14) { if (sigma <= 0.0) { throw new ArgumentException("Sigma must be > 0", nameof(sigma)); } if (period < 2) { throw new ArgumentException("Period must be >= 2", nameof(period)); } _mu = mu; _period = period; _invSigmaSqrt2 = 1.0 / (sigma * Math.Sqrt(2.0)); _buffer = new RingBuffer(period); Name = $"Normdist({mu:F2},{sigma:F2},{period})"; WarmupPeriod = period; _state = new State(0.5); _p_state = _state; } /// /// Initializes a new Normdist indicator with source for event-based chaining. /// /// Source indicator for chaining /// Mean shift μ (default 0.0) /// Scale σ > 0 (default 1.0) /// Lookback window (default 14) public Normdist(ITValuePublisher source, double mu = 0.0, double sigma = 1.0, int period = 14) : this(mu, sigma, period) { source.Pub += HandleUpdate; } [MethodImpl(MethodImplOptions.AggressiveInlining)] private void HandleUpdate(object? sender, in TValueEventArgs e) => Update(e.Value, e.IsNew); /// /// Error function approximation via Abramowitz & Stegun 7.1.25 (3-term, max error ~2.5e-5). /// erf(x) ≈ 1 - (a1*t + a2*t² + a3*t³)*exp(-x²), t = 1/(1 + 0.47047*|x|) /// [MethodImpl(MethodImplOptions.AggressiveInlining)] internal static double Erf(double x) { const double p = 0.47047; const double a1 = 0.3480242; const double a2 = -0.0958798; const double a3 = 0.7478556; double ax = Math.Abs(x); double t = 1.0 / Math.FusedMultiplyAdd(p, ax, 1.0); double poly = Math.FusedMultiplyAdd(a3, t, a2); poly = Math.FusedMultiplyAdd(poly, t, a1); poly *= t; double val = 1.0 - poly * Math.Exp(-(ax * ax)); return x >= 0.0 ? val : -val; } /// /// Normal Distribution CDF: Φ(x; μ, σ) = 0.5*(1 + erf((x-μ)/(σ*√2))). /// Returns 0.5 when σ ≤ 0. /// [MethodImpl(MethodImplOptions.AggressiveInlining)] public static double NormalCdf(double x, double mu, double sigma) { if (sigma <= 0.0) { return 0.5; } double z = (x - mu) / (sigma * Math.Sqrt(2.0)); return 0.5 * (1.0 + Erf(z)); } /// /// Pure static CDF helper — identical to with an explicit name /// for downstream consumers and validation tests. /// [MethodImpl(MethodImplOptions.AggressiveInlining)] public static double StaticCdf(double x, double mu, double sigma) => NormalCdf(x, mu, sigma); /// /// Computes rolling mean and population standard deviation from a span of values. /// Returns (mean=0, stddev=0, count=0) when span is empty. /// [MethodImpl(MethodImplOptions.AggressiveInlining)] private static (double mean, double stddev, int count) RollingStats(ReadOnlySpan values) { double sum = 0.0; double sumSq = 0.0; int count = 0; for (int i = 0; i < values.Length; i++) { double v = values[i]; if (double.IsFinite(v)) { sum += v; sumSq = Math.FusedMultiplyAdd(v, v, sumSq); count++; } } if (count < 2) { return (0.0, 0.0, count); } double mean = sum / count; double variance = sumSq / count - mean * mean; double stddev = variance > 0.0 ? Math.Sqrt(variance) : 0.0; return (mean, stddev, count); } [MethodImpl(MethodImplOptions.AggressiveInlining)] public override TValue Update(TValue input, bool isNew = true) { if (isNew) { _p_state = _state; } else { _state = _p_state; } double value = input.Value; double result; if (double.IsFinite(value)) { _buffer.Add(value, isNew); var (mean, stddev, count) = RollingStats(_buffer.GetSpan()); if (count < 2) { result = 0.5; } else { // Z-score relative to rolling distribution double z = stddev > 0.0 ? (value - mean) / stddev : 0.0; // Apply user mu/sigma shift: z_final = (z - mu) / sigma → CDF input double zFinal = (z - _mu) * _invSigmaSqrt2; // = (z-mu)/(sigma*sqrt(2)) double erf = Erf(zFinal); result = 0.5 * (1.0 + erf); } _state = new State(result); } else { result = _state.LastValid; } Last = new TValue(input.Time, result); PubEvent(Last, isNew); return Last; } public override TSeries Update(TSeries source) { var result = new TSeries(source.Count); ReadOnlySpan values = source.Values; ReadOnlySpan times = source.Times; for (int i = 0; i < source.Count; i++) { var tv = Update(new TValue(new DateTime(times[i], DateTimeKind.Utc), values[i]), true); result.Add(tv, true); } return result; } public override void Prime(ReadOnlySpan 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, double mu = 0.0, double sigma = 1.0, int period = 14) { var indicator = new Normdist(mu, sigma, period); return indicator.Update(source); } /// /// Calculates Normal Distribution CDF over a span of values. /// Uses a sliding window z-score normalization identical to the streaming path. /// public static void Batch( ReadOnlySpan source, Span output, double mu = 0.0, double sigma = 1.0, int period = 14) { if (source.Length == 0) { throw new ArgumentException("Source cannot be empty", nameof(source)); } if (output.Length < source.Length) { throw new ArgumentException("Output length must be >= source length", nameof(output)); } if (sigma <= 0.0) { throw new ArgumentException("Sigma must be > 0", nameof(sigma)); } if (period < 2) { throw new ArgumentException("Period must be >= 2", nameof(period)); } double invSigmaSqrt2 = 1.0 / (sigma * Math.Sqrt(2.0)); double lastValid = 0.5; for (int i = 0; i < source.Length; i++) { double val = source[i]; if (!double.IsFinite(val)) { output[i] = lastValid; continue; } int start = Math.Max(0, i - period + 1); double sum = 0.0; double sumSq = 0.0; int count = 0; for (int j = start; j <= i; j++) { double v = source[j]; if (double.IsFinite(v)) { sum += v; sumSq = Math.FusedMultiplyAdd(v, v, sumSq); count++; } } double result; if (count < 2) { result = 0.5; } else { double mean = sum / count; double variance = sumSq / count - mean * mean; double stddev = variance > 0.0 ? Math.Sqrt(variance) : 0.0; double z = stddev > 0.0 ? (val - mean) / stddev : 0.0; double zFinal = (z - mu) * invSigmaSqrt2; result = 0.5 * (1.0 + Erf(zFinal)); } lastValid = result; output[i] = result; } } public static (TSeries Results, Normdist Indicator) Calculate( TSeries source, double mu = 0.0, double sigma = 1.0, int period = 14) { var indicator = new Normdist(mu, sigma, period); TSeries results = indicator.Update(source); return (results, indicator); } public override void Reset() { _buffer.Clear(); _state = new State(0.5); _p_state = _state; Last = default; } }