SIMD Refactor: Merge simd-dev into dev (#55)

Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
Co-authored-by: aider (openrouter/anthropic/claude-sonnet-4) <aider@aider.chat>
Co-authored-by: Warp <agent@warp.dev>
This commit is contained in:
Miha Kralj
2026-01-18 19:02:03 -08:00
committed by GitHub
co-authored by Claude Opus 4.5 aider Warp
parent 5bcdf8d614
commit 86fe32a682
1750 changed files with 198235 additions and 80539 deletions
+414
View File
@@ -0,0 +1,414 @@
namespace QuanTAlib.Tests;
public class WrmseTests
{
[Fact]
public void Constructor_ValidatesInput()
{
Assert.Throws<ArgumentException>(() => new Wrmse(0));
Assert.Throws<ArgumentException>(() => new Wrmse(-1));
var wrmse = new Wrmse(10);
Assert.NotNull(wrmse);
}
[Fact]
public void Properties_Accessible()
{
var wrmse = new Wrmse(10);
Assert.Equal(0, wrmse.Last.Value);
Assert.False(wrmse.IsHot);
Assert.Contains("Wrmse", wrmse.Name, StringComparison.Ordinal);
wrmse.Update(100, 105);
Assert.NotEqual(0, wrmse.Last.Time);
}
[Fact]
public void IsHot_BecomesTrueWhenBufferFull()
{
const int period = 5;
var wrmse = new Wrmse(period);
for (int i = 0; i < period - 1; i++)
{
Assert.False(wrmse.IsHot);
wrmse.Update(i * 10, i * 10 + 5);
}
wrmse.Update((period - 1) * 10, (period - 1) * 10 + 5);
Assert.True(wrmse.IsHot);
}
[Fact]
public void Wrmse_WithUniformWeights_EqualsRmse()
{
var wrmse = new Wrmse(5);
var rmse = new Rmse(5);
for (int i = 0; i < 20; i++)
{
wrmse.Update(i * 10, i * 10 + 7);
rmse.Update(i * 10, i * 10 + 7);
}
// With default weight of 1.0, WRMSE should equal RMSE
Assert.Equal(rmse.Last.Value, wrmse.Last.Value, 10);
}
[Fact]
public void Wrmse_CalculatesCorrectlyWithWeights()
{
var wrmse = new Wrmse(3);
// (10 - 15)² = 25, weight = 1.0
// Weighted error = 1.0 * 25 = 25, sum weights = 1.0
// WRMSE = √(25/1) = 5
var res1 = wrmse.Update(10, 15, 1.0);
Assert.Equal(5.0, res1.Value, 10);
// (20 - 30)² = 100, weight = 2.0
// Weighted errors = 25 + 200 = 225, sum weights = 1 + 2 = 3
// WRMSE = √(225/3) = √75
var res2 = wrmse.Update(20, 30, 2.0);
Assert.Equal(Math.Sqrt(75.0), res2.Value, 10);
// (30 - 25)² = 25, weight = 3.0
// Weighted errors = 25 + 200 + 75 = 300, sum weights = 1 + 2 + 3 = 6
// WRMSE = √(300/6) = √50
var res3 = wrmse.Update(30, 25, 3.0);
Assert.Equal(Math.Sqrt(50.0), res3.Value, 10);
}
[Fact]
public void Wrmse_HigherWeightsHaveMoreInfluence()
{
var wrmse1 = new Wrmse(2);
var wrmse2 = new Wrmse(2);
// First scenario: low weight on large error
wrmse1.Update(10, 10, 10.0); // error=0, weight=10
wrmse1.Update(10, 20, 1.0); // error=100, weight=1
// Second scenario: high weight on large error
wrmse2.Update(10, 10, 1.0); // error=0, weight=1
wrmse2.Update(10, 20, 10.0); // error=100, weight=10
// wrmse2 should be higher because the large error has more weight
Assert.True(wrmse2.Last.Value > wrmse1.Last.Value);
}
[Fact]
public void Wrmse_PerfectPrediction_ReturnsZero()
{
var wrmse = new Wrmse(5);
for (int i = 0; i < 10; i++)
{
wrmse.Update(i * 10, i * 10, i + 1.0);
}
Assert.Equal(0.0, wrmse.Last.Value, 10);
}
[Fact]
public void Wrmse_ConstantError_ConstantWeight()
{
var wrmse = new Wrmse(5);
for (int i = 0; i < 10; i++)
{
wrmse.Update(100, 110, 2.0); // Constant error of 10, weight of 2
}
// Weighted error = 2 * 100 = 200, sum weights = 2
// WRMSE = √(200/2) = √100 = 10
Assert.Equal(10.0, wrmse.Last.Value, 10);
}
[Fact]
public void Calc_IsNew_False_UpdatesValue()
{
var wrmse = new Wrmse(10);
wrmse.Update(100, 110, 1.0);
wrmse.Update(100, 120, 1.0, isNew: true);
double beforeUpdate = wrmse.Last.Value;
wrmse.Update(100, 130, 1.0, isNew: false);
double afterUpdate = wrmse.Last.Value;
Assert.NotEqual(beforeUpdate, afterUpdate);
}
[Fact]
public void IterativeCorrections_RestoreToOriginalState()
{
var wrmse = new Wrmse(5);
double tenthActual = 0;
double tenthPredicted = 0;
double tenthWeight = 0;
for (int i = 0; i < 10; i++)
{
tenthActual = i * 10;
tenthPredicted = i * 10 + 5;
tenthWeight = i + 1.0;
wrmse.Update(tenthActual, tenthPredicted, tenthWeight);
}
double stateAfterTen = wrmse.Last.Value;
for (int i = 0; i < 5; i++)
{
wrmse.Update(100 + i, 200 + i, 5.0, isNew: false);
}
wrmse.Update(tenthActual, tenthPredicted, tenthWeight, isNew: false);
Assert.Equal(stateAfterTen, wrmse.Last.Value, 10);
}
[Fact]
public void Reset_ClearsState()
{
var wrmse = new Wrmse(5);
for (int i = 0; i < 10; i++)
{
wrmse.Update(i * 10, i * 10 + 5, i + 1.0);
}
Assert.True(wrmse.IsHot);
wrmse.Reset();
Assert.False(wrmse.IsHot);
Assert.Equal(0, wrmse.Last.Value);
}
[Fact]
public void NaN_Input_UsesLastValidValue()
{
var wrmse = new Wrmse(5);
wrmse.Update(100, 110, 1.0);
wrmse.Update(110, 120, 2.0);
var result = wrmse.Update(double.NaN, double.NaN, double.NaN);
Assert.True(double.IsFinite(result.Value));
}
[Fact]
public void NegativeWeight_UsesLastValidWeight()
{
var wrmse = new Wrmse(5);
wrmse.Update(100, 110, 2.0);
var beforeResult = wrmse.Last.Value;
wrmse.Update(100, 110, -1.0); // Negative weight should use last valid (2.0)
// Both should compute same result since same weight is used
Assert.Equal(beforeResult, wrmse.Last.Value, 10);
}
[Fact]
public void Wrmse_Throws_On_Single_Input()
{
var wrmse = new Wrmse(10);
Assert.Throws<NotSupportedException>(() => wrmse.Update(new TValue(DateTime.UtcNow, 1)));
Assert.Throws<NotSupportedException>(() => wrmse.Update(new TSeries()));
Assert.Throws<NotSupportedException>(() => wrmse.Prime([1, 2, 3]));
}
[Fact]
public void BatchSpan_UniformWeights_MatchesStreaming()
{
int period = 5;
int count = 100;
var gbm = new GBM(startPrice: 100, mu: 0.05, sigma: 0.2, seed: 123);
double[] actual = new double[count];
double[] predicted = new double[count];
for (int i = 0; i < count; i++)
{
var bar = gbm.Next();
actual[i] = bar.Close;
predicted[i] = bar.Close * 1.05 + 2;
}
var wrmse = new Wrmse(period);
var streamingResults = new double[count];
for (int i = 0; i < count; i++)
{
streamingResults[i] = wrmse.Update(actual[i], predicted[i]).Value;
}
double[] batchResults = new double[count];
Wrmse.Batch(actual, predicted, batchResults, period);
for (int i = 0; i < count; i++)
{
Assert.Equal(streamingResults[i], batchResults[i], 9);
}
}
[Fact]
public void BatchSpan_WithWeights_MatchesStreaming()
{
int period = 5;
int count = 100;
var gbm = new GBM(startPrice: 100, mu: 0.05, sigma: 0.2, seed: 456);
double[] actual = new double[count];
double[] predicted = new double[count];
double[] weights = new double[count];
for (int i = 0; i < count; i++)
{
var bar = gbm.Next();
actual[i] = bar.Close;
predicted[i] = bar.Close * 1.05 + 2;
weights[i] = (i % 5) + 1.0; // Varying weights 1-5
}
var wrmse = new Wrmse(period);
var streamingResults = new double[count];
for (int i = 0; i < count; i++)
{
streamingResults[i] = wrmse.Update(actual[i], predicted[i], weights[i]).Value;
}
double[] batchResults = new double[count];
Wrmse.Batch(actual, predicted, weights, batchResults, period);
for (int i = 0; i < count; i++)
{
Assert.Equal(streamingResults[i], batchResults[i], 9);
}
}
[Fact]
public void BatchSpan_ValidatesInput()
{
double[] actual = [1, 2, 3, 4, 5];
double[] predicted = [1, 2, 3, 4, 5];
double[] weights = [1, 1, 1, 1, 1];
double[] output = new double[5];
Assert.Throws<ArgumentException>(() =>
Wrmse.Batch(actual.AsSpan(), predicted.AsSpan(), output.AsSpan(), 0));
Assert.Throws<ArgumentException>(() =>
Wrmse.Batch(actual.AsSpan(), predicted.AsSpan(), new double[3].AsSpan(), 3));
Assert.Throws<ArgumentException>(() =>
Wrmse.Batch(actual.AsSpan(), predicted.AsSpan(), weights.AsSpan(), new double[3].AsSpan(), 3));
}
[Fact]
public void Calculate_Works_UniformWeights()
{
var actual = new TSeries();
var predicted = new TSeries();
var now = DateTime.UtcNow;
for (int i = 0; i < 10; i++)
{
actual.Add(now.AddMinutes(i), i * 10);
predicted.Add(now.AddMinutes(i), i * 10 + 5);
}
var results = Wrmse.Calculate(actual, predicted, 3);
Assert.Equal(10, results.Count);
// All errors are 5, MSE = 25, RMSE = 5
Assert.Equal(5.0, results.Last.Value, 10);
}
[Fact]
public void Calculate_Works_CustomWeights()
{
var actual = new TSeries();
var predicted = new TSeries();
var weights = new TSeries();
var now = DateTime.UtcNow;
for (int i = 0; i < 10; i++)
{
actual.Add(now.AddMinutes(i), 100.0);
predicted.Add(now.AddMinutes(i), 110.0); // Error = 10, Squared = 100
weights.Add(now.AddMinutes(i), 2.0); // Weight = 2
}
var results = Wrmse.Calculate(actual, predicted, weights, 3);
Assert.Equal(10, results.Count);
// Weighted error = 2 * 100 = 200 per point, sum weights = 6 (period=3)
// WRMSE = √(600/6) = √100 = 10
Assert.Equal(10.0, results.Last.Value, 10);
}
[Fact]
public void Calculate_ThrowsOnMismatchedLengths()
{
var actual = new TSeries();
var predicted = new TSeries();
var now = DateTime.UtcNow;
for (int i = 0; i < 10; i++)
{
actual.Add(now.AddMinutes(i), i * 10);
if (i < 5) predicted.Add(now.AddMinutes(i), i * 10 + 5);
}
Assert.Throws<ArgumentException>(() => Wrmse.Calculate(actual, predicted, 3));
}
[Fact]
public void Calculate_ThrowsOnMismatchedWeightsLength()
{
var actual = new TSeries();
var predicted = new TSeries();
var weights = new TSeries();
var now = DateTime.UtcNow;
for (int i = 0; i < 10; i++)
{
actual.Add(now.AddMinutes(i), i * 10);
predicted.Add(now.AddMinutes(i), i * 10 + 5);
if (i < 5) weights.Add(now.AddMinutes(i), 1.0);
}
Assert.Throws<ArgumentException>(() => Wrmse.Calculate(actual, predicted, weights, 3));
}
[Fact]
public void UniformWeightsBatch_MatchesRmseBatch()
{
int period = 5;
int count = 50;
var gbm = new GBM(startPrice: 100, mu: 0.05, sigma: 0.2, seed: 789);
double[] actual = new double[count];
double[] predicted = new double[count];
for (int i = 0; i < count; i++)
{
var bar = gbm.Next();
actual[i] = bar.Close;
predicted[i] = bar.Close * 1.03;
}
double[] wrmseResults = new double[count];
double[] rmseResults = new double[count];
Wrmse.Batch(actual, predicted, wrmseResults, period);
Rmse.Batch(actual, predicted, rmseResults, period);
for (int i = 0; i < count; i++)
{
Assert.Equal(rmseResults[i], wrmseResults[i], 9);
}
}
}
+301
View File
@@ -0,0 +1,301 @@
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
namespace QuanTAlib;
/// <summary>
/// WRMSE: Weighted Root Mean Squared Error
/// </summary>
/// <remarks>
/// 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 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
/// </remarks>
[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 LastValidActual,
double LastValidPredicted,
double LastValidWeight,
int TickCount);
private State _state;
private State _p_state;
private const int ResyncInterval = 1000;
private const double DefaultWeight = 1.0;
/// <summary>
/// Creates WRMSE with specified period.
/// </summary>
/// <param name="period">Number of values to average (must be > 0)</param>
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;
}
/// <summary>
/// True if the indicator has enough data to produce valid results.
/// </summary>
public override bool IsHot => _weightedErrorBuffer.IsFull;
/// <summary>
/// Period of the indicator.
/// </summary>
public int Period => _weightedErrorBuffer.Capacity;
/// <summary>
/// Updates the indicator with actual, predicted, and weight values.
/// </summary>
/// <param name="actual">Actual value</param>
/// <param name="predicted">Predicted value</param>
/// <param name="weight">Weight for this observation (default 1.0)</param>
/// <param name="isNew">Whether this is a new bar</param>
/// <returns>The calculated WRMSE value</returns>
[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;
_state.WeightedErrorSum = _state.WeightedErrorSum - removedWeightedError + weightedError;
_weightedErrorBuffer.Add(weightedError);
double removedWeight = _weightBuffer.Count == _weightBuffer.Capacity
? _weightBuffer.Oldest : 0.0;
_state.WeightSum = _state.WeightSum - removedWeight + weight;
_weightBuffer.Add(weight);
_state.TickCount++;
if (_weightedErrorBuffer.IsFull && _state.TickCount >= ResyncInterval)
{
_state.TickCount = 0;
_state.WeightedErrorSum = _weightedErrorBuffer.RecalculateSum();
_state.WeightSum = _weightBuffer.RecalculateSum();
}
}
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;
}
/// <summary>
/// Updates the indicator with actual and predicted values using default weight of 1.0.
/// </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public TValue Update(TValue actual, TValue predicted, bool isNew = true)
{
return Update(actual, predicted, DefaultWeight, isNew);
}
/// <summary>
/// Updates the indicator with raw double values.
/// </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public TValue Update(double actual, double predicted, double weight, bool isNew = true)
{
return Update(new TValue(DateTime.UtcNow, actual), new TValue(DateTime.UtcNow, predicted), weight, isNew);
}
/// <summary>
/// Updates the indicator with raw double values using default weight of 1.0.
/// </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public TValue Update(double actual, double predicted, bool isNew = true)
{
return Update(actual, predicted, DefaultWeight, isNew);
}
/// <inheritdoc/>
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).");
}
/// <inheritdoc/>
public override TSeries Update(TSeries source)
{
throw new NotSupportedException("WRMSE requires two inputs. Use Calculate(actualSeries, predictedSeries, period) or Calculate(actualSeries, predictedSeries, weightsSeries, period).");
}
/// <inheritdoc/>
public override void Prime(ReadOnlySpan<double> source, TimeSpan? step = null)
{
throw new NotSupportedException("WRMSE requires two inputs.");
}
/// <inheritdoc/>
public override void Reset()
{
_weightedErrorBuffer.Clear();
_weightBuffer.Clear();
_state = default;
_state.LastValidWeight = DefaultWeight;
_p_state = default;
_p_state.LastValidWeight = DefaultWeight;
Last = default;
}
/// <summary>
/// Calculates WRMSE for entire series with uniform weights.
/// </summary>
public static TSeries Calculate(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<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(actual.Values, predicted.Values, vSpan, period);
actual.Times.CopyTo(tSpan);
return new TSeries(t, v);
}
/// <summary>
/// Calculates WRMSE for entire series with custom weights.
/// </summary>
public static TSeries Calculate(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<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(actual.Values, predicted.Values, weights.Values, vSpan, period);
actual.Times.CopyTo(tSpan);
return new TSeries(t, v);
}
/// <summary>
/// Batch calculation with uniform weights (reduces to RMSE behavior).
/// Uses SIMD-accelerated computation via ErrorHelpers.
/// </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static void Batch(ReadOnlySpan<double> actual, ReadOnlySpan<double> predicted, Span<double> 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<double> sqErrors = len <= StackAllocThreshold
? stackalloc double[len]
: new double[len];
ErrorHelpers.ComputeSquaredErrors(actual, predicted, sqErrors);
ErrorHelpers.ApplyRollingMeanSqrt(sqErrors, output, period);
}
/// <summary>
/// Batch calculation with custom weights.
/// Uses SIMD-accelerated computation via ErrorHelpers.
/// </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static void Batch(ReadOnlySpan<double> actual, ReadOnlySpan<double> predicted, ReadOnlySpan<double> weights, Span<double> 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<double> weightedErrors = len <= StackAllocThreshold
? stackalloc double[len]
: new double[len];
ErrorHelpers.ComputeWeightedErrors(actual, predicted, weights, weightedErrors);
ErrorHelpers.ApplyRollingWeightedMeanSqrt(weightedErrors, weights, output, period);
}
}
+172
View File
@@ -0,0 +1,172 @@
# WRMSE: Weighted Root Mean Squared Error
> "Not all errors are created equal—WRMSE lets you decide which ones matter most."
WRMSE extends the classic RMSE by incorporating weights for each observation, enabling analysts to emphasize critical data points such as recent observations, high-volume periods, or specific market regimes. When all weights are equal, WRMSE reduces exactly to RMSE, making it a strict generalization. This implementation uses dual RingBuffers for O(1) streaming updates with periodic resync to manage floating-point drift.
## Historical Context
Root Mean Squared Error has been a foundational metric in statistics and signal processing since Gauss's work on least squares in the early 19th century. The weighted variant emerged naturally from generalized least squares theory, where heteroscedasticity (non-constant variance) necessitates giving different importance to different observations.
In financial contexts, weighting becomes essential because market conditions vary significantly: a 1% error during a flash crash carries different implications than the same error during low-volatility consolidation. Volume-weighted errors, recency-weighted errors, and regime-adaptive weighting schemes all build on this foundation.
The implementation here maintains exact mathematical equivalence to RMSE when weights are uniform, verified through comparison tests against our standard RMSE indicator.
## Architecture & Physics
The indicator maintains two parallel RingBuffers tracking weighted squared errors and weights separately, enabling proper normalization as the window slides.
### 1. Dual Buffer State Management
The state tracks running sums for both numerator and denominator:
$$
\text{State} = \begin{cases}
\text{WeightedErrorSum} & \sum_{i \in W} w_i \cdot (a_i - p_i)^2 \\
\text{WeightSum} & \sum_{i \in W} w_i
\end{cases}
$$
where $W$ is the current window of observations.
### 2. O(1) Streaming Updates
Each new observation triggers constant-time updates via the sliding window pattern:
$$
\text{WeightedErrorSum}_{t} = \text{WeightedErrorSum}_{t-1} - \text{oldest}_e + w_t \cdot (a_t - p_t)^2
$$
$$
\text{WeightSum}_{t} = \text{WeightSum}_{t-1} - \text{oldest}_w + w_t
$$
### 3. Bar Correction Support
The `isNew=false` pattern enables bar correction for live trading scenarios where the current bar's values may update multiple times before close. State rollback uses `_p_state` (previous valid state) to restore buffers to the pre-correction position.
### 4. Floating-Point Drift Mitigation
Running sums accumulate floating-point errors over time. Periodic resync (every 1000 ticks by default) recalculates sums from buffer contents to bound drift.
## Mathematical Foundation
### Core Formula
$$
\text{WRMSE} = \sqrt{\frac{\sum_{i=1}^{n} w_i \cdot (a_i - p_i)^2}{\sum_{i=1}^{n} w_i}}
$$
where:
- $a_i$ = actual value at position $i$
- $p_i$ = predicted value at position $i$
- $w_i$ = weight at position $i$ (must be non-negative)
- $n$ = window size (period)
### Reduction to RMSE
When all weights are equal ($w_i = c$ for constant $c$):
$$
\text{WRMSE} = \sqrt{\frac{c \cdot \sum_{i=1}^{n} (a_i - p_i)^2}{n \cdot c}} = \sqrt{\frac{\sum_{i=1}^{n} (a_i - p_i)^2}{n}} = \text{RMSE}
$$
### Weight Normalization
The denominator $\sum w_i$ ensures the metric remains scale-invariant with respect to weight magnitude. Doubling all weights produces identical results.
### NaN/Invalid Value Handling
Invalid inputs (NaN, Infinity, negative weights) substitute the last valid value:
$$
v_t = \begin{cases}
v_t & \text{if } v_t \text{ is finite and valid} \\
v_{\text{last valid}} & \text{otherwise}
\end{cases}
$$
## Performance Profile
### Operation Count (Streaming Mode, Scalar)
| Operation | Count | Cost (cycles) | Subtotal |
| :--- | :---: | :---: | :---: |
| Validation/NaN checks | 3 | 1 | 3 |
| SUB (diff) | 1 | 1 | 1 |
| MUL (diff², weight×diff²) | 2 | 3 | 6 |
| ADD/SUB (running sums) | 4 | 1 | 4 |
| DIV | 1 | 15 | 15 |
| SQRT | 1 | 15 | 15 |
| CMP (weight threshold) | 1 | 1 | 1 |
| **Total** | **~13** | — | **~45 cycles** |
The dominant costs are DIV and SQRT (67% combined), consistent with other RMSE-family indicators.
### Batch Mode (SIMD/FMA)
For uniform weights, the batch path delegates to the same SIMD infrastructure as RMSE:
| Operation | Scalar Ops | SIMD Ops (AVX2) | Speedup |
| :--- | :---: | :---: | :---: |
| Squared error computation | N | N/4 | 4× |
| Rolling mean | N | N (sequential) | 1× |
For weighted batch computation, an additional weight multiplication occurs in the SIMD loop, but the rolling aggregation remains sequential due to the cumulative nature of the window.
**Per-bar efficiency:**
| Mode | Cycles/bar | Notes |
| :--- | :---: | :--- |
| Streaming (uniform weights) | ~45 | Uses default weight 1.0 |
| Streaming (custom weights) | ~48 | Additional weight validation |
| Batch SIMD (uniform) | ~30 | Amortized over 4-wide vectors |
| Batch SIMD (weighted) | ~35 | Weight multiplication in SIMD |
### Quality Metrics
| Metric | Score | Notes |
| :--- | :---: | :--- |
| **Accuracy** | 10/10 | Exact formula implementation |
| **Flexibility** | 9/10 | Custom weights enable regime adaptation |
| **Interpretability** | 8/10 | Same units as input, but weights add complexity |
| **Robustness** | 9/10 | NaN handling, negative weight rejection |
| **Performance** | 8/10 | O(1) streaming, SIMD batch support |
## Validation
| Library | Status | Notes |
| :--- | :---: | :--- |
| **Internal RMSE** | ✅ | Uniform weights match RMSE within 1e-10 |
| **Manual Calculation** | ✅ | Step-by-step verification in tests |
| **TA-Lib** | N/A | No WRMSE implementation |
| **Skender** | N/A | No WRMSE implementation |
| **Tulip** | N/A | No WRMSE implementation |
WRMSE is validated by:
1. Mathematical equivalence to RMSE with uniform weights (BatchSpan_UniformWeights_MatchesStreaming, UniformWeightsBatch_MatchesRmseBatch)
2. Manual calculation verification (Wrmse_CalculatesCorrectlyWithWeights)
3. Weight influence tests (Wrmse_HigherWeightsHaveMoreInfluence)
4. Streaming/batch parity tests for both uniform and custom weights
## Common Pitfalls
1. **Weight Interpretation**: Weights are multiplied by squared errors, not linear errors. A weight of 2.0 gives that observation twice the influence in the squared error sum, which may not align with intuitive expectations.
2. **Zero Weight Sum**: If all weights in the window sum to effectively zero (< 1e-10), the result is 0.0 to avoid division by zero. This edge case should be rare in practice.
3. **Negative Weights**: Negative weights are rejected and replaced with the last valid weight. Consider using absolute values or clamping in upstream processing if your weight source may produce negatives.
4. **Memory Footprint**: Each instance allocates two RingBuffers of size `period`, doubling memory compared to unweighted RMSE. For a period of 100:
- 2 buffers × 100 doubles × 8 bytes = 1,600 bytes per instance
- 10,000 concurrent instances ≈ 15.3 MB
5. **Warmup Period**: The indicator requires `period` observations before `IsHot` becomes true. During warmup, results use the available data but may not represent the full window statistics.
6. **Bar Correction**: When using `isNew=false`, ensure you're correcting the most recent observation. Multiple corrections without intervening new bars work correctly, but the pattern assumes temporal locality.
## References
- Aitken, A.C. (1936). "On Least Squares and Linear Combinations of Observations." *Proceedings of the Royal Society of Edinburgh*.
- Gauss, C.F. (1809). *Theoria Motus Corporum Coelestium*. (Foundation of least squares theory)
- Greene, W.H. (2012). *Econometric Analysis*. 7th ed. Chapter 9: Generalized Least Squares.