mirror of
https://github.com/mihakralj/QuanTAlib.git
synced 2026-08-19 02:58:05 +00:00
test: setup common stability and robustness properties tracking
This commit is contained in:
@@ -304,4 +304,4 @@ public class CorrelationIndicatorTests
|
||||
Assert.Equal(1.0, lastValue, precision: 6);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,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);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user