mirror of
https://github.com/mihakralj/QuanTAlib.git
synced 2026-07-30 02:27:43 +00:00
060649192f
- Remove 'C# Implementation Considerations' sections from 34 indicator .md files - Delete 29 temp PowerShell scripts (_fix_mojibake.ps1, _hex_scan.ps1, etc.) - Move test files into tests/ subdirectories for consistent project structure - Add trader-focused bullet points to indicator documentation
289 lines
9.8 KiB
C#
289 lines
9.8 KiB
C#
using Xunit;
|
||
|
||
namespace QuanTAlib.Tests;
|
||
|
||
/// <summary>
|
||
/// Validation tests for ACF (Autocorrelation Function).
|
||
/// ACF is not commonly implemented in trading libraries (TA-Lib, Skender, etc.),
|
||
/// so validation is done against mathematical properties and known theoretical results.
|
||
/// </summary>
|
||
public class AcfValidationTests
|
||
{
|
||
private const double Tolerance = 1e-9;
|
||
|
||
#region Mathematical Property Validation
|
||
|
||
[Fact]
|
||
public void Validation_AcfAtLagZero_ShouldBeOne()
|
||
{
|
||
// ACF at lag 0 = variance / variance = 1
|
||
// We can't directly test lag=0 (our minimum is 1), but we can verify
|
||
// that with highly correlated data (perfect positive correlation), ACF approaches 1
|
||
var acf = new Acf(20, 1);
|
||
|
||
// Create a series where each value is very close to the previous
|
||
// (linear trend: x_t = t)
|
||
for (int i = 0; i < 30; i++)
|
||
{
|
||
acf.Update(new TValue(DateTime.UtcNow.AddSeconds(i), i * 1.0));
|
||
}
|
||
|
||
// For a linear trend, lag-1 autocorrelation should be high (close to 1)
|
||
// With period=20, the sample ACF may be lower than theoretical due to finite window
|
||
Assert.True(acf.Last.Value >= 0.8, $"Linear trend should have high lag-1 ACF, got {acf.Last.Value}");
|
||
}
|
||
|
||
[Fact]
|
||
public void Validation_AcfBoundedByOne()
|
||
{
|
||
// ACF must always be in [-1, 1]
|
||
var gbm = new GBM(seed: 42);
|
||
var bars = gbm.Fetch(500, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1));
|
||
|
||
for (int lag = 1; lag <= 10; lag++)
|
||
{
|
||
var acf = new Acf(50, lag);
|
||
foreach (var bar in bars)
|
||
{
|
||
acf.Update(new TValue(bar.Time, bar.Close));
|
||
Assert.True(acf.Last.Value >= -1.0 && acf.Last.Value <= 1.0,
|
||
$"ACF at lag {lag} = {acf.Last.Value} is out of bounds");
|
||
}
|
||
}
|
||
}
|
||
|
||
[Fact]
|
||
public void Validation_ConstantSeries_AcfIsZeroOrUndefined()
|
||
{
|
||
// For a constant series, variance = 0, so ACF is undefined
|
||
// Our implementation returns 0 in this case
|
||
var acf = new Acf(20, 1);
|
||
|
||
for (int i = 0; i < 30; i++)
|
||
{
|
||
acf.Update(new TValue(DateTime.UtcNow.AddSeconds(i), 100.0));
|
||
}
|
||
|
||
Assert.Equal(0, acf.Last.Value);
|
||
}
|
||
|
||
[Fact]
|
||
public void Validation_AlternatingSequence_NegativeAcf()
|
||
{
|
||
// For an alternating sequence (100, -100, 100, -100, ...),
|
||
// the lag-1 ACF should be strongly negative (close to -1)
|
||
var acf = new Acf(20, 1);
|
||
|
||
for (int i = 0; i < 30; i++)
|
||
{
|
||
double val = i % 2 == 0 ? 100.0 : -100.0;
|
||
acf.Update(new TValue(DateTime.UtcNow.AddSeconds(i), val));
|
||
}
|
||
|
||
// Alternating sequence has perfect negative correlation at lag 1
|
||
Assert.True(acf.Last.Value < -0.9, $"Alternating sequence should have negative ACF, got {acf.Last.Value}");
|
||
}
|
||
|
||
[Fact]
|
||
public void Validation_PeriodicSequence_AcfMatchesPeriod()
|
||
{
|
||
// For a periodic sequence with period p, ACF at lag p should be high
|
||
int period = 4;
|
||
var acfLag4 = new Acf(20, period);
|
||
var acfLag2 = new Acf(20, 2); // Half period
|
||
|
||
// Create periodic sequence: 1, 2, 3, 4, 1, 2, 3, 4, ...
|
||
for (int i = 0; i < 50; i++)
|
||
{
|
||
double val = (i % period) + 1;
|
||
acfLag4.Update(new TValue(DateTime.UtcNow.AddSeconds(i), val));
|
||
acfLag2.Update(new TValue(DateTime.UtcNow.AddSeconds(i), val));
|
||
}
|
||
|
||
// ACF at lag=period should be high (perfect correlation in theory)
|
||
// With period=20 window and lag=4, finite sample effects reduce measured ACF
|
||
Assert.True(acfLag4.Last.Value >= 0.75, $"ACF at period lag should be high, got {acfLag4.Last.Value}");
|
||
|
||
// ACF at lag=period/2 for a sawtooth pattern (1,2,3,4 repeating) should be lower
|
||
// because values at distance 2 are not as correlated as at distance 4
|
||
}
|
||
|
||
[Fact]
|
||
public void Validation_RandomWhiteNoise_AcfNearZero()
|
||
{
|
||
// For white noise, ACF at any lag > 0 should be close to zero
|
||
var acf = new Acf(100, 5);
|
||
|
||
// Generate incremental bar-to-bar log-returns: log(close_i / close_{i-1})
|
||
// These are approximately i.i.d. N(0, σ²·dt) — genuine white noise
|
||
var gbm = new GBM(startPrice: 100.0, sigma: 0.2, seed: 42);
|
||
var bars = gbm.Fetch(501, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1));
|
||
for (int i = 1; i < 501; i++)
|
||
{
|
||
double val = Math.Log(bars[i].Close / bars[i - 1].Close); // incremental log-return
|
||
acf.Update(new TValue(DateTime.UtcNow.AddSeconds(i), val));
|
||
}
|
||
|
||
// For white noise, ACF should be close to zero (but not exactly due to finite sample)
|
||
// Standard error is approximately 1/sqrt(n) ≈ 0.1 for n=100
|
||
Assert.True(Math.Abs(acf.Last.Value) < 0.3,
|
||
$"White noise ACF at lag 5 should be near zero, got {acf.Last.Value}");
|
||
}
|
||
|
||
#endregion
|
||
|
||
#region Streaming vs Batch Consistency
|
||
|
||
[Theory]
|
||
[InlineData(42)]
|
||
[InlineData(123)]
|
||
[InlineData(999)]
|
||
public void Validation_StreamingMatchesBatch(int seed)
|
||
{
|
||
const int period = 20;
|
||
const int lag = 1;
|
||
const int dataLen = 100;
|
||
|
||
var gbm = new GBM(seed: seed);
|
||
var bars = gbm.Fetch(dataLen, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1));
|
||
|
||
// Streaming
|
||
var streaming = new Acf(period, lag);
|
||
foreach (var bar in bars)
|
||
{
|
||
streaming.Update(new TValue(bar.Time, bar.Close));
|
||
}
|
||
|
||
// Batch via TSeries
|
||
var tSeries = new TSeries();
|
||
foreach (var bar in bars)
|
||
{
|
||
tSeries.Add(new TValue(bar.Time, bar.Close));
|
||
}
|
||
|
||
var batch = Acf.Batch(tSeries, period, lag);
|
||
|
||
// Compare last values
|
||
Assert.Equal(batch[^1].Value, streaming.Last.Value, Tolerance);
|
||
}
|
||
|
||
[Fact]
|
||
public void Validation_SpanMatchesTSeries()
|
||
{
|
||
const int period = 14;
|
||
const int lag = 2;
|
||
const int dataLen = 200;
|
||
|
||
var gbm = new GBM(seed: 77);
|
||
var bars = gbm.Fetch(dataLen, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1));
|
||
|
||
// TSeries approach
|
||
var tSeries = new TSeries();
|
||
foreach (var bar in bars)
|
||
{
|
||
tSeries.Add(new TValue(bar.Time, bar.Close));
|
||
}
|
||
|
||
var tSeriesResult = Acf.Batch(tSeries, period, lag);
|
||
|
||
// Span approach
|
||
double[] source = new double[dataLen];
|
||
double[] spanResult = new double[dataLen];
|
||
for (int i = 0; i < dataLen; i++)
|
||
{
|
||
source[i] = bars[i].Close;
|
||
}
|
||
|
||
Acf.Batch(source, spanResult, period, lag);
|
||
|
||
// Compare all values after warmup
|
||
for (int i = period; i < dataLen; i++)
|
||
{
|
||
Assert.Equal(tSeriesResult[i].Value, spanResult[i], Tolerance);
|
||
}
|
||
}
|
||
|
||
#endregion
|
||
|
||
#region AR(1) Process Validation
|
||
|
||
[Fact]
|
||
public void Validation_AR1Process_AcfDecaysExponentially()
|
||
{
|
||
// For an AR(1) process: X_t = φ * X_{t-1} + ε_t
|
||
// The theoretical ACF at lag k is φ^k
|
||
double phi = 0.8; // AR(1) coefficient
|
||
const int n = 1000;
|
||
|
||
double[] ar1Data = new double[n];
|
||
ar1Data[0] = 0;
|
||
|
||
// Use incremental bar-to-bar log-returns as i.i.d. noise: log(close_i / close_{i-1})
|
||
var gbm = new GBM(startPrice: 100.0, sigma: 0.2, seed: 42);
|
||
var bars = gbm.Fetch(n, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1));
|
||
|
||
// Generate AR(1) process
|
||
for (int i = 1; i < n; i++)
|
||
{
|
||
double epsilon = Math.Log(bars[i].Close / bars[i - 1].Close) * 0.5; // incremental log-return scaled as noise
|
||
ar1Data[i] = phi * ar1Data[i - 1] + epsilon;
|
||
}
|
||
|
||
// Compute ACF at different lags
|
||
var acfLag1 = new Acf(200, 1);
|
||
var acfLag2 = new Acf(200, 2);
|
||
var acfLag3 = new Acf(200, 3);
|
||
|
||
for (int i = 0; i < n; i++)
|
||
{
|
||
var tv = new TValue(DateTime.UtcNow.AddSeconds(i), ar1Data[i]);
|
||
acfLag1.Update(tv);
|
||
acfLag2.Update(tv);
|
||
acfLag3.Update(tv);
|
||
}
|
||
|
||
// Theoretical values: ρ_1 = φ = 0.8, ρ_2 = φ² = 0.64, ρ_3 = φ³ = 0.512
|
||
// Allow some tolerance due to finite sample effects
|
||
Assert.True(acfLag1.Last.Value > 0.7 && acfLag1.Last.Value < 0.9,
|
||
$"ACF at lag 1 for AR(1) with φ=0.8 should be ~0.8, got {acfLag1.Last.Value}");
|
||
Assert.True(acfLag2.Last.Value > 0.5 && acfLag2.Last.Value < 0.8,
|
||
$"ACF at lag 2 for AR(1) with φ=0.8 should be ~0.64, got {acfLag2.Last.Value}");
|
||
Assert.True(acfLag3.Last.Value > 0.4 && acfLag3.Last.Value < 0.7,
|
||
$"ACF at lag 3 for AR(1) with φ=0.8 should be ~0.512, got {acfLag3.Last.Value}");
|
||
|
||
// Verify decay: ρ_1 > ρ_2 > ρ_3
|
||
Assert.True(acfLag1.Last.Value > acfLag2.Last.Value,
|
||
$"ACF should decay: lag1={acfLag1.Last.Value} should be > lag2={acfLag2.Last.Value}");
|
||
Assert.True(acfLag2.Last.Value > acfLag3.Last.Value,
|
||
$"ACF should decay: lag2={acfLag2.Last.Value} should be > lag3={acfLag3.Last.Value}");
|
||
}
|
||
|
||
#endregion
|
||
|
||
#region Different Period Sizes
|
||
|
||
[Theory]
|
||
[InlineData(10)]
|
||
[InlineData(20)]
|
||
[InlineData(50)]
|
||
[InlineData(100)]
|
||
public void Validation_DifferentPeriods_ConsistentResults(int period)
|
||
{
|
||
const int lag = 1;
|
||
var gbm = new GBM(seed: 42);
|
||
var bars = gbm.Fetch(500, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1));
|
||
|
||
var acf = new Acf(period, lag);
|
||
foreach (var bar in bars)
|
||
{
|
||
acf.Update(new TValue(bar.Time, bar.Close));
|
||
}
|
||
|
||
Assert.True(acf.IsHot);
|
||
Assert.True(double.IsFinite(acf.Last.Value));
|
||
Assert.True(acf.Last.Value >= -1.0 && acf.Last.Value <= 1.0);
|
||
}
|
||
|
||
#endregion
|
||
}
|