test: setup common stability and robustness properties tracking

This commit is contained in:
Miha Kralj
2026-02-27 12:50:05 -08:00
parent 4ab3a7fb53
commit 769a923a24
287 changed files with 1314 additions and 867 deletions
@@ -304,4 +304,4 @@ public class CorrelationIndicatorTests
Assert.Equal(1.0, lastValue, precision: 6);
}
}
}
}
+27 -23
View File
@@ -119,31 +119,33 @@ public class CorrelationTests
[Fact]
public void Update_IterativeCorrections_Restore()
{
var indicator = new Correlation(5);
var corrected = new Correlation(5);
var direct = new Correlation(5);
// Feed initial data
// Feed identical initial state
for (int i = 0; i < 8; i++)
{
double x = 100.0 + i;
double y = 200.0 + (i * 2);
indicator.Update(x, y, true);
corrected.Update(x, y, true);
direct.Update(x, y, true);
}
// Add new bar
indicator.Update(108.0, 216.0, true);
// Target final value for the current bar
const double finalX = 108.0;
const double finalY = 216.0;
// Make multiple corrections
for (int j = 0; j < 5; j++)
{
double x = 108.0 + (j * 0.1);
double y = 216.0 + (j * 0.2);
_ = indicator.Update(x, y, false);
}
// Correction path: new bar, several rewrites, final rewrite back to target
corrected.Update(finalX, finalY, true);
corrected.Update(finalX + 1.0, finalY + 2.0, false);
corrected.Update(finalX - 0.5, finalY - 1.0, false);
corrected.Update(finalX + 0.25, finalY + 0.5, false);
corrected.Update(finalX, finalY, false);
// Final correction back to original values
indicator.Update(108.0, 216.0, false);
// Direct path: same initial state + one new bar with final value
direct.Update(finalX, finalY, true);
Assert.True(double.IsFinite(indicator.Last.Value));
Assert.Equal(direct.Last.Value, corrected.Last.Value, 1e-12);
}
[Fact]
@@ -159,9 +161,9 @@ public class CorrelationTests
_ = indicator.Last.Value;
// Add NaN - should use last valid value
// Add NaN - should use last valid value, result must be finite
var result = indicator.Update(double.NaN, double.NaN, true);
Assert.True(double.IsFinite(result.Value) || double.IsNaN(result.Value));
Assert.True(double.IsFinite(result.Value));
}
[Fact]
@@ -175,9 +177,9 @@ public class CorrelationTests
indicator.Update(100.0 + i, 200.0 + i, true);
}
// Add Infinity - should use last valid value
// Add Infinity - should use last valid value, result must be finite
var result = indicator.Update(double.PositiveInfinity, double.NegativeInfinity, true);
Assert.True(double.IsFinite(result.Value) || double.IsNaN(result.Value));
Assert.True(double.IsFinite(result.Value));
}
[Fact]
@@ -189,11 +191,13 @@ public class CorrelationTests
}
[Fact]
public void IsHot_AtLeastTwoValues_ReturnsTrue()
public void IsHot_AtPeriod_ReturnsTrue()
{
var indicator = new Correlation(10);
indicator.Update(100.0, 200.0, true);
indicator.Update(101.0, 201.0, true);
for (int i = 0; i < 10; i++)
{
indicator.Update(100.0 + i, 200.0 + i, true);
}
Assert.True(indicator.IsHot);
}
@@ -385,4 +389,4 @@ public class CorrelationTests
}
}
}
}
}
@@ -609,14 +609,20 @@ public sealed class CorrelationValidationTests : IDisposable
[Fact]
public void Correlation_WeakCorrelation_DetectedCorrectly()
{
// Create two series with weak correlation (lots of noise)
// Create two series with weak correlation: pure independent noise, no shared trend.
// Use two independent GBMs (different seeds) and feed their incremental log-returns directly.
// With period=20 and fully independent noise sequences, correlation should be near zero.
var indicator = new Correlation(20);
var random = new GBM(startPrice: 100.0, sigma: 1.0, seed: 43);
var gbmX = new GBM(startPrice: 100.0, sigma: 0.2, seed: 43);
var gbmY = new GBM(startPrice: 100.0, sigma: 0.2, seed: 9871);
var barsX = gbmX.Fetch(101, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1));
var barsY = gbmY.Fetch(101, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1));
for (int i = 0; i < 100; i++)
for (int i = 1; i <= 100; i++)
{
double x = 100.0 + i + Math.Log(random.Next().Close / 100.0) * 50;
double y = 100.0 + 0.1 * i + Math.Log(random.Next().Close / 100.0) * 50; // Weak relationship
// Pure independent white noise — no shared linear component
double x = Math.Log(barsX[i].Close / barsX[i - 1].Close);
double y = Math.Log(barsY[i].Close / barsY[i - 1].Close);
indicator.Update(x, y);
}
@@ -720,7 +726,7 @@ public sealed class CorrelationValidationTests : IDisposable
double[] taOut = new double[_data.Count];
var retCode = Functions.Correl<double>(closeArr, openArr, 0..^0, taOut, out var outRange, period);
Assert.Equal(Core.RetCode.Success, retCode);
Assert.Equal(TALib.Core.RetCode.Success, retCode);
(int offset, int length) = outRange.GetOffsetAndLength(taOut.Length);
Assert.True(length > 100, $"TALib Correl produced only {length} values");
@@ -760,7 +766,7 @@ public sealed class CorrelationValidationTests : IDisposable
{
double[] taOut = new double[_data.Count];
var retCode = Functions.Correl<double>(highArr, lowArr, 0..^0, taOut, out var outRange, period);
Assert.Equal(Core.RetCode.Success, retCode);
Assert.Equal(TALib.Core.RetCode.Success, retCode);
(int offset, int length) = outRange.GetOffsetAndLength(taOut.Length);
+39 -38
View File
@@ -39,12 +39,13 @@ public sealed class Correlation : AbstractBase
// Last valid values for NaN handling
private double _lastValidX, _lastValidY;
private double _p_lastValidX, _p_lastValidY;
private int _updateCount;
private const int ResyncInterval = 1000;
private const double Epsilon = 1e-10;
public override bool IsHot => _bufferX.Count >= 2;
public override bool IsHot => _bufferX.Count >= WarmupPeriod;
/// <summary>
/// Creates a new Correlation indicator.
@@ -74,6 +75,17 @@ public sealed class Correlation : AbstractBase
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public TValue Update(TValue seriesX, TValue seriesY, bool isNew = true)
{
if (isNew)
{
_p_lastValidX = _lastValidX;
_p_lastValidY = _lastValidY;
}
else
{
_lastValidX = _p_lastValidX;
_lastValidY = _p_lastValidY;
}
double x = SanitizeX(seriesX.Value);
double y = SanitizeY(seriesY.Value);
@@ -99,7 +111,7 @@ public sealed class Correlation : AbstractBase
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public TValue Update(double seriesX, double seriesY, bool isNew = true)
{
return Update(new TValue(DateTime.UtcNow, seriesX), new TValue(DateTime.UtcNow, seriesY), isNew);
return Update(new TValue(DateTime.MinValue, seriesX), new TValue(DateTime.MinValue, seriesY), isNew);
}
/// <inheritdoc/>
@@ -175,14 +187,7 @@ public sealed class Correlation : AbstractBase
{
if (_bufferX.Count == 0)
{
// No data yet, just add
_bufferX.Add(x);
_bufferY.Add(y);
_sumX = x;
_sumY = y;
_sumX2 = x * x;
_sumY2 = y * y;
_sumXY = x * y;
// Nothing to correct yet; no current bar exists
return;
}
@@ -190,12 +195,12 @@ public sealed class Correlation : AbstractBase
double oldX = _bufferX.Newest;
double oldY = _bufferY.Newest;
// Update the running sums: remove old, add new
// Update the running sums: remove old, add new (using FMA for consistency with ProcessNewBar)
_sumX = _sumX - oldX + x;
_sumY = _sumY - oldY + y;
_sumX2 = _sumX2 - (oldX * oldX) + (x * x);
_sumY2 = _sumY2 - (oldY * oldY) + (y * y);
_sumXY = _sumXY - (oldX * oldY) + (x * y);
_sumX2 = FusedMultiplyAdd(x, x, FusedMultiplyAdd(-oldX, oldX, _sumX2));
_sumY2 = FusedMultiplyAdd(y, y, FusedMultiplyAdd(-oldY, oldY, _sumY2));
_sumXY = FusedMultiplyAdd(x, y, FusedMultiplyAdd(-oldX, oldY, _sumXY));
// Update the buffer values
_bufferX.UpdateNewest(x);
@@ -278,6 +283,8 @@ public sealed class Correlation : AbstractBase
_lastValidX = 0;
_lastValidY = 0;
_p_lastValidX = 0;
_p_lastValidY = 0;
_updateCount = 0;
Last = default;
@@ -287,28 +294,7 @@ public sealed class Correlation : AbstractBase
/// Calculates correlation for two time series.
/// </summary>
public static TSeries Batch(TSeries seriesX, TSeries seriesY, int period = 20)
{
if (seriesX.Count != seriesY.Count)
{
throw new ArgumentException("Series must have the same length", nameof(seriesY));
}
var indicator = new Correlation(period);
var result = new TSeries(seriesX.Count);
var timesX = seriesX.Times;
var valuesX = seriesX.Values;
var valuesY = seriesY.Values;
for (int i = 0; i < seriesX.Count; i++)
{
var tvalX = new TValue(timesX[i], valuesX[i]);
var tvalY = new TValue(timesX[i], valuesY[i]);
result.Add(indicator.Update(tvalX, tvalY, isNew: true));
}
return result;
}
=> Calculate(seriesX, seriesY, period).Results;
/// <summary>
/// Static batch calculation for span-based processing.
@@ -345,9 +331,24 @@ public sealed class Correlation : AbstractBase
public static (TSeries Results, Correlation Indicator) Calculate(TSeries seriesX, TSeries seriesY, int period = 20)
{
if (seriesX.Count != seriesY.Count)
{
throw new ArgumentException("Series must have the same length", nameof(seriesY));
}
var indicator = new Correlation(period);
TSeries results = Batch(seriesX, seriesY, period);
return (results, indicator);
var result = new TSeries(seriesX.Count);
var timesX = seriesX.Times;
var valuesX = seriesX.Values;
var valuesY = seriesY.Values;
for (int i = 0; i < seriesX.Count; i++)
{
result.Add(indicator.Update(new TValue(timesX[i], valuesX[i]), new TValue(timesX[i], valuesY[i]), isNew: true));
}
return (result, indicator);
}
}
+4 -4
View File
@@ -3,9 +3,9 @@
| Property | Value |
| ---------------- | -------------------------------- |
| **Category** | Statistic |
| **Inputs** | Source (close) |
| **Inputs** | Two series (X, Y) |
| **Parameters** | `period` (default 20) |
| **Outputs** | Single series (Correlation) |
| **Outputs** | Single series (Pearson r) |
| **Output range** | Varies (see docs) |
| **Warmup** | `period` bars |
@@ -15,7 +15,7 @@
- Parameterized by `period` (default 20).
- Output range: Varies (see docs).
- Requires `period` bars of warmup before first valid output (IsHot = true).
- Validated against TA-Lib, Skender, and Tulip reference implementations where available.
- Validated against TradingView reference behavior and mathematical invariants.
> "Correlation is not causation, but it sure is a hint. The market doesn't care why two instruments move together—only that they do, and whether that relationship will persist long enough for you to profit from it."
@@ -219,7 +219,7 @@ double[] pricesA = new double[1000];
double[] pricesB = new double[1000];
double[] output = new double[1000];
// ... populate inputs ...
Correlation.Calculate(pricesA.AsSpan(), pricesB.AsSpan(), output.AsSpan(), period: 20);
Correlation.Batch(pricesA.AsSpan(), pricesB.AsSpan(), output.AsSpan(), period: 20);
```
### Bar Correction Support