using System.Runtime.CompilerServices; using System.Runtime.InteropServices; namespace QuanTAlib; /// /// WRMSE: Weighted Root Mean Squared Error /// /// /// WRMSE extends RMSE by allowing each error to be weighted differently, /// enabling emphasis on certain data points (e.g., recent observations, /// high-volume periods, or critical price levels). /// /// Formula: /// WRMSE = √(Σ(w_i * (actual_i - predicted_i)²) / Σ(w_i)) /// /// Uses dual RingBuffers for O(1) streaming updates with Kahan compensated running sums. /// /// Key properties: /// - Always non-negative (WRMSE ≥ 0) /// - Same units as the original data /// - Weights allow emphasizing important observations /// - Reduces to RMSE when all weights are equal /// - WRMSE = 0 indicates perfect prediction /// /// Kahan compensated summation prevents floating-point drift without periodic resync. /// [SkipLocalsInit] public sealed class Wrmse : AbstractBase { private readonly RingBuffer _weightedErrorBuffer; private readonly RingBuffer _weightBuffer; [StructLayout(LayoutKind.Auto)] private record struct State( double WeightedErrorSum, double WeightSum, double WeightedErrorComp, double WeightComp, double LastValidActual, double LastValidPredicted, double LastValidWeight); private State _state; private State _p_state; private const double DefaultWeight = 1.0; /// /// Creates WRMSE with specified period. /// /// Number of values to average (must be > 0) public Wrmse(int period) { if (period <= 0) { throw new ArgumentException("Period must be greater than 0", nameof(period)); } _weightedErrorBuffer = new RingBuffer(period); _weightBuffer = new RingBuffer(period); Name = $"Wrmse({period})"; WarmupPeriod = period; _state.LastValidWeight = DefaultWeight; _p_state.LastValidWeight = DefaultWeight; } /// /// True if the indicator has enough data to produce valid results. /// public override bool IsHot => _weightedErrorBuffer.IsFull; /// /// Period of the indicator. /// public int Period => _weightedErrorBuffer.Capacity; /// /// Updates the indicator with actual, predicted, and weight values. /// /// Actual value /// Predicted value /// Weight for this observation (default 1.0) /// Whether this is a new bar /// The calculated WRMSE value [MethodImpl(MethodImplOptions.AggressiveInlining)] public TValue Update(TValue actual, TValue predicted, double weight, bool isNew = true) { double actualVal = actual.Value; double predictedVal = predicted.Value; // Sanitize inputs if (!double.IsFinite(actualVal)) { actualVal = double.IsFinite(_state.LastValidActual) ? _state.LastValidActual : 0.0; } else { _state.LastValidActual = actualVal; } if (!double.IsFinite(predictedVal)) { predictedVal = double.IsFinite(_state.LastValidPredicted) ? _state.LastValidPredicted : 0.0; } else { _state.LastValidPredicted = predictedVal; } if (!double.IsFinite(weight) || weight < 0) { weight = _state.LastValidWeight; } else { _state.LastValidWeight = weight; } // Compute weighted squared error double diff = actualVal - predictedVal; double weightedError = weight * diff * diff; if (isNew) { _p_state = _state; double removedWeightedError = _weightedErrorBuffer.Count == _weightedErrorBuffer.Capacity ? _weightedErrorBuffer.Oldest : 0.0; { double delta = weightedError - removedWeightedError; double y = delta - _state.WeightedErrorComp; double t = _state.WeightedErrorSum + y; _state.WeightedErrorComp = (t - _state.WeightedErrorSum) - y; _state.WeightedErrorSum = t; } _weightedErrorBuffer.Add(weightedError); double removedWeight = _weightBuffer.Count == _weightBuffer.Capacity ? _weightBuffer.Oldest : 0.0; { double delta = weight - removedWeight; double y = delta - _state.WeightComp; double t = _state.WeightSum + y; _state.WeightComp = (t - _state.WeightSum) - y; _state.WeightSum = t; } _weightBuffer.Add(weight); } else { _state = _p_state; _weightedErrorBuffer.UpdateNewest(weightedError); _state.WeightedErrorSum = _weightedErrorBuffer.RecalculateSum(); _weightBuffer.UpdateNewest(weight); _state.WeightSum = _weightBuffer.RecalculateSum(); } // WRMSE = sqrt(Σ(w*e²) / Σ(w)) double result = _state.WeightSum > 1e-10 ? Math.Sqrt(_state.WeightedErrorSum / _state.WeightSum) : 0.0; Last = new TValue(actual.Time, result); PubEvent(Last, isNew); return Last; } /// /// Updates the indicator with actual and predicted values using default weight of 1.0. /// [MethodImpl(MethodImplOptions.AggressiveInlining)] public TValue Update(TValue actual, TValue predicted, bool isNew = true) { return Update(actual, predicted, DefaultWeight, isNew); } /// /// Updates the indicator with raw double values. /// [MethodImpl(MethodImplOptions.AggressiveInlining)] public TValue Update(double actual, double predicted, double weight, bool isNew = true) { var now = DateTime.UtcNow; return Update(new TValue(now, actual), new TValue(now, predicted), weight, isNew); } /// /// Updates the indicator with raw double values using default weight of 1.0. /// [MethodImpl(MethodImplOptions.AggressiveInlining)] public TValue Update(double actual, double predicted, bool isNew = true) { return Update(actual, predicted, DefaultWeight, isNew); } public override TValue Update(TValue input, bool isNew = true) { throw new NotSupportedException("WRMSE requires two inputs. Use Update(actual, predicted) or Update(actual, predicted, weight)."); } public override TSeries Update(TSeries source) { throw new NotSupportedException("WRMSE requires two inputs. Use Batch(actualSeries, predictedSeries, period) or Batch(actualSeries, predictedSeries, weightsSeries, period)."); } public override void Prime(ReadOnlySpan source, TimeSpan? step = null) { throw new NotSupportedException("WRMSE requires two inputs."); } public override void Reset() { _weightedErrorBuffer.Clear(); _weightBuffer.Clear(); _state = default; _state.LastValidWeight = DefaultWeight; _p_state = default; _p_state.LastValidWeight = DefaultWeight; Last = default; } /// /// Calculates WRMSE for entire series with uniform weights. /// public static TSeries Batch(TSeries actual, TSeries predicted, int period) { if (actual.Count != predicted.Count) { throw new ArgumentException("Actual and predicted series must have the same length", nameof(predicted)); } int len = actual.Count; var t = new List(len); var v = new List(len); CollectionsMarshal.SetCount(t, len); CollectionsMarshal.SetCount(v, len); var tSpan = CollectionsMarshal.AsSpan(t); var vSpan = CollectionsMarshal.AsSpan(v); Batch(actual.Values, predicted.Values, vSpan, period); actual.Times.CopyTo(tSpan); return new TSeries(t, v); } /// /// Calculates WRMSE for entire series with custom weights. /// public static TSeries Batch(TSeries actual, TSeries predicted, TSeries weights, int period) { if (actual.Count != predicted.Count || actual.Count != weights.Count) { throw new ArgumentException("All series must have the same length", nameof(weights)); } int len = actual.Count; var t = new List(len); var v = new List(len); CollectionsMarshal.SetCount(t, len); CollectionsMarshal.SetCount(v, len); var tSpan = CollectionsMarshal.AsSpan(t); var vSpan = CollectionsMarshal.AsSpan(v); Batch(actual.Values, predicted.Values, weights.Values, vSpan, period); actual.Times.CopyTo(tSpan); return new TSeries(t, v); } /// /// Batch calculation with uniform weights (reduces to RMSE behavior). /// Uses SIMD-accelerated computation via ErrorHelpers. /// [MethodImpl(MethodImplOptions.AggressiveInlining)] public static void Batch(ReadOnlySpan actual, ReadOnlySpan predicted, Span output, int period) { if (actual.Length != predicted.Length || actual.Length != output.Length) { throw new ArgumentException("All spans must have the same length", nameof(output)); } if (period <= 0) { throw new ArgumentException("Period must be greater than 0", nameof(period)); } int len = actual.Length; if (len == 0) { return; } // With uniform weights, WRMSE = RMSE const int StackAllocThreshold = 256; Span sqErrors = len <= StackAllocThreshold ? stackalloc double[len] : new double[len]; ErrorHelpers.ComputeSquaredErrors(actual, predicted, sqErrors); ErrorHelpers.ApplyRollingMeanSqrt(sqErrors, output, period); } /// /// Batch calculation with custom weights. /// Uses SIMD-accelerated computation via ErrorHelpers. /// [MethodImpl(MethodImplOptions.AggressiveInlining)] public static void Batch(ReadOnlySpan actual, ReadOnlySpan predicted, ReadOnlySpan weights, Span output, int period) { if (actual.Length != predicted.Length || actual.Length != weights.Length || actual.Length != output.Length) { throw new ArgumentException("All spans must have the same length", nameof(output)); } if (period <= 0) { throw new ArgumentException("Period must be greater than 0", nameof(period)); } int len = actual.Length; if (len == 0) { return; } const int StackAllocThreshold = 256; Span weightedErrors = len <= StackAllocThreshold ? stackalloc double[len] : new double[len]; ErrorHelpers.ComputeWeightedErrors(actual, predicted, weights, weightedErrors); ErrorHelpers.ApplyRollingWeightedMeanSqrt(weightedErrors, weights, output, period); } public static (TSeries Results, Wrmse Indicator) Calculate(TSeries actual, TSeries predicted, int period) { var indicator = new Wrmse(period); TSeries results = Batch(actual, predicted, period); return (results, indicator); } }