mirror of
https://github.com/mihakralj/QuanTAlib.git
synced 2026-08-18 10:38:05 +00:00
Add Yang-Zhang Volatility (YZV) Indicator Implementation
- Introduced YZV class for calculating Yang-Zhang Volatility, a comprehensive volatility measure that incorporates overnight, open-to-close, and high-low components. - Implemented calculation methods, including batch processing for TBarSeries and spans. - Added documentation for YZV, detailing its mathematical foundation, performance profile, and trading applications. - Updated volume index documentation to reflect changes in file paths. - Refactored VWMA calculation method to use a more generic source parameter instead of price.
This commit is contained in:
@@ -0,0 +1,307 @@
|
||||
using TradingPlatform.BusinessLayer;
|
||||
|
||||
namespace QuanTAlib.Tests;
|
||||
|
||||
public class CorrelationIndicatorTests
|
||||
{
|
||||
[Fact]
|
||||
public void CorrelationIndicator_Constructor_SetsDefaults()
|
||||
{
|
||||
var indicator = new CorrelationIndicator();
|
||||
|
||||
Assert.Equal(20, indicator.Period);
|
||||
Assert.Equal(SourceType.Close, indicator.Source);
|
||||
Assert.Equal(SourceType.Open, indicator.Source2);
|
||||
Assert.True(indicator.ShowColdValues);
|
||||
Assert.Equal("CORR - Pearson Correlation Coefficient", indicator.Name);
|
||||
Assert.True(indicator.SeparateWindow);
|
||||
Assert.True(indicator.OnBackGround);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CorrelationIndicator_MinHistoryDepths_EqualsTwo()
|
||||
{
|
||||
var indicator = new CorrelationIndicator();
|
||||
|
||||
Assert.Equal(2, CorrelationIndicator.MinHistoryDepths);
|
||||
Assert.Equal(2, ((IWatchlistIndicator)indicator).MinHistoryDepths);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CorrelationIndicator_ShortName_IncludesPeriodAndSources()
|
||||
{
|
||||
var indicator = new CorrelationIndicator { Period = 20 };
|
||||
|
||||
Assert.Contains("CORR", indicator.ShortName, StringComparison.Ordinal);
|
||||
Assert.Contains("20", indicator.ShortName, StringComparison.Ordinal);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CorrelationIndicator_Initialize_CreatesInternalCorrelation()
|
||||
{
|
||||
var indicator = new CorrelationIndicator { Period = 10 };
|
||||
|
||||
// Initialize should not throw
|
||||
indicator.Initialize();
|
||||
|
||||
// After init, line series should exist
|
||||
Assert.Single(indicator.LinesSeries);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CorrelationIndicator_ProcessUpdate_HistoricalBar_ComputesValue()
|
||||
{
|
||||
var indicator = new CorrelationIndicator { Period = 5 };
|
||||
indicator.Initialize();
|
||||
|
||||
// Add historical data
|
||||
var now = DateTime.UtcNow;
|
||||
indicator.HistoricalData.AddBar(now, 100, 105, 95, 102);
|
||||
|
||||
// Process update
|
||||
var args = new UpdateArgs(UpdateReason.HistoricalBar);
|
||||
indicator.ProcessUpdate(args);
|
||||
|
||||
// Line series should have a value (may be NaN during warmup)
|
||||
Assert.Equal(1, indicator.LinesSeries[0].Count);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CorrelationIndicator_ProcessUpdate_NewBar_ComputesValue()
|
||||
{
|
||||
var indicator = new CorrelationIndicator { Period = 5 };
|
||||
indicator.Initialize();
|
||||
|
||||
var now = DateTime.UtcNow;
|
||||
indicator.HistoricalData.AddBar(now, 100, 105, 95, 102);
|
||||
indicator.HistoricalData.AddBar(now.AddMinutes(1), 102, 108, 100, 106);
|
||||
|
||||
indicator.ProcessUpdate(new UpdateArgs(UpdateReason.HistoricalBar));
|
||||
indicator.ProcessUpdate(new UpdateArgs(UpdateReason.NewBar));
|
||||
|
||||
Assert.Equal(2, indicator.LinesSeries[0].Count);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CorrelationIndicator_ProcessUpdate_NewTick_ProcessesWithoutError()
|
||||
{
|
||||
var indicator = new CorrelationIndicator { Period = 5 };
|
||||
indicator.Initialize();
|
||||
|
||||
var now = DateTime.UtcNow;
|
||||
indicator.HistoricalData.AddBar(now, 100, 105, 95, 102);
|
||||
|
||||
indicator.ProcessUpdate(new UpdateArgs(UpdateReason.HistoricalBar));
|
||||
double firstValue = indicator.LinesSeries[0].GetValue(0);
|
||||
|
||||
// NewTick should not throw
|
||||
indicator.ProcessUpdate(new UpdateArgs(UpdateReason.NewTick));
|
||||
double secondValue = indicator.LinesSeries[0].GetValue(0);
|
||||
|
||||
// Values should be produced (may be NaN during warmup, but should not throw)
|
||||
Assert.True(double.IsNaN(firstValue) || double.IsFinite(firstValue));
|
||||
Assert.True(double.IsNaN(secondValue) || double.IsFinite(secondValue));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CorrelationIndicator_MultipleUpdates_ProducesSequence()
|
||||
{
|
||||
var indicator = new CorrelationIndicator { Period = 3 };
|
||||
indicator.Initialize();
|
||||
|
||||
var now = DateTime.UtcNow;
|
||||
// Add bars with different O/C patterns to create varying correlation
|
||||
double[] opens = { 100, 101, 102, 103, 104, 105 };
|
||||
double[] closes = { 100, 101, 102, 103, 104, 105 };
|
||||
|
||||
for (int i = 0; i < opens.Length; i++)
|
||||
{
|
||||
indicator.HistoricalData.AddBar(now.AddMinutes(i), opens[i], opens[i] + 5, opens[i] - 5, closes[i]);
|
||||
indicator.ProcessUpdate(new UpdateArgs(i == 0 ? UpdateReason.HistoricalBar : UpdateReason.NewBar));
|
||||
}
|
||||
|
||||
// All values should exist
|
||||
Assert.Equal(opens.Length, indicator.LinesSeries[0].Count);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CorrelationIndicator_DifferentSourceTypes_Work()
|
||||
{
|
||||
var sources = new[] { SourceType.Open, SourceType.High, SourceType.Low, SourceType.Close, SourceType.HL2, SourceType.HLC3 };
|
||||
|
||||
foreach (var source in sources)
|
||||
{
|
||||
var indicator = new CorrelationIndicator { Period = 5, Source = source, Source2 = SourceType.Close };
|
||||
indicator.Initialize();
|
||||
|
||||
var now = DateTime.UtcNow;
|
||||
indicator.HistoricalData.AddBar(now, 100, 110, 90, 105);
|
||||
indicator.ProcessUpdate(new UpdateArgs(UpdateReason.HistoricalBar));
|
||||
|
||||
// Should have computed a value (may be NaN during warmup, but should not throw)
|
||||
Assert.Equal(1, indicator.LinesSeries[0].Count);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CorrelationIndicator_CorrelationBounds()
|
||||
{
|
||||
// This test verifies the indicator produces values in valid range [-1, +1]
|
||||
var indicator = new CorrelationIndicator { Period = 5, Source = SourceType.Close, Source2 = SourceType.Open };
|
||||
indicator.Initialize();
|
||||
|
||||
var now = DateTime.UtcNow;
|
||||
|
||||
// Add bars with varying patterns
|
||||
for (int i = 0; i < 20; i++)
|
||||
{
|
||||
double open = 100 + i;
|
||||
double close = 100 + i + (i % 2 == 0 ? 2 : -1); // Varying relationship
|
||||
indicator.HistoricalData.AddBar(now.AddMinutes(i), open, open + 5, open - 5, close);
|
||||
indicator.ProcessUpdate(new UpdateArgs(i == 0 ? UpdateReason.HistoricalBar : UpdateReason.NewBar));
|
||||
}
|
||||
|
||||
// After warmup, should have values in valid range
|
||||
Assert.Equal(20, indicator.LinesSeries[0].Count);
|
||||
|
||||
// Check that values are bounded
|
||||
for (int i = 0; i < 20; i++)
|
||||
{
|
||||
double value = indicator.LinesSeries[0].GetValue(i);
|
||||
if (double.IsFinite(value))
|
||||
{
|
||||
Assert.InRange(value, -1.0, 1.0);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CorrelationIndicator_DifferentSource2Types_Work()
|
||||
{
|
||||
var source2Types = new[] { SourceType.Open, SourceType.High, SourceType.Low, SourceType.HL2 };
|
||||
|
||||
foreach (var source2 in source2Types)
|
||||
{
|
||||
var indicator = new CorrelationIndicator { Period = 5, Source = SourceType.Close, Source2 = source2 };
|
||||
indicator.Initialize();
|
||||
|
||||
var now = DateTime.UtcNow;
|
||||
indicator.HistoricalData.AddBar(now, 100, 110, 90, 105);
|
||||
indicator.ProcessUpdate(new UpdateArgs(UpdateReason.HistoricalBar));
|
||||
|
||||
Assert.Equal(1, indicator.LinesSeries[0].Count);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CorrelationIndicator_Period_CanBeChanged()
|
||||
{
|
||||
var indicator = new CorrelationIndicator { Period = 50 };
|
||||
|
||||
Assert.Equal(50, indicator.Period);
|
||||
|
||||
indicator.Period = 100;
|
||||
Assert.Equal(100, indicator.Period);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CorrelationIndicator_Source2_CanBeChanged()
|
||||
{
|
||||
var indicator = new CorrelationIndicator { Source2 = SourceType.High };
|
||||
|
||||
Assert.Equal(SourceType.High, indicator.Source2);
|
||||
|
||||
indicator.Source2 = SourceType.Low;
|
||||
Assert.Equal(SourceType.Low, indicator.Source2);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CorrelationIndicator_ReInitialize_ResetsState()
|
||||
{
|
||||
var indicator = new CorrelationIndicator { Period = 5 };
|
||||
indicator.Initialize();
|
||||
|
||||
var now = DateTime.UtcNow;
|
||||
for (int i = 0; i < 10; i++)
|
||||
{
|
||||
indicator.HistoricalData.AddBar(now.AddMinutes(i), 100 + i, 105 + i, 95 + i, 102 + i);
|
||||
indicator.ProcessUpdate(new UpdateArgs(i == 0 ? UpdateReason.HistoricalBar : UpdateReason.NewBar));
|
||||
}
|
||||
|
||||
Assert.Equal(10, indicator.LinesSeries[0].Count);
|
||||
|
||||
// Re-initialize should work without errors
|
||||
var indicator2 = new CorrelationIndicator { Period = 5 };
|
||||
indicator2.Initialize();
|
||||
indicator2.HistoricalData.AddBar(now.AddMinutes(100), 200, 210, 190, 205);
|
||||
indicator2.ProcessUpdate(new UpdateArgs(UpdateReason.HistoricalBar));
|
||||
|
||||
Assert.Equal(1, indicator2.LinesSeries[0].Count);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CorrelationIndicator_HighLow_ProducesPositiveCorrelation()
|
||||
{
|
||||
// Test with High vs Low - they should be positively correlated
|
||||
var indicator = new CorrelationIndicator { Period = 10, Source = SourceType.High, Source2 = SourceType.Low };
|
||||
indicator.Initialize();
|
||||
|
||||
var now = DateTime.UtcNow;
|
||||
|
||||
// Add bars with typical High > Low relationship
|
||||
for (int i = 0; i < 15; i++)
|
||||
{
|
||||
double mid = 100 + (i * 0.5);
|
||||
double spread = 5;
|
||||
indicator.HistoricalData.AddBar(now.AddMinutes(i), mid, mid + spread, mid - spread, mid);
|
||||
indicator.ProcessUpdate(new UpdateArgs(i == 0 ? UpdateReason.HistoricalBar : UpdateReason.NewBar));
|
||||
}
|
||||
|
||||
Assert.Equal(15, indicator.LinesSeries[0].Count);
|
||||
|
||||
// After warmup period, High and Low should show positive correlation
|
||||
// (they both trend together as price moves)
|
||||
double lastValue = indicator.LinesSeries[0].GetValue(0);
|
||||
if (double.IsFinite(lastValue))
|
||||
{
|
||||
Assert.True(lastValue > 0, $"Expected positive correlation for High vs Low, got {lastValue}");
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CorrelationIndicator_Description_IsSet()
|
||||
{
|
||||
var indicator = new CorrelationIndicator();
|
||||
|
||||
Assert.Contains("linear", indicator.Description, StringComparison.OrdinalIgnoreCase);
|
||||
Assert.Contains("-1", indicator.Description, StringComparison.Ordinal);
|
||||
Assert.Contains("+1", indicator.Description, StringComparison.Ordinal);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CorrelationIndicator_PerfectCorrelation_ReturnsOne()
|
||||
{
|
||||
// When Close == Open for all bars, correlation should be 1.0 (or NaN if zero variance)
|
||||
var indicator = new CorrelationIndicator { Period = 5, Source = SourceType.Close, Source2 = SourceType.Open };
|
||||
indicator.Initialize();
|
||||
|
||||
var now = DateTime.UtcNow;
|
||||
|
||||
// Add bars where Close always equals Open (perfect linear relationship)
|
||||
for (int i = 0; i < 10; i++)
|
||||
{
|
||||
double price = 100 + i * 2; // Trending up
|
||||
indicator.HistoricalData.AddBar(now.AddMinutes(i), price, price + 5, price - 5, price);
|
||||
indicator.ProcessUpdate(new UpdateArgs(i == 0 ? UpdateReason.HistoricalBar : UpdateReason.NewBar));
|
||||
}
|
||||
|
||||
Assert.Equal(10, indicator.LinesSeries[0].Count);
|
||||
|
||||
// When Open == Close exactly, we get perfect correlation = 1.0
|
||||
double lastValue = indicator.LinesSeries[0].GetValue(0);
|
||||
if (double.IsFinite(lastValue))
|
||||
{
|
||||
Assert.Equal(1.0, lastValue, precision: 6);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,80 @@
|
||||
using System.Drawing;
|
||||
using System.Runtime.CompilerServices;
|
||||
using TradingPlatform.BusinessLayer;
|
||||
|
||||
namespace QuanTAlib;
|
||||
|
||||
/// <summary>
|
||||
/// Quantower adapter for Correlation indicator.
|
||||
/// Measures the Pearson correlation coefficient between two price series.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// This adapter compares two different price sources from the same symbol (e.g., Close vs Open,
|
||||
/// Close vs Volume, High vs Low). For cross-symbol correlation analysis, use the core
|
||||
/// Correlation class directly with data from multiple symbols.
|
||||
///
|
||||
/// The output is the Pearson correlation coefficient, ranging from -1 to +1.
|
||||
/// Values near +1 indicate strong positive correlation, near -1 indicate strong negative correlation.
|
||||
/// </remarks>
|
||||
[SkipLocalsInit]
|
||||
public sealed class CorrelationIndicator : Indicator, IWatchlistIndicator
|
||||
{
|
||||
[InputParameter("Period", sortIndex: 0, minimum: 2, maximum: 10000)]
|
||||
public int Period { get; set; } = 20;
|
||||
|
||||
[IndicatorExtensions.DataSourceInput]
|
||||
public SourceType Source { get; set; } = SourceType.Close;
|
||||
|
||||
[InputParameter("Source 2 Type", sortIndex: 2)]
|
||||
public SourceType Source2 { get; set; } = SourceType.Open;
|
||||
|
||||
[InputParameter("Show cold values", sortIndex: 21)]
|
||||
public bool ShowColdValues { get; set; } = true;
|
||||
|
||||
private Correlation _correlation = null!;
|
||||
private readonly LineSeries _series;
|
||||
private string _sourceName = null!;
|
||||
private Func<IHistoryItem, double> _priceSelector = null!;
|
||||
private Func<IHistoryItem, double> _priceSelector2 = null!;
|
||||
|
||||
public static int MinHistoryDepths => 2;
|
||||
int IWatchlistIndicator.MinHistoryDepths => MinHistoryDepths;
|
||||
|
||||
public override string ShortName => $"CORR({Period}):{_sourceName}/{Source2}";
|
||||
|
||||
public CorrelationIndicator()
|
||||
{
|
||||
OnBackGround = true;
|
||||
SeparateWindow = true;
|
||||
Name = "CORR - Pearson Correlation Coefficient";
|
||||
Description = "Measures linear relationship between two price sources. Range: -1 (inverse) to +1 (perfect positive).";
|
||||
_series = new LineSeries(name: "Correlation", color: IndicatorExtensions.Statistics, width: 2, style: LineStyle.Solid);
|
||||
AddLineSeries(_series);
|
||||
}
|
||||
|
||||
protected override void OnInit()
|
||||
{
|
||||
_priceSelector = Source.GetPriceSelector();
|
||||
_priceSelector2 = Source2.GetPriceSelector();
|
||||
_sourceName = Source.ToString();
|
||||
_correlation = new Correlation(Period);
|
||||
base.OnInit();
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
protected override void OnUpdate(UpdateArgs args)
|
||||
{
|
||||
bool isNew = args.IsNewBar();
|
||||
|
||||
// Get both price sources from the same bar
|
||||
var item = HistoricalData[Count - 1, SeekOriginHistory.Begin];
|
||||
double valueA = _priceSelector(item);
|
||||
double valueB = _priceSelector2(item);
|
||||
|
||||
var tvalA = new TValue(item.TimeLeft.Ticks, valueA);
|
||||
var tvalB = new TValue(item.TimeLeft.Ticks, valueB);
|
||||
|
||||
double value = _correlation.Update(tvalA, tvalB, isNew).Value;
|
||||
_series.SetValue(value, _correlation.IsHot, ShowColdValues);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,388 @@
|
||||
namespace QuanTAlib.Tests;
|
||||
|
||||
public class CorrelationTests
|
||||
{
|
||||
[Fact]
|
||||
public void Constructor_ValidPeriod_CreatesIndicator()
|
||||
{
|
||||
var indicator = new Correlation(20);
|
||||
Assert.Equal("Correlation(20)", indicator.Name);
|
||||
Assert.Equal(20, indicator.WarmupPeriod);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Constructor_MinimumValidPeriod_CreatesIndicator()
|
||||
{
|
||||
var indicator = new Correlation(2);
|
||||
Assert.Equal("Correlation(2)", indicator.Name);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Constructor_InvalidPeriod_ThrowsArgumentException()
|
||||
{
|
||||
Assert.Throws<ArgumentException>(() => new Correlation(1));
|
||||
Assert.Throws<ArgumentException>(() => new Correlation(0));
|
||||
Assert.Throws<ArgumentException>(() => new Correlation(-5));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Update_SingleValue_ReturnsNaN()
|
||||
{
|
||||
var indicator = new Correlation(5);
|
||||
var result = indicator.Update(100.0, 200.0, true);
|
||||
Assert.True(double.IsNaN(result.Value));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Update_TwoValues_ReturnsValidCorrelation()
|
||||
{
|
||||
var indicator = new Correlation(5);
|
||||
indicator.Update(100.0, 200.0, true);
|
||||
var result = indicator.Update(102.0, 204.0, true);
|
||||
Assert.True(double.IsFinite(result.Value));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Update_PerfectPositiveCorrelation_ReturnsOne()
|
||||
{
|
||||
var indicator = new Correlation(5);
|
||||
|
||||
// Same values scaled by constant should give correlation = 1
|
||||
for (int i = 0; i < 10; i++)
|
||||
{
|
||||
double x = 100.0 + i;
|
||||
double y = 200.0 + (2 * i); // y = 200 + 2x (perfectly correlated)
|
||||
indicator.Update(x, y, true);
|
||||
}
|
||||
|
||||
Assert.True(indicator.IsHot);
|
||||
Assert.InRange(indicator.Last.Value, 0.999, 1.001);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Update_PerfectNegativeCorrelation_ReturnsMinusOne()
|
||||
{
|
||||
var indicator = new Correlation(5);
|
||||
|
||||
// Opposite movements should give correlation = -1
|
||||
for (int i = 0; i < 10; i++)
|
||||
{
|
||||
double x = 100.0 + i;
|
||||
double y = 200.0 - (2 * i); // y = 200 - 2x (perfectly negatively correlated)
|
||||
indicator.Update(x, y, true);
|
||||
}
|
||||
|
||||
Assert.True(indicator.IsHot);
|
||||
Assert.InRange(indicator.Last.Value, -1.001, -0.999);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Update_ConstantValues_ReturnsNaN()
|
||||
{
|
||||
var indicator = new Correlation(5);
|
||||
|
||||
// Constant values have zero variance, so correlation is undefined
|
||||
for (int i = 0; i < 10; i++)
|
||||
{
|
||||
indicator.Update(100.0, 200.0, true);
|
||||
}
|
||||
|
||||
Assert.True(double.IsNaN(indicator.Last.Value));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Update_BarCorrection_RestoresState()
|
||||
{
|
||||
var indicator1 = new Correlation(5);
|
||||
var indicator2 = new Correlation(5);
|
||||
|
||||
// Feed same initial data
|
||||
for (int i = 0; i < 10; i++)
|
||||
{
|
||||
double x = 100.0 + i;
|
||||
double y = 200.0 + (i * 0.5);
|
||||
indicator1.Update(x, y, true);
|
||||
indicator2.Update(x, y, true);
|
||||
}
|
||||
|
||||
// indicator1: Add another bar
|
||||
indicator1.Update(110.0, 205.0, true);
|
||||
|
||||
// indicator2: Add bar, then correct it
|
||||
indicator2.Update(999.0, 999.0, true); // Wrong values
|
||||
indicator2.Update(110.0, 205.0, false); // Correct them
|
||||
|
||||
// Values should match
|
||||
Assert.Equal(indicator1.Last.Value, indicator2.Last.Value, 1e-9);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Update_IterativeCorrections_Restore()
|
||||
{
|
||||
var indicator = new Correlation(5);
|
||||
|
||||
// Feed initial data
|
||||
for (int i = 0; i < 8; i++)
|
||||
{
|
||||
double x = 100.0 + i;
|
||||
double y = 200.0 + (i * 2);
|
||||
indicator.Update(x, y, true);
|
||||
}
|
||||
|
||||
// Add new bar
|
||||
indicator.Update(108.0, 216.0, true);
|
||||
|
||||
// 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);
|
||||
}
|
||||
|
||||
// Final correction back to original values
|
||||
indicator.Update(108.0, 216.0, false);
|
||||
|
||||
Assert.True(double.IsFinite(indicator.Last.Value));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Update_NaNInput_UsesLastValidValue()
|
||||
{
|
||||
var indicator = new Correlation(5);
|
||||
|
||||
// Add valid data
|
||||
for (int i = 0; i < 5; i++)
|
||||
{
|
||||
indicator.Update(100.0 + i, 200.0 + i, true);
|
||||
}
|
||||
|
||||
_ = indicator.Last.Value;
|
||||
|
||||
// Add NaN - should use last valid value
|
||||
var result = indicator.Update(double.NaN, double.NaN, true);
|
||||
Assert.True(double.IsFinite(result.Value) || double.IsNaN(result.Value));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Update_InfinityInput_UsesLastValidValue()
|
||||
{
|
||||
var indicator = new Correlation(5);
|
||||
|
||||
// Add valid data
|
||||
for (int i = 0; i < 5; i++)
|
||||
{
|
||||
indicator.Update(100.0 + i, 200.0 + i, true);
|
||||
}
|
||||
|
||||
// Add Infinity - should use last valid value
|
||||
var result = indicator.Update(double.PositiveInfinity, double.NegativeInfinity, true);
|
||||
Assert.True(double.IsFinite(result.Value) || double.IsNaN(result.Value));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void IsHot_BelowPeriod_ReturnsFalse()
|
||||
{
|
||||
var indicator = new Correlation(10);
|
||||
indicator.Update(100.0, 200.0, true);
|
||||
Assert.False(indicator.IsHot);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void IsHot_AtLeastTwoValues_ReturnsTrue()
|
||||
{
|
||||
var indicator = new Correlation(10);
|
||||
indicator.Update(100.0, 200.0, true);
|
||||
indicator.Update(101.0, 201.0, true);
|
||||
Assert.True(indicator.IsHot);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Reset_ClearsState()
|
||||
{
|
||||
var indicator = new Correlation(5);
|
||||
|
||||
// Add data
|
||||
for (int i = 0; i < 10; i++)
|
||||
{
|
||||
indicator.Update(100.0 + i, 200.0 + (i * 2), true);
|
||||
}
|
||||
|
||||
Assert.True(indicator.IsHot);
|
||||
|
||||
// Reset
|
||||
indicator.Reset();
|
||||
|
||||
Assert.False(indicator.IsHot);
|
||||
Assert.Equal(default, indicator.Last);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Update_TValue_ThrowsNotSupportedException()
|
||||
{
|
||||
var indicator = new Correlation(5);
|
||||
Assert.Throws<NotSupportedException>(() => indicator.Update(new TValue(DateTime.UtcNow, 100.0)));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Update_TSeries_ThrowsNotSupportedException()
|
||||
{
|
||||
var indicator = new Correlation(5);
|
||||
var series = new TSeries(10);
|
||||
Assert.Throws<NotSupportedException>(() => indicator.Update(series));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Prime_ThrowsNotSupportedException()
|
||||
{
|
||||
var indicator = new Correlation(5);
|
||||
Assert.Throws<NotSupportedException>(() => indicator.Prime(new double[] { 1, 2, 3 }));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Calculate_TSeries_ReturnsCorrectLength()
|
||||
{
|
||||
var seriesX = new TSeries(20);
|
||||
var seriesY = new TSeries(20);
|
||||
|
||||
for (int i = 0; i < 20; i++)
|
||||
{
|
||||
seriesX.Add(new TValue(DateTime.UtcNow.AddMinutes(i), 100.0 + i));
|
||||
seriesY.Add(new TValue(DateTime.UtcNow.AddMinutes(i), 200.0 + (i * 2)));
|
||||
}
|
||||
|
||||
var result = Correlation.Calculate(seriesX, seriesY, 5);
|
||||
|
||||
Assert.Equal(20, result.Count);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Calculate_TSeries_DifferentLengths_ThrowsArgumentException()
|
||||
{
|
||||
var seriesX = new TSeries(10);
|
||||
var seriesY = new TSeries(15);
|
||||
|
||||
for (int i = 0; i < 10; i++)
|
||||
{
|
||||
seriesX.Add(new TValue(DateTime.UtcNow.AddMinutes(i), 100.0 + i));
|
||||
}
|
||||
for (int i = 0; i < 15; i++)
|
||||
{
|
||||
seriesY.Add(new TValue(DateTime.UtcNow.AddMinutes(i), 200.0 + i));
|
||||
}
|
||||
|
||||
Assert.Throws<ArgumentException>(() => Correlation.Calculate(seriesX, seriesY, 5));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Calculate_Span_ReturnsCorrectValues()
|
||||
{
|
||||
double[] seriesX = new double[20];
|
||||
double[] seriesY = new double[20];
|
||||
double[] output = new double[20];
|
||||
|
||||
for (int i = 0; i < 20; i++)
|
||||
{
|
||||
seriesX[i] = 100.0 + i;
|
||||
seriesY[i] = 200.0 + (i * 2);
|
||||
}
|
||||
|
||||
Correlation.Calculate(seriesX, seriesY, output, 5);
|
||||
|
||||
// First value should be NaN (not enough data)
|
||||
Assert.True(double.IsNaN(output[0]));
|
||||
|
||||
// After warmup, should have valid correlation
|
||||
Assert.True(double.IsFinite(output[19]));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Calculate_Span_DifferentLengths_ThrowsArgumentException()
|
||||
{
|
||||
double[] seriesX = new double[10];
|
||||
double[] seriesY = new double[15];
|
||||
double[] output = new double[10];
|
||||
|
||||
Assert.Throws<ArgumentException>(() => Correlation.Calculate(seriesX, seriesY, output, 5));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Calculate_Span_OutputWrongLength_ThrowsArgumentException()
|
||||
{
|
||||
double[] seriesX = new double[20];
|
||||
double[] seriesY = new double[20];
|
||||
double[] output = new double[10];
|
||||
|
||||
Assert.Throws<ArgumentException>(() => Correlation.Calculate(seriesX, seriesY, output, 5));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Calculate_Span_InvalidPeriod_ThrowsArgumentException()
|
||||
{
|
||||
double[] seriesX = new double[20];
|
||||
double[] seriesY = new double[20];
|
||||
double[] output = new double[20];
|
||||
|
||||
Assert.Throws<ArgumentException>(() => Correlation.Calculate(seriesX, seriesY, output, 1));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CorrelationRange_AlwaysBetweenMinusOneAndOne()
|
||||
{
|
||||
var indicator = new Correlation(10);
|
||||
var gbmX = new GBM(startPrice: 100, mu: 0.02, sigma: 0.3, seed: 12345);
|
||||
var gbmY = new GBM(startPrice: 200, mu: 0.01, sigma: 0.5, seed: 54321);
|
||||
|
||||
for (int i = 0; i < 1000; i++)
|
||||
{
|
||||
double x = gbmX.Next().Close;
|
||||
double y = gbmY.Next().Close;
|
||||
var result = indicator.Update(x, y, true);
|
||||
|
||||
if (double.IsFinite(result.Value))
|
||||
{
|
||||
Assert.InRange(result.Value, -1.0, 1.0);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void StreamingVsBatch_Consistency()
|
||||
{
|
||||
int period = 10;
|
||||
int length = 100;
|
||||
|
||||
// Generate data
|
||||
var gbmX = new GBM(startPrice: 100, mu: 0.02, sigma: 0.3, seed: 42);
|
||||
var gbmY = new GBM(startPrice: 200, mu: 0.01, sigma: 0.4, seed: 123);
|
||||
double[] seriesX = new double[length];
|
||||
double[] seriesY = new double[length];
|
||||
|
||||
for (int i = 0; i < length; i++)
|
||||
{
|
||||
seriesX[i] = gbmX.Next().Close;
|
||||
seriesY[i] = gbmY.Next().Close;
|
||||
}
|
||||
|
||||
// Streaming calculation
|
||||
var indicator = new Correlation(period);
|
||||
double[] streamingResults = new double[length];
|
||||
for (int i = 0; i < length; i++)
|
||||
{
|
||||
streamingResults[i] = indicator.Update(seriesX[i], seriesY[i], true).Value;
|
||||
}
|
||||
|
||||
// Batch calculation
|
||||
double[] batchResults = new double[length];
|
||||
Correlation.Calculate(seriesX, seriesY, batchResults, period);
|
||||
|
||||
// Compare last 50 values (after warmup)
|
||||
for (int i = length - 50; i < length; i++)
|
||||
{
|
||||
if (double.IsFinite(streamingResults[i]) && double.IsFinite(batchResults[i]))
|
||||
{
|
||||
Assert.Equal(streamingResults[i], batchResults[i], 1e-9);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,510 @@
|
||||
namespace QuanTAlib.Tests;
|
||||
|
||||
/// <summary>
|
||||
/// Validation tests for Correlation (Pearson Correlation Coefficient) indicator.
|
||||
/// Validates against mathematical properties and expected statistical behavior.
|
||||
/// </summary>
|
||||
public class CorrelationValidationTests
|
||||
{
|
||||
private const double Tolerance = 1e-10;
|
||||
|
||||
#region Mathematical Property Validation
|
||||
|
||||
[Fact]
|
||||
public void Correlation_PerfectLinearPositive_ReturnsOne()
|
||||
{
|
||||
// y = a + b*x with b > 0 should give r = 1
|
||||
var indicator = new Correlation(20);
|
||||
|
||||
for (int i = 0; i < 50; i++)
|
||||
{
|
||||
double x = 10.0 + i * 2.5;
|
||||
double y = 5.0 + 3.0 * x; // y = 5 + 3x
|
||||
indicator.Update(x, y);
|
||||
}
|
||||
|
||||
Assert.Equal(1.0, indicator.Last.Value, 1e-9);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Correlation_PerfectLinearNegative_ReturnsMinusOne()
|
||||
{
|
||||
// y = a + b*x with b < 0 should give r = -1
|
||||
var indicator = new Correlation(20);
|
||||
|
||||
for (int i = 0; i < 50; i++)
|
||||
{
|
||||
double x = 10.0 + i * 2.5;
|
||||
double y = 100.0 - 2.0 * x; // y = 100 - 2x
|
||||
indicator.Update(x, y);
|
||||
}
|
||||
|
||||
Assert.Equal(-1.0, indicator.Last.Value, 1e-9);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Correlation_SymmetryProperty_XY_Equals_YX()
|
||||
{
|
||||
// Correlation(X, Y) should equal Correlation(Y, X)
|
||||
var indicatorXY = new Correlation(10);
|
||||
var indicatorYX = new Correlation(10);
|
||||
var gbmX = new GBM(startPrice: 100, mu: 0.02, sigma: 0.2, seed: 12345);
|
||||
var gbmY = new GBM(startPrice: 50, mu: 0.01, sigma: 0.15, seed: 54321);
|
||||
|
||||
for (int i = 0; i < 100; i++)
|
||||
{
|
||||
double x = gbmX.Next().Close;
|
||||
double y = gbmY.Next().Close;
|
||||
indicatorXY.Update(x, y);
|
||||
indicatorYX.Update(y, x);
|
||||
}
|
||||
|
||||
Assert.Equal(indicatorXY.Last.Value, indicatorYX.Last.Value, 1e-10);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Correlation_ScaleInvariance_AffineTransform()
|
||||
{
|
||||
// Correlation is invariant under positive linear transformations
|
||||
// corr(X, Y) = corr(aX + b, cY + d) when a, c > 0
|
||||
var indicator1 = new Correlation(10);
|
||||
var indicator2 = new Correlation(10);
|
||||
var gbmX = new GBM(startPrice: 100, mu: 0.02, sigma: 0.2, seed: 12345);
|
||||
var gbmY = new GBM(startPrice: 50, mu: 0.01, sigma: 0.15, seed: 54321);
|
||||
|
||||
double a = 2.5, b = 100.0, c = 0.5, d = -50.0;
|
||||
|
||||
for (int i = 0; i < 100; i++)
|
||||
{
|
||||
double x = gbmX.Next().Close;
|
||||
double y = gbmY.Next().Close;
|
||||
indicator1.Update(x, y);
|
||||
indicator2.Update(a * x + b, c * y + d);
|
||||
}
|
||||
|
||||
// Relax tolerance due to floating point precision with large transformations
|
||||
Assert.Equal(indicator1.Last.Value, indicator2.Last.Value, 1e-6);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Correlation_BoundedProperty_AlwaysBetweenMinusOneAndOne()
|
||||
{
|
||||
// Correlation coefficient is always in [-1, 1]
|
||||
var indicator = new Correlation(10);
|
||||
var gbmX = new GBM(startPrice: 100, mu: 0.1, sigma: 0.5, seed: 12345);
|
||||
var gbmY = new GBM(startPrice: 50, mu: -0.05, sigma: 0.3, seed: 54321);
|
||||
|
||||
for (int i = 0; i < 1000; i++)
|
||||
{
|
||||
double x = gbmX.Next().Close;
|
||||
double y = gbmY.Next().Close;
|
||||
var result = indicator.Update(x, y);
|
||||
|
||||
if (double.IsFinite(result.Value))
|
||||
{
|
||||
Assert.InRange(result.Value, -1.0, 1.0);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Correlation_ZeroVariance_ReturnsNaN()
|
||||
{
|
||||
// When one or both series have zero variance, correlation is undefined
|
||||
var indicator = new Correlation(10);
|
||||
|
||||
for (int i = 0; i < 20; i++)
|
||||
{
|
||||
indicator.Update(100.0, 50.0 + i); // X constant, Y varying
|
||||
}
|
||||
|
||||
// Correlation with constant series is undefined (0/0)
|
||||
Assert.True(double.IsNaN(indicator.Last.Value));
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Known Value Tests
|
||||
|
||||
[Fact]
|
||||
public void Correlation_KnownValues_SimpleSet()
|
||||
{
|
||||
// Test with known values that can be hand-calculated
|
||||
// X = [1, 2, 3, 4, 5], Y = [2, 4, 5, 4, 5]
|
||||
// Mean(X) = 3, Mean(Y) = 4
|
||||
// Cov(X,Y) = ((1-3)(2-4) + (2-3)(4-4) + (3-3)(5-4) + (4-3)(4-4) + (5-3)(5-4)) / 5
|
||||
// = (4 + 0 + 0 + 0 + 2) / 5 = 1.2
|
||||
// Var(X) = ((1-3)² + (2-3)² + (3-3)² + (4-3)² + (5-3)²) / 5 = (4+1+0+1+4)/5 = 2
|
||||
// Var(Y) = ((2-4)² + (4-4)² + (5-4)² + (4-4)² + (5-4)²) / 5 = (4+0+1+0+1)/5 = 1.2
|
||||
// r = Cov(X,Y) / sqrt(Var(X) * Var(Y)) = 1.2 / sqrt(2 * 1.2) = 1.2 / sqrt(2.4)
|
||||
// = 1.2 / 1.5492 ≈ 0.7746
|
||||
|
||||
var indicator = new Correlation(5);
|
||||
double[] x = [1, 2, 3, 4, 5];
|
||||
double[] y = [2, 4, 5, 4, 5];
|
||||
|
||||
for (int i = 0; i < 5; i++)
|
||||
{
|
||||
indicator.Update(x[i], y[i]);
|
||||
}
|
||||
|
||||
double expected = 1.2 / Math.Sqrt(2.0 * 1.2); // ≈ 0.7746
|
||||
Assert.Equal(expected, indicator.Last.Value, 1e-4);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Correlation_KnownValues_NoCorrelation()
|
||||
{
|
||||
// X = [1, 2, 3, 4, 5], Y = [3, 3, 3, 3, 3] (constant)
|
||||
// Should be NaN (or 0 with special handling)
|
||||
var indicator = new Correlation(5);
|
||||
double[] x = [1, 2, 3, 4, 5];
|
||||
double[] y = [3, 3, 3, 3, 3];
|
||||
|
||||
for (int i = 0; i < 5; i++)
|
||||
{
|
||||
indicator.Update(x[i], y[i]);
|
||||
}
|
||||
|
||||
// Zero variance in Y means correlation is undefined
|
||||
Assert.True(double.IsNaN(indicator.Last.Value));
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Consistency Tests
|
||||
|
||||
[Fact]
|
||||
public void Correlation_BatchMatchesStreaming()
|
||||
{
|
||||
var seriesX = new TSeries();
|
||||
var seriesY = new TSeries();
|
||||
var baseTime = DateTime.UtcNow;
|
||||
var gbmX = new GBM(startPrice: 100, mu: 0.02, sigma: 0.2, seed: 12345);
|
||||
var gbmY = new GBM(startPrice: 50, mu: 0.01, sigma: 0.15, seed: 54321);
|
||||
|
||||
for (int i = 0; i < 100; i++)
|
||||
{
|
||||
seriesX.Add(baseTime.AddMinutes(i), gbmX.Next().Close);
|
||||
seriesY.Add(baseTime.AddMinutes(i), gbmY.Next().Close);
|
||||
}
|
||||
|
||||
// Batch calculation
|
||||
var batchResult = Correlation.Calculate(seriesX, seriesY, 20);
|
||||
|
||||
// Streaming calculation
|
||||
var streamingIndicator = new Correlation(20);
|
||||
for (int i = 0; i < seriesX.Count; i++)
|
||||
{
|
||||
streamingIndicator.Update(seriesX[i].Value, seriesY[i].Value);
|
||||
}
|
||||
|
||||
// Last values should match
|
||||
if (double.IsNaN(batchResult.Last.Value) && double.IsNaN(streamingIndicator.Last.Value))
|
||||
{
|
||||
Assert.True(true);
|
||||
}
|
||||
else
|
||||
{
|
||||
Assert.Equal(batchResult.Last.Value, streamingIndicator.Last.Value, Tolerance);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Correlation_SpanMatchesStreaming()
|
||||
{
|
||||
const int length = 100;
|
||||
var seriesX = new double[length];
|
||||
var seriesY = new double[length];
|
||||
var output = new double[length];
|
||||
var gbmX = new GBM(startPrice: 100, mu: 0.02, sigma: 0.2, seed: 12345);
|
||||
var gbmY = new GBM(startPrice: 50, mu: 0.01, sigma: 0.15, seed: 54321);
|
||||
|
||||
for (int i = 0; i < length; i++)
|
||||
{
|
||||
seriesX[i] = gbmX.Next().Close;
|
||||
seriesY[i] = gbmY.Next().Close;
|
||||
}
|
||||
|
||||
// Span calculation
|
||||
Correlation.Calculate(seriesX, seriesY, output, 20);
|
||||
|
||||
// Streaming calculation
|
||||
var streamingIndicator = new Correlation(20);
|
||||
for (int i = 0; i < length; i++)
|
||||
{
|
||||
streamingIndicator.Update(seriesX[i], seriesY[i]);
|
||||
}
|
||||
|
||||
// Last values should match
|
||||
if (double.IsNaN(output[length - 1]) && double.IsNaN(streamingIndicator.Last.Value))
|
||||
{
|
||||
Assert.True(true);
|
||||
}
|
||||
else
|
||||
{
|
||||
Assert.Equal(output[length - 1], streamingIndicator.Last.Value, Tolerance);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Correlation_ResetProducesSameResults()
|
||||
{
|
||||
var indicator = new Correlation(20);
|
||||
var gbmX = new GBM(startPrice: 100, mu: 0.02, sigma: 0.2, seed: 12345);
|
||||
var gbmY = new GBM(startPrice: 50, mu: 0.01, sigma: 0.15, seed: 54321);
|
||||
|
||||
// First run
|
||||
for (int i = 0; i < 50; i++)
|
||||
{
|
||||
indicator.Update(gbmX.Next().Close, gbmY.Next().Close);
|
||||
}
|
||||
var firstResult = indicator.Last.Value;
|
||||
|
||||
indicator.Reset();
|
||||
|
||||
// Second run with same seeds
|
||||
gbmX = new GBM(startPrice: 100, mu: 0.02, sigma: 0.2, seed: 12345);
|
||||
gbmY = new GBM(startPrice: 50, mu: 0.01, sigma: 0.15, seed: 54321);
|
||||
for (int i = 0; i < 50; i++)
|
||||
{
|
||||
indicator.Update(gbmX.Next().Close, gbmY.Next().Close);
|
||||
}
|
||||
var secondResult = indicator.Last.Value;
|
||||
|
||||
Assert.Equal(firstResult, secondResult, Tolerance);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Rolling Window Tests
|
||||
|
||||
[Fact]
|
||||
public void Correlation_SlidingWindow_MovesCorrectly()
|
||||
{
|
||||
var indicator = new Correlation(5);
|
||||
|
||||
// Build up with known values for period 5
|
||||
// After 5 values, window should be full
|
||||
double[] x = [10, 20, 30, 40, 50, 60, 70];
|
||||
double[] y = [15, 25, 35, 45, 55, 65, 75];
|
||||
|
||||
for (int i = 0; i < 5; i++)
|
||||
{
|
||||
indicator.Update(x[i], y[i]);
|
||||
}
|
||||
|
||||
// Perfect correlation with same-slope linear data
|
||||
Assert.Equal(1.0, indicator.Last.Value, 1e-9);
|
||||
|
||||
// Add more - window should slide
|
||||
indicator.Update(x[5], y[5]);
|
||||
Assert.Equal(1.0, indicator.Last.Value, 1e-9); // Still perfect linear
|
||||
|
||||
indicator.Update(x[6], y[6]);
|
||||
Assert.Equal(1.0, indicator.Last.Value, 1e-9); // Still perfect linear
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Correlation_SlidingWindow_DropsOldValues()
|
||||
{
|
||||
var indicator = new Correlation(3);
|
||||
|
||||
// First window: perfectly correlated
|
||||
indicator.Update(1, 2);
|
||||
indicator.Update(2, 4);
|
||||
indicator.Update(3, 6);
|
||||
Assert.Equal(1.0, indicator.Last.Value, 1e-9);
|
||||
|
||||
// Add value that breaks perfect correlation in new window
|
||||
indicator.Update(4, 7); // Window is now [2,4,7] for Y, [2,3,4] for X
|
||||
// Not perfect linear anymore
|
||||
Assert.NotEqual(1.0, indicator.Last.Value);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Numerical Stability
|
||||
|
||||
[Fact]
|
||||
public void Correlation_LargeValues_MaintainsStability()
|
||||
{
|
||||
var indicator = new Correlation(20);
|
||||
|
||||
for (int i = 0; i < 50; i++)
|
||||
{
|
||||
double x = 1e8 + i * 1e5;
|
||||
double y = 2e8 + 2.0 * (i * 1e5); // Linear relationship
|
||||
indicator.Update(x, y);
|
||||
}
|
||||
|
||||
// Should still detect linear relationship
|
||||
Assert.InRange(indicator.Last.Value, 0.99, 1.01);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Correlation_SmallValues_MaintainsStability()
|
||||
{
|
||||
var indicator = new Correlation(20);
|
||||
|
||||
// Use values that are small but not so small they cause numerical issues
|
||||
for (int i = 0; i < 50; i++)
|
||||
{
|
||||
double x = 0.001 + i * 0.0001;
|
||||
double y = 0.002 + 1.5 * (i * 0.0001); // Linear relationship
|
||||
indicator.Update(x, y);
|
||||
}
|
||||
|
||||
// Should still detect linear relationship
|
||||
Assert.InRange(indicator.Last.Value, 0.99, 1.01);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Correlation_MixedMagnitudes_HandlesCorrectly()
|
||||
{
|
||||
var indicator = new Correlation(20);
|
||||
|
||||
for (int i = 0; i < 50; i++)
|
||||
{
|
||||
double x = 1000.0 + i;
|
||||
double y = 0.001 * (1000.0 + i); // Same pattern, different scale
|
||||
indicator.Update(x, y);
|
||||
}
|
||||
|
||||
// Should detect perfect correlation despite scale difference
|
||||
Assert.Equal(1.0, indicator.Last.Value, 1e-9);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Statistical Scenarios
|
||||
|
||||
[Fact]
|
||||
public void Correlation_HighPositiveCorrelation_DetectedCorrectly()
|
||||
{
|
||||
// Create two series with high positive correlation (r ≈ 0.95+)
|
||||
var indicator = new Correlation(20);
|
||||
|
||||
// Use deterministic data that creates high correlation
|
||||
for (int i = 0; i < 100; i++)
|
||||
{
|
||||
double x = 100.0 + i + (i % 3) * 0.1; // Small variation
|
||||
double y = 0.9 * x + (i % 5) * 0.2; // High correlation with small noise
|
||||
indicator.Update(x, y);
|
||||
}
|
||||
|
||||
Assert.True(indicator.Last.Value > 0.9);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Correlation_NegativeCorrelation_DetectedCorrectly()
|
||||
{
|
||||
// Create two series with negative correlation
|
||||
var indicator = new Correlation(20);
|
||||
var random = new Random(42);
|
||||
|
||||
for (int i = 0; i < 100; i++)
|
||||
{
|
||||
double x = 100.0 + i + (random.NextDouble() - 0.5) * 2;
|
||||
double y = 200.0 - 0.8 * i + (random.NextDouble() - 0.5) * 2; // Negative relationship
|
||||
indicator.Update(x, y);
|
||||
}
|
||||
|
||||
Assert.True(indicator.Last.Value < -0.8);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Correlation_WeakCorrelation_DetectedCorrectly()
|
||||
{
|
||||
// Create two series with weak correlation (lots of noise)
|
||||
var indicator = new Correlation(20);
|
||||
var random = new Random(42);
|
||||
|
||||
for (int i = 0; i < 100; i++)
|
||||
{
|
||||
double x = 100.0 + i + (random.NextDouble() - 0.5) * 50;
|
||||
double y = 100.0 + 0.1 * i + (random.NextDouble() - 0.5) * 50; // Weak relationship
|
||||
indicator.Update(x, y);
|
||||
}
|
||||
|
||||
// Should be close to zero but may be positive or negative
|
||||
Assert.InRange(Math.Abs(indicator.Last.Value), 0, 0.5);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Different Period Tests
|
||||
|
||||
[Fact]
|
||||
public void Correlation_DifferentPeriods_ProduceDifferentResults()
|
||||
{
|
||||
var indicator5 = new Correlation(5);
|
||||
var indicator20 = new Correlation(20);
|
||||
var indicator50 = new Correlation(50);
|
||||
var gbmX = new GBM(startPrice: 100, mu: 0.02, sigma: 0.2, seed: 12345);
|
||||
var gbmY = new GBM(startPrice: 50, mu: 0.01, sigma: 0.15, seed: 54321);
|
||||
|
||||
for (int i = 0; i < 100; i++)
|
||||
{
|
||||
double x = gbmX.Next().Close;
|
||||
double y = gbmY.Next().Close;
|
||||
indicator5.Update(x, y);
|
||||
indicator20.Update(x, y);
|
||||
indicator50.Update(x, y);
|
||||
}
|
||||
|
||||
// Different periods should yield different values
|
||||
Assert.NotEqual(indicator5.Last.Value, indicator20.Last.Value);
|
||||
Assert.NotEqual(indicator20.Last.Value, indicator50.Last.Value);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Correlation_SmallPeriod_MoreVolatile()
|
||||
{
|
||||
var indicator3 = new Correlation(3);
|
||||
var indicator30 = new Correlation(30);
|
||||
var gbmX = new GBM(startPrice: 100, mu: 0.02, sigma: 0.2, seed: 12345);
|
||||
var gbmY = new GBM(startPrice: 50, mu: 0.01, sigma: 0.15, seed: 54321);
|
||||
|
||||
var values3 = new List<double>();
|
||||
var values30 = new List<double>();
|
||||
|
||||
for (int i = 0; i < 100; i++)
|
||||
{
|
||||
double x = gbmX.Next().Close;
|
||||
double y = gbmY.Next().Close;
|
||||
indicator3.Update(x, y);
|
||||
indicator30.Update(x, y);
|
||||
|
||||
if (double.IsFinite(indicator3.Last.Value))
|
||||
{
|
||||
values3.Add(indicator3.Last.Value);
|
||||
}
|
||||
|
||||
if (double.IsFinite(indicator30.Last.Value))
|
||||
{
|
||||
values30.Add(indicator30.Last.Value);
|
||||
}
|
||||
}
|
||||
|
||||
// Calculate variance of correlation values
|
||||
double variance3 = CalculateVariance(values3);
|
||||
double variance30 = CalculateVariance(values30);
|
||||
|
||||
// Shorter period should have higher variance (more volatile)
|
||||
Assert.True(variance3 > variance30, $"Expected small period variance ({variance3}) > large period variance ({variance30})");
|
||||
}
|
||||
|
||||
private static double CalculateVariance(List<double> values)
|
||||
{
|
||||
if (values.Count < 2)
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
|
||||
double mean = values.Average();
|
||||
return values.Sum(v => (v - mean) * (v - mean)) / (values.Count - 1);
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
@@ -0,0 +1,345 @@
|
||||
using System.Runtime.CompilerServices;
|
||||
using static System.Math;
|
||||
|
||||
namespace QuanTAlib;
|
||||
|
||||
/// <summary>
|
||||
/// Correlation: Calculates Pearson's correlation coefficient between two price series
|
||||
/// using a streaming single-pass algorithm with circular buffers.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// The Pearson correlation coefficient measures the linear relationship between two variables.
|
||||
/// It ranges from -1 (perfect negative correlation) to +1 (perfect positive correlation).
|
||||
///
|
||||
/// Algorithm:
|
||||
/// 1. Maintain running sums: Σx, Σy, Σx², Σy², Σxy
|
||||
/// 2. Calculate means: μx = Σx/n, μy = Σy/n
|
||||
/// 3. Calculate variances: σx² = Σx²/n - μx², σy² = Σy²/n - μy²
|
||||
/// 4. Calculate covariance: cov(x,y) = Σxy/n - μx×μy
|
||||
/// 5. Correlation: r = cov(x,y) / (σx × σy)
|
||||
///
|
||||
/// Interpretation:
|
||||
/// - r = +1: Perfect positive linear relationship
|
||||
/// - r = -1: Perfect negative linear relationship
|
||||
/// - r = 0: No linear relationship
|
||||
/// - |r| > 0.7: Strong correlation
|
||||
/// - 0.3 < |r| < 0.7: Moderate correlation
|
||||
/// - |r| < 0.3: Weak correlation
|
||||
/// </remarks>
|
||||
[SkipLocalsInit]
|
||||
public sealed class Correlation : AbstractBase
|
||||
{
|
||||
private readonly RingBuffer _bufferX;
|
||||
private readonly RingBuffer _bufferY;
|
||||
|
||||
// Running sums for O(1) statistics
|
||||
private double _sumX, _sumY;
|
||||
private double _sumX2, _sumY2;
|
||||
private double _sumXY;
|
||||
|
||||
// Last valid values for NaN handling
|
||||
private double _lastValidX, _lastValidY;
|
||||
|
||||
private int _updateCount;
|
||||
private const int ResyncInterval = 1000;
|
||||
private const double Epsilon = 1e-10;
|
||||
|
||||
public override bool IsHot => _bufferX.Count >= 2;
|
||||
|
||||
/// <summary>
|
||||
/// Creates a new Correlation indicator.
|
||||
/// </summary>
|
||||
/// <param name="period">Lookback period for calculation (must be > 1)</param>
|
||||
public Correlation(int period = 20)
|
||||
{
|
||||
if (period <= 1)
|
||||
{
|
||||
throw new ArgumentException("Period must be greater than 1", nameof(period));
|
||||
}
|
||||
|
||||
_bufferX = new RingBuffer(period);
|
||||
_bufferY = new RingBuffer(period);
|
||||
|
||||
Name = $"Correlation({period})";
|
||||
WarmupPeriod = period;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Updates the Correlation indicator with new values from both series.
|
||||
/// </summary>
|
||||
/// <param name="seriesX">First series value</param>
|
||||
/// <param name="seriesY">Second series value</param>
|
||||
/// <param name="isNew">Whether this is a new bar</param>
|
||||
/// <returns>The Pearson correlation coefficient (-1 to +1)</returns>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public TValue Update(TValue seriesX, TValue seriesY, bool isNew = true)
|
||||
{
|
||||
double x = SanitizeX(seriesX.Value);
|
||||
double y = SanitizeY(seriesY.Value);
|
||||
|
||||
if (isNew)
|
||||
{
|
||||
ProcessNewBar(x, y);
|
||||
}
|
||||
else
|
||||
{
|
||||
ProcessBarCorrection(x, y);
|
||||
}
|
||||
|
||||
double correlation = CalculateCorrelation();
|
||||
|
||||
Last = new TValue(seriesX.Time, correlation);
|
||||
PubEvent(Last);
|
||||
return Last;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Updates with raw double values.
|
||||
/// </summary>
|
||||
[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);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
/// <remarks>Not supported for bi-input indicator. Use Update(seriesX, seriesY) instead.</remarks>
|
||||
public override TValue Update(TValue input, bool isNew = true)
|
||||
{
|
||||
throw new NotSupportedException("Correlation requires two inputs (seriesX and seriesY). Use Update(seriesX, seriesY).");
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
/// <remarks>Not supported for bi-input indicator. Use Calculate(seriesX, seriesY, period) instead.</remarks>
|
||||
public override TSeries Update(TSeries source)
|
||||
{
|
||||
throw new NotSupportedException("Correlation requires two inputs. Use Calculate(seriesX, seriesY, period).");
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
private double SanitizeX(double value)
|
||||
{
|
||||
if (double.IsFinite(value))
|
||||
{
|
||||
_lastValidX = value;
|
||||
return value;
|
||||
}
|
||||
return double.IsFinite(_lastValidX) ? _lastValidX : 0.0;
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
private double SanitizeY(double value)
|
||||
{
|
||||
if (double.IsFinite(value))
|
||||
{
|
||||
_lastValidY = value;
|
||||
return value;
|
||||
}
|
||||
return double.IsFinite(_lastValidY) ? _lastValidY : 0.0;
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
private void ProcessNewBar(double x, double y)
|
||||
{
|
||||
// Remove oldest values if buffer is full
|
||||
if (_bufferX.IsFull)
|
||||
{
|
||||
double oldX = _bufferX.Oldest;
|
||||
double oldY = _bufferY.Oldest;
|
||||
_sumX -= oldX;
|
||||
_sumY -= oldY;
|
||||
_sumX2 = FusedMultiplyAdd(-oldX, oldX, _sumX2);
|
||||
_sumY2 = FusedMultiplyAdd(-oldY, oldY, _sumY2);
|
||||
_sumXY = FusedMultiplyAdd(-oldX, oldY, _sumXY);
|
||||
}
|
||||
|
||||
// Add new values
|
||||
_bufferX.Add(x);
|
||||
_bufferY.Add(y);
|
||||
|
||||
_sumX += x;
|
||||
_sumY += y;
|
||||
_sumX2 = FusedMultiplyAdd(x, x, _sumX2);
|
||||
_sumY2 = FusedMultiplyAdd(y, y, _sumY2);
|
||||
_sumXY = FusedMultiplyAdd(x, y, _sumXY);
|
||||
|
||||
_updateCount++;
|
||||
if (_updateCount % ResyncInterval == 0)
|
||||
{
|
||||
Resync();
|
||||
}
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
private void ProcessBarCorrection(double x, double y)
|
||||
{
|
||||
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;
|
||||
return;
|
||||
}
|
||||
|
||||
// Get the current newest values (which are wrong and need to be corrected)
|
||||
double oldX = _bufferX.Newest;
|
||||
double oldY = _bufferY.Newest;
|
||||
|
||||
// Update the running sums: remove old, add new
|
||||
_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);
|
||||
|
||||
// Update the buffer values
|
||||
_bufferX.UpdateNewest(x);
|
||||
_bufferY.UpdateNewest(y);
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
private double CalculateCorrelation()
|
||||
{
|
||||
int n = _bufferX.Count;
|
||||
if (n < 2)
|
||||
{
|
||||
return double.NaN;
|
||||
}
|
||||
|
||||
// Calculate means
|
||||
double meanX = _sumX / n;
|
||||
double meanY = _sumY / n;
|
||||
|
||||
// Calculate variances (population variance)
|
||||
double varX = Max(0.0, (_sumX2 / n) - (meanX * meanX));
|
||||
double varY = Max(0.0, (_sumY2 / n) - (meanY * meanY));
|
||||
|
||||
// Calculate covariance
|
||||
double cov = (_sumXY / n) - (meanX * meanY);
|
||||
|
||||
// Calculate standard deviations
|
||||
double stdX = Sqrt(varX);
|
||||
double stdY = Sqrt(varY);
|
||||
|
||||
// Calculate correlation
|
||||
double denominator = stdX * stdY;
|
||||
if (Abs(denominator) < Epsilon)
|
||||
{
|
||||
return double.NaN;
|
||||
}
|
||||
|
||||
double correlation = cov / denominator;
|
||||
|
||||
// Clamp to [-1, 1] range to handle floating point precision issues
|
||||
return Max(-1.0, Min(1.0, correlation));
|
||||
}
|
||||
|
||||
private void Resync()
|
||||
{
|
||||
_sumX = 0;
|
||||
_sumY = 0;
|
||||
_sumX2 = 0;
|
||||
_sumY2 = 0;
|
||||
_sumXY = 0;
|
||||
|
||||
for (int i = 0; i < _bufferX.Count; i++)
|
||||
{
|
||||
double x = _bufferX[i];
|
||||
double y = _bufferY[i];
|
||||
_sumX += x;
|
||||
_sumY += y;
|
||||
_sumX2 = FusedMultiplyAdd(x, x, _sumX2);
|
||||
_sumY2 = FusedMultiplyAdd(y, y, _sumY2);
|
||||
_sumXY = FusedMultiplyAdd(x, y, _sumXY);
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override void Prime(ReadOnlySpan<double> source, TimeSpan? step = null)
|
||||
{
|
||||
throw new NotSupportedException("Correlation requires two inputs.");
|
||||
}
|
||||
|
||||
public override void Reset()
|
||||
{
|
||||
_bufferX.Clear();
|
||||
_bufferY.Clear();
|
||||
|
||||
_sumX = 0;
|
||||
_sumY = 0;
|
||||
_sumX2 = 0;
|
||||
_sumY2 = 0;
|
||||
_sumXY = 0;
|
||||
|
||||
_lastValidX = 0;
|
||||
_lastValidY = 0;
|
||||
|
||||
_updateCount = 0;
|
||||
Last = default;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Calculates correlation for two time series.
|
||||
/// </summary>
|
||||
public static TSeries 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);
|
||||
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;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Static batch calculation for span-based processing.
|
||||
/// </summary>
|
||||
public static void Calculate(
|
||||
ReadOnlySpan<double> seriesX,
|
||||
ReadOnlySpan<double> seriesY,
|
||||
Span<double> output,
|
||||
int period = 20)
|
||||
{
|
||||
if (seriesX.Length != seriesY.Length)
|
||||
{
|
||||
throw new ArgumentException("Series must have the same length", nameof(seriesY));
|
||||
}
|
||||
|
||||
if (seriesX.Length != output.Length)
|
||||
{
|
||||
throw new ArgumentException("Output must have the same length as input", nameof(output));
|
||||
}
|
||||
|
||||
if (period <= 1)
|
||||
{
|
||||
throw new ArgumentException("Period must be greater than 1", nameof(period));
|
||||
}
|
||||
|
||||
var indicator = new Correlation(period);
|
||||
|
||||
for (int i = 0; i < seriesX.Length; i++)
|
||||
{
|
||||
var result = indicator.Update(seriesX[i], seriesY[i], isNew: true);
|
||||
output[i] = result.Value;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,268 @@
|
||||
# CORR: Pearson Correlation Coefficient
|
||||
|
||||
> "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."
|
||||
|
||||
The Pearson Correlation Coefficient measures the linear relationship between two variables, returning a value from -1 (perfect negative correlation) to +1 (perfect positive correlation). Zero indicates no linear relationship. This implementation uses running sums for O(1) streaming updates, making it suitable for real-time analysis of price relationships.
|
||||
|
||||
## Historical Context
|
||||
|
||||
Karl Pearson formalized the correlation coefficient in the 1890s, building on earlier work by Francis Galton. The formula has remained unchanged for over a century because it elegantly captures what traders intuitively understand: when two instruments move together, there's an exploitable relationship.
|
||||
|
||||
Unlike cointegration (which tests for long-run equilibrium), correlation measures instantaneous co-movement. Two stocks can be highly correlated yet drift apart permanently—correlation tells you about direction, not destination. This distinction matters enormously for pairs trading: correlation helps with hedging and timing, but cointegration determines whether mean-reversion is statistically justified.
|
||||
|
||||
This implementation follows the PineScript reference, using circular buffers and running sums to achieve constant-time updates regardless of lookback period.
|
||||
|
||||
## Architecture & Physics
|
||||
|
||||
### 1. Running Sums Framework
|
||||
|
||||
The indicator maintains five running sums updated incrementally:
|
||||
|
||||
| Sum | Description | Formula |
|
||||
| :--- | :--- | :--- |
|
||||
| $S_X$ | Sum of X values | $\sum_{i=1}^{n} X_i$ |
|
||||
| $S_Y$ | Sum of Y values | $\sum_{i=1}^{n} Y_i$ |
|
||||
| $S_{X^2}$ | Sum of X squared | $\sum_{i=1}^{n} X_i^2$ |
|
||||
| $S_{Y^2}$ | Sum of Y squared | $\sum_{i=1}^{n} Y_i^2$ |
|
||||
| $S_{XY}$ | Sum of X×Y products | $\sum_{i=1}^{n} X_i Y_i$ |
|
||||
|
||||
### 2. Circular Buffer
|
||||
|
||||
A `RingBuffer` of capacity `period` stores paired values. When full, the oldest pair is subtracted from running sums before adding the new pair—maintaining O(1) complexity regardless of period length.
|
||||
|
||||
### 3. Correlation Formula
|
||||
|
||||
The Pearson coefficient is computed as:
|
||||
|
||||
$$r = \frac{\text{Cov}(X, Y)}{\sigma_X \cdot \sigma_Y}$$
|
||||
|
||||
Expanded using running sums:
|
||||
|
||||
$$r = \frac{n \cdot S_{XY} - S_X \cdot S_Y}{\sqrt{(n \cdot S_{X^2} - S_X^2)(n \cdot S_{Y^2} - S_Y^2)}}$$
|
||||
|
||||
Where $n$ is the number of observations (capped at `period`).
|
||||
|
||||
### 4. Edge Case Handling
|
||||
|
||||
| Condition | Result | Rationale |
|
||||
| :--- | :--- | :--- |
|
||||
| Zero variance in X or Y | NaN | Division by zero—undefined correlation |
|
||||
| Insufficient data | NaN | Need at least 2 points |
|
||||
| NaN/Infinity input | Last valid value | Substitution preserves series continuity |
|
||||
|
||||
## Mathematical Foundation
|
||||
|
||||
### Derivation from Covariance
|
||||
|
||||
Starting with the population covariance:
|
||||
|
||||
$$\text{Cov}(X, Y) = \frac{\sum(X_i - \bar{X})(Y_i - \bar{Y})}{n}$$
|
||||
|
||||
Expanding:
|
||||
|
||||
$$\text{Cov}(X, Y) = \frac{\sum X_i Y_i}{n} - \bar{X} \cdot \bar{Y}$$
|
||||
|
||||
$$= \frac{S_{XY}}{n} - \frac{S_X}{n} \cdot \frac{S_Y}{n}$$
|
||||
|
||||
$$= \frac{n \cdot S_{XY} - S_X \cdot S_Y}{n^2}$$
|
||||
|
||||
Similarly for standard deviations:
|
||||
|
||||
$$\sigma_X = \sqrt{\frac{S_{X^2}}{n} - \left(\frac{S_X}{n}\right)^2} = \frac{\sqrt{n \cdot S_{X^2} - S_X^2}}{n}$$
|
||||
|
||||
Combining:
|
||||
|
||||
$$r = \frac{\text{Cov}(X, Y)}{\sigma_X \cdot \sigma_Y} = \frac{n \cdot S_{XY} - S_X \cdot S_Y}{\sqrt{(n \cdot S_{X^2} - S_X^2)(n \cdot S_{Y^2} - S_Y^2)}}$$
|
||||
|
||||
### Update Mechanics
|
||||
|
||||
When a new pair $(x_{new}, y_{new})$ arrives and an old pair $(x_{old}, y_{old})$ exits the window:
|
||||
|
||||
$$S_X \leftarrow S_X - x_{old} + x_{new}$$
|
||||
$$S_Y \leftarrow S_Y - y_{old} + y_{new}$$
|
||||
$$S_{X^2} \leftarrow S_{X^2} - x_{old}^2 + x_{new}^2$$
|
||||
$$S_{Y^2} \leftarrow S_{Y^2} - y_{old}^2 + y_{new}^2$$
|
||||
$$S_{XY} \leftarrow S_{XY} - x_{old} \cdot y_{old} + x_{new} \cdot y_{new}$$
|
||||
|
||||
This achieves O(1) per-bar complexity.
|
||||
|
||||
## Performance Profile
|
||||
|
||||
### Operation Count (Streaming Mode, Scalar)
|
||||
|
||||
| Operation | Count | Cost (cycles) | Subtotal |
|
||||
| :--- | :---: | :---: | :---: |
|
||||
| ADD/SUB | 12 | 1 | 12 |
|
||||
| MUL | 8 | 3 | 24 |
|
||||
| DIV | 1 | 15 | 15 |
|
||||
| SQRT | 1 | 15 | 15 |
|
||||
| Buffer Access | 2 | 3 | 6 |
|
||||
| **Total** | **24** | — | **~72 cycles** |
|
||||
|
||||
Correlation is significantly cheaper than cointegration (~72 vs ~282 cycles) because it doesn't require the ADF regression step.
|
||||
|
||||
### Memory Footprint
|
||||
|
||||
| Component | Size |
|
||||
| :--- | :--- |
|
||||
| Ring buffer (period × 2 doubles) | 16 × period bytes |
|
||||
| Running sums (5 doubles) | 40 bytes |
|
||||
| State variables | 32 bytes |
|
||||
| **Total per instance** | **~16 × period + 72 bytes** |
|
||||
|
||||
For period=20: ~392 bytes per indicator instance.
|
||||
|
||||
### Batch Mode (SIMD Potential)
|
||||
|
||||
The correlation formula is not directly SIMD-friendly due to the final division and square root. However, the running sum accumulation phase can benefit from vectorization when processing batches:
|
||||
|
||||
| Phase | SIMD Benefit |
|
||||
| :--- | :--- |
|
||||
| Sum accumulation | 4-8× (AVX2/AVX-512) |
|
||||
| Final formula | 1× (scalar) |
|
||||
| **Overall improvement** | ~2-3× for batch processing |
|
||||
|
||||
### Quality Metrics
|
||||
|
||||
| Metric | Score | Notes |
|
||||
| :--- | :---: | :--- |
|
||||
| **Accuracy** | 10/10 | Exact Pearson formula |
|
||||
| **Timeliness** | 8/10 | Responsive to recent changes |
|
||||
| **Robustness** | 9/10 | Handles edge cases gracefully |
|
||||
| **Interpretability** | 10/10 | Universal [-1, +1] scale |
|
||||
|
||||
## Validation
|
||||
|
||||
| Library | Status | Notes |
|
||||
| :--- | :---: | :--- |
|
||||
| **TA-Lib** | N/A | No correlation implementation |
|
||||
| **Skender** | N/A | No direct correlation (has Beta) |
|
||||
| **Tulip** | N/A | No correlation implementation |
|
||||
| **Ooples** | N/A | No correlation implementation |
|
||||
| **TradingView** | ✅ | Matches PineScript `ta.correlation()` |
|
||||
| **Mathematical** | ✅ | Validated against known properties |
|
||||
|
||||
Note: Correlation is typically found in statistical packages rather than TA libraries. This implementation validates against mathematical properties (symmetry, boundedness, scale invariance) and the PineScript reference.
|
||||
|
||||
## Use Cases
|
||||
|
||||
### 1. Hedging
|
||||
|
||||
Find correlated instruments to offset risk:
|
||||
- **r > 0.7**: Strong positive correlation, use for portfolio diversification analysis
|
||||
- **r < -0.7**: Strong negative correlation, natural hedges
|
||||
|
||||
### 2. Pairs Trading (Short-Term)
|
||||
|
||||
Identify co-moving pairs for short-term mean reversion:
|
||||
- High correlation indicates pairs move together
|
||||
- Combine with cointegration for statistical justification
|
||||
|
||||
### 3. Sector Analysis
|
||||
|
||||
Measure how closely a stock tracks its sector or index:
|
||||
- Rolling correlation reveals changing relationships
|
||||
- Divergence from sector may signal alpha opportunities
|
||||
|
||||
### 4. Risk Management
|
||||
|
||||
Monitor correlation stability:
|
||||
- Correlations tend toward 1 during market stress
|
||||
- "Correlation breakdown" can devastate hedged portfolios
|
||||
|
||||
## API Usage
|
||||
|
||||
### Streaming Mode (Bi-Input)
|
||||
|
||||
```csharp
|
||||
var corr = new Correlation(period: 20);
|
||||
foreach (var (priceA, priceB) in pricePairs)
|
||||
{
|
||||
var result = corr.Update(priceA, priceB);
|
||||
if (corr.IsHot)
|
||||
{
|
||||
Console.WriteLine($"Correlation: {result.Value:F4}");
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Batch Mode
|
||||
|
||||
```csharp
|
||||
var seriesA = new TSeries();
|
||||
var seriesB = new TSeries();
|
||||
// ... populate series ...
|
||||
var results = Correlation.Calculate(seriesA, seriesB, period: 20);
|
||||
```
|
||||
|
||||
### Span Mode (Zero Allocation)
|
||||
|
||||
```csharp
|
||||
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);
|
||||
```
|
||||
|
||||
### Bar Correction Support
|
||||
|
||||
```csharp
|
||||
var corr = new Correlation(20);
|
||||
|
||||
// New bar
|
||||
corr.Update(100.0, 50.0, isNew: true); // r = 0.85
|
||||
|
||||
// Same bar corrected (e.g., real-time tick update)
|
||||
corr.Update(101.0, 51.0, isNew: false); // Recalculates without advancing state
|
||||
```
|
||||
|
||||
## Interpreting Results
|
||||
|
||||
| Correlation | Interpretation |
|
||||
| :---: | :--- |
|
||||
| **+0.7 to +1.0** | Strong positive: move in same direction |
|
||||
| **+0.3 to +0.7** | Moderate positive |
|
||||
| **-0.3 to +0.3** | Weak or no linear relationship |
|
||||
| **-0.7 to -0.3** | Moderate negative |
|
||||
| **-1.0 to -0.7** | Strong negative: move in opposite directions |
|
||||
|
||||
**Warning**: Correlation only measures *linear* relationships. Two variables with a perfect quadratic relationship (Y = X²) may show r ≈ 0.
|
||||
|
||||
## Common Pitfalls
|
||||
|
||||
1. **Confusing Correlation with Causation**: High correlation does not imply one variable causes changes in the other. Both may be driven by a third factor (confounding).
|
||||
|
||||
2. **Assuming Stability**: Correlations change over time. A 0.9 correlation over the past year doesn't guarantee 0.9 tomorrow. Rolling correlation reveals regime changes.
|
||||
|
||||
3. **Ignoring Non-Linear Relationships**: Pearson correlation misses curvilinear dependencies. If you suspect non-linear relationships, consider Spearman rank correlation instead.
|
||||
|
||||
4. **Crisis Correlation Spike**: During market stress, correlations tend toward 1.0 (or -1.0 for inverse ETFs). Diversification benefits evaporate precisely when you need them most.
|
||||
|
||||
5. **Lookback Period Selection**: Short periods (5-10) are noisy but responsive. Long periods (50-100) are stable but slow to adapt. Match the period to your trading horizon.
|
||||
|
||||
6. **Zero-Variance Edge Case**: If either series is constant within the window, variance is zero and correlation is undefined (NaN). This is mathematically correct.
|
||||
|
||||
7. **Warmup Period**: The indicator requires `period` bars before producing valid results. During warmup, `IsHot` returns false.
|
||||
|
||||
8. **Outlier Sensitivity**: Pearson correlation is sensitive to outliers. A single extreme observation can dramatically shift the coefficient. Consider winsorizing data or using Spearman for robustness.
|
||||
|
||||
## Correlation vs Cointegration
|
||||
|
||||
| Aspect | Correlation | Cointegration |
|
||||
| :--- | :--- | :--- |
|
||||
| **Measures** | Linear co-movement | Long-run equilibrium |
|
||||
| **Range** | [-1, +1] | ADF statistic (unbounded) |
|
||||
| **Time horizon** | Short-term | Long-term |
|
||||
| **Use case** | Hedging, risk | Pairs trading |
|
||||
| **Computational cost** | ~72 cycles | ~282 cycles |
|
||||
| **Stationarity required** | No | Yes (I(1) series) |
|
||||
|
||||
**Rule of thumb**: Use correlation for hedging and short-term analysis. Use cointegration for pairs trading and mean-reversion strategies.
|
||||
|
||||
## References
|
||||
|
||||
- Pearson, K. (1895). "Notes on regression and inheritance in the case of two parents." *Proceedings of the Royal Society of London*, 58, 240-242.
|
||||
- TradingView. "ta.correlation() function." *Pine Script Language Reference Manual*.
|
||||
- Vidyamurthy, G. (2004). "Pairs Trading: Quantitative Methods and Analysis." *Wiley Finance*. Chapter on correlation analysis.
|
||||
- Embrechts, P., McNeil, A., & Straumann, D. (2002). "Correlation and dependence in risk management: properties and pitfalls." *Risk Management: Value at Risk and Beyond*, Cambridge University Press.
|
||||
Reference in New Issue
Block a user