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
+385
View File
@@ -0,0 +1,385 @@
namespace QuanTAlib.Tests;
public class MeTests
{
[Fact]
public void Constructor_ValidatesInput()
{
Assert.Throws<ArgumentException>(() => new Me(0));
Assert.Throws<ArgumentException>(() => new Me(-1));
var me = new Me(10);
Assert.NotNull(me);
}
[Fact]
public void Properties_Accessible()
{
var me = new Me(10);
Assert.Equal(0, me.Last.Value);
Assert.False(me.IsHot);
Assert.Contains("Me", me.Name, StringComparison.Ordinal);
me.Update(100, 105);
Assert.NotEqual(0, me.Last.Time);
}
[Fact]
public void IsHot_BecomesTrueWhenBufferFull()
{
const int period = 5;
var me = new Me(period);
for (int i = 0; i < period - 1; i++)
{
Assert.False(me.IsHot, $"IsHot should be false at index {i}");
me.Update(i * 10, i * 10 + 5);
}
me.Update((period - 1) * 10, (period - 1) * 10 + 5);
Assert.True(me.IsHot, "IsHot should be true after period updates");
}
[Fact]
public void Me_CalculatesCorrectly()
{
var me = new Me(3);
// 10 - 15 = -5
var res1 = me.Update(10, 15);
Assert.Equal(-5.0, res1.Value, 10);
// 20 - 30 = -10, Mean = (-5 + -10) / 2 = -7.5
var res2 = me.Update(20, 30);
Assert.Equal(-7.5, res2.Value, 10);
// 30 - 25 = 5, Mean = (-5 + -10 + 5) / 3 = -10/3
var res3 = me.Update(30, 25);
Assert.Equal(-10.0 / 3.0, res3.Value, 10);
// 40 - 35 = 5, Window slides: (-10 + 5 + 5) / 3 = 0
var res4 = me.Update(40, 35);
Assert.Equal(0.0, res4.Value, 10);
}
[Fact]
public void Me_PerfectPrediction_ReturnsZero()
{
var me = new Me(5);
for (int i = 0; i < 10; i++)
{
me.Update(i * 10, i * 10); // Perfect prediction
}
Assert.Equal(0.0, me.Last.Value, 10);
}
[Fact]
public void Me_ConstantUnderPrediction_ReturnsPositive()
{
var me = new Me(5);
for (int i = 0; i < 10; i++)
{
me.Update(110, 100); // Actual > predicted (under-prediction)
}
Assert.Equal(10.0, me.Last.Value, 10);
}
[Fact]
public void Me_ConstantOverPrediction_ReturnsNegative()
{
var me = new Me(5);
for (int i = 0; i < 10; i++)
{
me.Update(100, 110); // Actual < predicted (over-prediction)
}
Assert.Equal(-10.0, me.Last.Value, 10);
}
[Fact]
public void Me_BalancedErrors_CancelOut()
{
var me = new Me(4);
// Errors: +10, -10, +10, -10 should cancel out
me.Update(110, 100); // +10
me.Update(90, 100); // -10
me.Update(110, 100); // +10
me.Update(90, 100); // -10
Assert.Equal(0.0, me.Last.Value, 10);
}
[Fact]
public void Me_PreservesSign()
{
var me = new Me(3);
// Error = 15 - 10 = 5 (under-prediction)
me.Update(15, 10);
Assert.True(me.Last.Value > 0, "ME should be positive for under-prediction");
var me2 = new Me(3);
// Error = 10 - 15 = -5 (over-prediction)
me2.Update(10, 15);
Assert.True(me2.Last.Value < 0, "ME should be negative for over-prediction");
}
[Fact]
public void Calc_IsNew_AcceptsParameter()
{
var me = new Me(10);
me.Update(100, 110, isNew: true);
double value1 = me.Last.Value;
me.Update(100, 120, isNew: true);
double value2 = me.Last.Value;
Assert.NotEqual(value1, value2);
}
[Fact]
public void Calc_IsNew_False_UpdatesValue()
{
var me = new Me(10);
me.Update(100, 110);
me.Update(100, 120, isNew: true);
double beforeUpdate = me.Last.Value;
me.Update(100, 130, isNew: false);
double afterUpdate = me.Last.Value;
Assert.NotEqual(beforeUpdate, afterUpdate);
}
[Fact]
public void IterativeCorrections_RestoreToOriginalState()
{
var me = new Me(5);
double tenthActual = 0;
double tenthPredicted = 0;
// Feed 10 updates
for (int i = 0; i < 10; i++)
{
tenthActual = i * 10;
tenthPredicted = i * 10 + 5;
me.Update(tenthActual, tenthPredicted);
}
double stateAfterTen = me.Last.Value;
// Apply 5 corrections with isNew=false
for (int i = 0; i < 5; i++)
{
me.Update(100 + i, 200 + i, isNew: false);
}
// Restore to original values
me.Update(tenthActual, tenthPredicted, isNew: false);
Assert.Equal(stateAfterTen, me.Last.Value, 10);
}
[Fact]
public void Reset_ClearsState()
{
var me = new Me(5);
for (int i = 0; i < 10; i++)
{
me.Update(i * 10, i * 10 + 5);
}
Assert.True(me.IsHot);
me.Reset();
Assert.False(me.IsHot);
Assert.Equal(0, me.Last.Value);
}
[Fact]
public void NaN_Input_UsesLastValidValue()
{
var me = new Me(5);
me.Update(100, 110);
me.Update(110, 120);
me.Update(120, 130);
var result = me.Update(double.NaN, double.NaN);
Assert.True(double.IsFinite(result.Value));
}
[Fact]
public void Infinity_Input_UsesLastValidValue()
{
var me = new Me(5);
me.Update(100, 110);
me.Update(110, 120);
var result = me.Update(double.PositiveInfinity, double.NegativeInfinity);
Assert.True(double.IsFinite(result.Value));
}
[Fact]
public void MultipleNaN_ContinuesWithLastValid()
{
var me = new Me(5);
me.Update(100, 110);
me.Update(110, 120);
me.Update(120, 130);
var r1 = me.Update(double.NaN, double.NaN);
var r2 = me.Update(double.NaN, double.NaN);
var r3 = me.Update(double.NaN, double.NaN);
Assert.True(double.IsFinite(r1.Value));
Assert.True(double.IsFinite(r2.Value));
Assert.True(double.IsFinite(r3.Value));
}
[Fact]
public void Me_Throws_On_Single_Input()
{
var me = new Me(10);
Assert.Throws<NotSupportedException>(() => me.Update(new TValue(DateTime.UtcNow, 1)));
Assert.Throws<NotSupportedException>(() => me.Update(new TSeries()));
Assert.Throws<NotSupportedException>(() => me.Prime([1, 2, 3]));
}
[Fact]
public void BatchSpan_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; // Offset prediction
}
// Streaming
var me = new Me(period);
var streamingResults = new double[count];
for (int i = 0; i < count; i++)
{
streamingResults[i] = me.Update(actual[i], predicted[i]).Value;
}
// Batch
double[] batchResults = new double[count];
Me.Batch(actual, predicted, batchResults, period);
// Compare
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[] output = new double[5];
double[] wrongSizeOutput = new double[3];
double[] wrongSizePredicted = new double[3];
// Period must be > 0
Assert.Throws<ArgumentException>(() =>
Me.Batch(actual.AsSpan(), predicted.AsSpan(), output.AsSpan(), 0));
Assert.Throws<ArgumentException>(() =>
Me.Batch(actual.AsSpan(), predicted.AsSpan(), output.AsSpan(), -1));
// Output must be same length as source
Assert.Throws<ArgumentException>(() =>
Me.Batch(actual.AsSpan(), predicted.AsSpan(), wrongSizeOutput.AsSpan(), 3));
// Predicted must be same length as actual
Assert.Throws<ArgumentException>(() =>
Me.Batch(actual.AsSpan(), wrongSizePredicted.AsSpan(), output.AsSpan(), 3));
}
[Fact]
public void Calculate_Works()
{
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 = Me.Calculate(actual, predicted, 3);
Assert.Equal(10, results.Count);
// All errors are -5, so ME should be -5
Assert.Equal(-5.0, results.Last.Value, 10);
}
[Fact]
public void Calculate_ValidatesMismatchedLengths()
{
var actual = new TSeries();
var predicted = new TSeries();
for (int i = 0; i < 10; i++) actual.Add(DateTime.UtcNow, i);
for (int i = 0; i < 5; i++) predicted.Add(DateTime.UtcNow, i);
Assert.Throws<ArgumentException>(() => Me.Calculate(actual, predicted, 3));
}
[Fact]
public void BatchSpan_HandlesNaN()
{
double[] actual = [100, 110, double.NaN, 130, 140];
double[] predicted = [105, 115, 125, double.NaN, 145];
double[] output = new double[5];
Me.Batch(actual, predicted, output, 3);
foreach (var val in output)
{
Assert.True(double.IsFinite(val), $"Expected finite value but got {val}");
}
}
[Fact]
public void Me_Resync_Works()
{
var me = new Me(5);
// Force many updates to trigger resync (ResyncInterval = 1000)
for (int i = 0; i < 1100; i++)
{
me.Update(110, 100); // Constant error of +10
}
// After resync, result should still be correct
Assert.Equal(10.0, me.Last.Value, 10);
}
}
+78
View File
@@ -0,0 +1,78 @@
using System.Buffers;
using System.Runtime.CompilerServices;
namespace QuanTAlib;
/// <summary>
/// ME: Mean Error (also known as Mean Bias Error)
/// </summary>
/// <remarks>
/// ME measures the average error between actual and predicted values,
/// preserving the sign to indicate systematic bias in predictions.
///
/// Formula:
/// ME = (1/n) * Σ(actual - predicted)
///
/// Key properties:
/// - Can be positive or negative
/// - Positive ME indicates under-prediction (actual > predicted)
/// - Negative ME indicates over-prediction (actual &lt; predicted)
/// - ME = 0 indicates no systematic bias (but not necessarily accurate predictions)
/// - Errors can cancel out, hiding large individual errors
/// </remarks>
[SkipLocalsInit]
public sealed class Me : BiInputIndicatorBase
{
/// <summary>
/// Creates ME with specified period.
/// </summary>
/// <param name="period">Number of values to average (must be > 0)</param>
public Me(int period) : base(period, $"Me({period})") { }
/// <inheritdoc/>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
protected override double ComputeError(double actual, double predicted)
{
// ME preserves sign: actual - predicted
return actual - predicted;
}
/// <summary>
/// Calculates ME for entire series.
/// </summary>
public static TSeries Calculate(TSeries actual, TSeries predicted, int period)
=> CalculateImpl(actual, predicted, period, Batch);
/// <summary>
/// Batch calculation using signed error computation with rolling mean.
/// </summary>
public static void Batch(ReadOnlySpan<double> actual, ReadOnlySpan<double> predicted, Span<double> output, int period)
{
ValidateBatchInputs(actual, predicted, output, period);
int len = actual.Length;
if (len == 0) return;
const int StackAllocThreshold = 256;
if (len <= StackAllocThreshold)
{
Span<double> errors = stackalloc double[len];
ErrorHelpers.ComputeSignedErrors(actual, predicted, errors);
ErrorHelpers.ApplyRollingMean(errors, output, period);
}
else
{
double[] rented = ArrayPool<double>.Shared.Rent(len);
try
{
Span<double> errors = rented.AsSpan(0, len);
ErrorHelpers.ComputeSignedErrors(actual, predicted, errors);
ErrorHelpers.ApplyRollingMean(errors, output, period);
}
finally
{
ArrayPool<double>.Shared.Return(rented);
}
}
}
}
+144
View File
@@ -0,0 +1,144 @@
# ME: Mean Error (Mean Bias Error)
> "Sometimes you need to know not just how wrong you are, but which direction you're wrong in."
Mean Error (ME), also known as Mean Bias Error, measures the average error between actual and predicted values while preserving the sign. Unlike MAE, ME reveals systematic bias in predictions: whether a model consistently over-predicts or under-predicts.
## Historical Context
ME is one of the fundamental error metrics in statistics and forecasting. While MAE and MSE focus on error magnitude, ME fills the critical role of detecting directional bias. A model could have low MAE but significant ME, indicating consistent over or under-prediction that cancels out when measuring magnitude alone.
## Architecture & Physics
ME preserves the sign of errors, allowing positive and negative errors to cancel each other. This makes it ideal for detecting systematic bias but unsuitable for measuring prediction accuracy alone.
### Properties
* **Can be negative**: ME can be positive, negative, or zero
* **Positive ME**: Model under-predicts (actual > predicted on average)
* **Negative ME**: Model over-predicts (actual < predicted on average)
* **Zero ME**: No systematic bias (but not necessarily accurate)
* **Same units**: ME is in the same units as the original data
* **Cancellation**: Errors can cancel out, hiding large individual errors
## Mathematical Foundation
### 1. Error Calculation
For each observation, calculate the signed difference between actual and predicted values:
$$e_i = y_i - \hat{y}_i$$
Where:
* $y_i$ = actual value
* $\hat{y}_i$ = predicted value
### 2. Mean Calculation
Average the errors over the period:
$$ME = \frac{1}{n} \sum_{i=1}^{n} (y_i - \hat{y}_i)$$
### 3. Running Update (O(1))
QuanTAlib uses a ring buffer with running sum for O(1) updates:
$$S_{new} = S_{old} - e_{oldest} + e_{newest}$$
$$ME = \frac{S_{new}}{n}$$
## Implementation Details
### Usage Patterns
```csharp
// Streaming mode - update with each new observation
var me = new Me(period: 20);
var result = me.Update(actualValue, predictedValue);
// Batch mode - calculate for entire series
var results = Me.Calculate(actualSeries, predictedSeries, period: 20);
// Span mode - zero-allocation for high performance
Me.Batch(actualSpan, predictedSpan, outputSpan, period: 20);
```
### Parameters
| Parameter | Type | Description |
| :--- | :--- | :--- |
| **period** | int | Lookback window for averaging (must be > 0) |
### Properties
| Property | Type | Description |
| :--- | :--- | :--- |
| **Last** | TValue | Most recent ME value |
| **IsHot** | bool | True when buffer is full |
| **Name** | string | Indicator name (e.g., "Me(20)") |
| **WarmupPeriod** | int | Number of periods before valid output |
## Performance Profile
| Metric | Score | Notes |
| :--- | :--- | :--- |
| **Throughput** | ~10 ns/bar | O(1) update complexity |
| **Allocations** | 0 | Uses pre-allocated ring buffer |
| **Complexity** | O(1) | Constant time per update |
| **Accuracy** | 10/10 | Exact calculation |
| **Timeliness** | 9/10 | No lag beyond the period |
| **Smoothness** | 7/10 | Moderate smoothing |
## Interpretation
| ME Value | Interpretation |
| :--- | :--- |
| **ME > 0** | Systematic under-prediction (actual > predicted) |
| **ME = 0** | No systematic bias |
| **ME < 0** | Systematic over-prediction (actual < predicted) |
## Comparison with Other Metrics
| Metric | Shows Bias | Units | Use Case |
| :--- | :--- | :--- | :--- |
| **ME** | Yes | Same as data | Detect systematic bias |
| **MAE** | No | Same as data | Average error magnitude |
| **MSE** | No | Squared units | Penalize large errors |
| **MPE** | Yes | Percentage | Relative bias |
## Common Use Cases
1. **Bias Detection**: Identify if a model consistently over or under-predicts
2. **Model Calibration**: Use ME to adjust model outputs
3. **Forecast Evaluation**: Distinguish between random errors and systematic bias
4. **Trading Signals**: Detect directional bias in price predictions
## Warning: Cancellation Problem
ME can be misleading when errors cancel out:
```csharp
var me = new Me(4);
me.Update(110, 100); // Error: +10
me.Update(90, 100); // Error: -10
me.Update(110, 100); // Error: +10
me.Update(90, 100); // Error: -10
// ME = 0, but individual errors are large!
```
Always use ME alongside MAE or MSE to get a complete picture.
## Edge Cases
* **Identical Values**: Returns 0 when actual equals predicted
* **NaN Handling**: Uses last valid value substitution
* **Single Input**: Not supported (requires two series)
* **Period = 1**: Returns current signed error
* **Balanced Errors**: Can return 0 even with large individual errors
## Related Indicators
* [MAE](../mae/Mae.md) - Mean Absolute Error (magnitude only)
* [MSE](../mse/Mse.md) - Mean Squared Error
* [MPE](../mpe/Mpe.md) - Mean Percentage Error (relative bias)