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
352 lines
11 KiB
C#
352 lines
11 KiB
C#
using Xunit;
|
|
|
|
namespace QuanTAlib.Tests;
|
|
|
|
/// <summary>
|
|
/// Validation tests for PACF (Partial Autocorrelation Function).
|
|
/// PACF is not commonly implemented in standard trading libraries (TA-Lib, Skender, Tulip, Ooples),
|
|
/// so validation is performed against mathematical properties and theoretical expectations.
|
|
/// </summary>
|
|
public class PacfValidationTests
|
|
{
|
|
private const double Epsilon = 1e-6;
|
|
|
|
#region Mathematical Property Validation
|
|
|
|
[Fact]
|
|
public void Pacf_OutputBoundedBetweenMinusOneAndOne()
|
|
{
|
|
// PACF must always be in range [-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 pacf = new Pacf(50, lag);
|
|
foreach (var bar in bars)
|
|
{
|
|
pacf.Update(new TValue(bar.Time, bar.Close));
|
|
Assert.True(pacf.Last.Value >= -1.0 && pacf.Last.Value <= 1.0,
|
|
$"PACF at lag {lag} must be in [-1, 1], got {pacf.Last.Value}");
|
|
}
|
|
}
|
|
}
|
|
|
|
[Fact]
|
|
public void Pacf_ConstantSeries_ReturnsZero()
|
|
{
|
|
// A constant series has zero variance, hence PACF = 0
|
|
var pacf = new Pacf(20, 1);
|
|
|
|
for (int i = 0; i < 50; i++)
|
|
{
|
|
pacf.Update(new TValue(DateTime.UtcNow.AddSeconds(i), 100.0));
|
|
}
|
|
|
|
Assert.Equal(0, pacf.Last.Value, Epsilon);
|
|
}
|
|
|
|
[Fact]
|
|
public void Pacf_Lag1_EqualsAcf_Lag1()
|
|
{
|
|
// By definition, φ_11 = r_1 (PACF at lag 1 equals ACF at lag 1)
|
|
var gbm = new GBM(seed: 42);
|
|
var bars = gbm.Fetch(200, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1));
|
|
|
|
var pacf = new Pacf(50, 1);
|
|
var acf = new Acf(50, 1);
|
|
|
|
foreach (var bar in bars)
|
|
{
|
|
var tv = new TValue(bar.Time, bar.Close);
|
|
pacf.Update(tv);
|
|
acf.Update(tv);
|
|
}
|
|
|
|
Assert.Equal(acf.Last.Value, pacf.Last.Value, 6);
|
|
}
|
|
|
|
[Fact]
|
|
public void Pacf_WhiteNoise_CloseToZeroForAllLags()
|
|
{
|
|
// For white noise (iid), all PACF values should be statistically close to zero
|
|
// Using returns which are approximately white noise
|
|
var gbm = new GBM(mu: 0.0, sigma: 0.1, seed: 42);
|
|
var bars = gbm.Fetch(1000, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1));
|
|
|
|
// Calculate returns
|
|
var returns = new List<double>();
|
|
for (int i = 1; i < bars.Count; i++)
|
|
{
|
|
returns.Add(Math.Log(bars[i].Close / bars[i - 1].Close));
|
|
}
|
|
|
|
// PACF of returns should be near zero (95% confidence: ±1.96/√n ≈ 0.062 for n=999)
|
|
// We use a wider tolerance (0.2) since this is stochastic
|
|
|
|
for (int lag = 1; lag <= 5; lag++)
|
|
{
|
|
var pacf = new Pacf(100, lag);
|
|
foreach (double ret in returns)
|
|
{
|
|
pacf.Update(new TValue(DateTime.UtcNow, ret));
|
|
}
|
|
|
|
// Most PACF values should be within confidence bounds
|
|
// We use a wider tolerance since this is stochastic
|
|
Assert.True(Math.Abs(pacf.Last.Value) < 0.2,
|
|
$"PACF at lag {lag} for white noise should be near zero, got {pacf.Last.Value}");
|
|
}
|
|
}
|
|
|
|
[Fact]
|
|
public void Pacf_AR1Process_CutoffAfterLag1()
|
|
{
|
|
// For AR(1) process: x_t = φ*x_{t-1} + ε_t
|
|
// PACF should be significant at lag 1 and cut off (near zero) after
|
|
double phi = 0.7; // AR(1) coefficient
|
|
|
|
// 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(501, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1));
|
|
var arProcess = new List<double> { 0.0 };
|
|
|
|
// Generate AR(1) process using incremental log-returns as white noise ε
|
|
for (int i = 1; i < 500; i++)
|
|
{
|
|
double noise = Math.Log(bars[i].Close / bars[i - 1].Close); // i.i.d. incremental return
|
|
double newValue = phi * arProcess[^1] + noise;
|
|
arProcess.Add(newValue);
|
|
}
|
|
|
|
// PACF at lag 1 should be close to phi
|
|
var pacf1 = new Pacf(100, 1);
|
|
foreach (double val in arProcess)
|
|
{
|
|
pacf1.Update(new TValue(DateTime.UtcNow, val));
|
|
}
|
|
|
|
// PACF at lag 1 should approximate the AR coefficient
|
|
Assert.True(Math.Abs(pacf1.Last.Value - phi) < 0.15,
|
|
$"PACF at lag 1 for AR(1) with φ={phi} should be near {phi}, got {pacf1.Last.Value}");
|
|
|
|
// PACF at higher lags should be smaller (cutoff behavior)
|
|
var pacf2 = new Pacf(100, 2);
|
|
var pacf3 = new Pacf(100, 3);
|
|
|
|
foreach (double val in arProcess)
|
|
{
|
|
pacf2.Update(new TValue(DateTime.UtcNow, val));
|
|
pacf3.Update(new TValue(DateTime.UtcNow, val));
|
|
}
|
|
|
|
Assert.True(Math.Abs(pacf2.Last.Value) < Math.Abs(pacf1.Last.Value),
|
|
$"PACF at lag 2 ({pacf2.Last.Value}) should be smaller than lag 1 ({pacf1.Last.Value})");
|
|
}
|
|
|
|
[Fact]
|
|
public void Pacf_DeterministicTrend_HighPositiveAtLag1()
|
|
{
|
|
// A deterministic trend shows high persistence
|
|
var pacf = new Pacf(30, 1);
|
|
|
|
for (int i = 0; i < 100; i++)
|
|
{
|
|
pacf.Update(new TValue(DateTime.UtcNow.AddSeconds(i), 100.0 + i * 0.5));
|
|
}
|
|
|
|
// Trending series should have high positive PACF at lag 1
|
|
Assert.True(pacf.Last.Value > 0.5,
|
|
$"PACF at lag 1 for trending series should be high positive, got {pacf.Last.Value}");
|
|
}
|
|
|
|
[Fact]
|
|
public void Pacf_AlternatingPattern_NegativeAtLag1()
|
|
{
|
|
// An alternating pattern should show negative PACF at lag 1
|
|
var pacf = new Pacf(30, 1);
|
|
|
|
for (int i = 0; i < 100; i++)
|
|
{
|
|
double value = (i % 2 == 0) ? 100.0 : 105.0;
|
|
pacf.Update(new TValue(DateTime.UtcNow.AddSeconds(i), value));
|
|
}
|
|
|
|
// Alternating series should have negative PACF at lag 1
|
|
Assert.True(pacf.Last.Value < -0.5,
|
|
$"PACF at lag 1 for alternating series should be negative, got {pacf.Last.Value}");
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region Batch vs Streaming Consistency
|
|
|
|
[Fact]
|
|
public void Pacf_BatchMatchesStreaming()
|
|
{
|
|
var gbm = new GBM(seed: 42);
|
|
var bars = gbm.Fetch(200, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1));
|
|
int period = 30;
|
|
int lag = 2;
|
|
|
|
// Create TSeries from bars
|
|
var tSeries = new TSeries();
|
|
foreach (var bar in bars)
|
|
{
|
|
tSeries.Add(new TValue(bar.Time, bar.Close));
|
|
}
|
|
|
|
// Streaming
|
|
var streaming = new Pacf(period, lag);
|
|
foreach (var tv in tSeries)
|
|
{
|
|
streaming.Update(tv);
|
|
}
|
|
|
|
// Batch
|
|
var batchResult = Pacf.Batch(tSeries, period, lag);
|
|
|
|
// Compare last values
|
|
Assert.Equal(batchResult[^1].Value, streaming.Last.Value, Epsilon);
|
|
}
|
|
|
|
[Fact]
|
|
public void Pacf_SpanMatchesTSeries()
|
|
{
|
|
var gbm = new GBM(seed: 42);
|
|
var bars = gbm.Fetch(200, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1));
|
|
int period = 30;
|
|
int lag = 3;
|
|
|
|
// Create arrays
|
|
double[] source = bars.Select(b => b.Close).ToArray();
|
|
double[] spanOutput = new double[source.Length];
|
|
|
|
// Span calculation
|
|
Pacf.Batch(source, spanOutput, period, lag);
|
|
|
|
// TSeries calculation
|
|
var tSeries = new TSeries();
|
|
foreach (var bar in bars)
|
|
{
|
|
tSeries.Add(new TValue(bar.Time, bar.Close));
|
|
}
|
|
var tSeriesResult = Pacf.Batch(tSeries, period, lag);
|
|
|
|
// Compare last 50 values
|
|
for (int i = source.Length - 50; i < source.Length; i++)
|
|
{
|
|
Assert.Equal(tSeriesResult[i].Value, spanOutput[i], Epsilon);
|
|
}
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region Edge Cases
|
|
|
|
[Fact]
|
|
public void Pacf_MinimumValidPeriod_Works()
|
|
{
|
|
// Period must be > lag + 1, so period=4 with lag=2 is minimum valid
|
|
var pacf = new Pacf(4, 2);
|
|
|
|
for (int i = 0; i < 10; i++)
|
|
{
|
|
pacf.Update(new TValue(DateTime.UtcNow.AddSeconds(i), 100 + i));
|
|
}
|
|
|
|
Assert.True(pacf.IsHot);
|
|
Assert.True(double.IsFinite(pacf.Last.Value));
|
|
}
|
|
|
|
[Fact]
|
|
public void Pacf_HighLag_Works()
|
|
{
|
|
// Test with high lag value
|
|
int lag = 20;
|
|
int period = 50;
|
|
var pacf = new Pacf(period, lag);
|
|
|
|
var gbm = new GBM(seed: 42);
|
|
var bars = gbm.Fetch(100, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1));
|
|
|
|
foreach (var bar in bars)
|
|
{
|
|
pacf.Update(new TValue(bar.Time, bar.Close));
|
|
}
|
|
|
|
Assert.True(pacf.IsHot);
|
|
Assert.True(pacf.Last.Value >= -1.0 && pacf.Last.Value <= 1.0);
|
|
}
|
|
|
|
[Fact]
|
|
public void Pacf_NaNHandling_ProducesFiniteOutput()
|
|
{
|
|
var pacf = new Pacf(20, 1);
|
|
|
|
// Feed some valid values
|
|
for (int i = 0; i < 30; i++)
|
|
{
|
|
pacf.Update(new TValue(DateTime.UtcNow.AddSeconds(i), 100 + i));
|
|
}
|
|
|
|
// Inject NaN
|
|
pacf.Update(new TValue(DateTime.UtcNow.AddSeconds(30), double.NaN));
|
|
|
|
Assert.True(double.IsFinite(pacf.Last.Value));
|
|
}
|
|
|
|
[Fact]
|
|
public void Pacf_InfinityHandling_ProducesFiniteOutput()
|
|
{
|
|
var pacf = new Pacf(20, 1);
|
|
|
|
// Feed some valid values
|
|
for (int i = 0; i < 30; i++)
|
|
{
|
|
pacf.Update(new TValue(DateTime.UtcNow.AddSeconds(i), 100 + i));
|
|
}
|
|
|
|
// Inject infinity
|
|
pacf.Update(new TValue(DateTime.UtcNow.AddSeconds(30), double.PositiveInfinity));
|
|
|
|
Assert.True(double.IsFinite(pacf.Last.Value));
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region Durbin-Levinson Recursion Verification
|
|
|
|
[Fact]
|
|
public void Pacf_DurbinLevinsonRecursion_ProducesCorrectResults()
|
|
{
|
|
// Verify that the Durbin-Levinson recursion produces mathematically valid results
|
|
// by checking that the result is bounded and consistent across multiple runs
|
|
|
|
var gbm = new GBM(seed: 42);
|
|
var bars = gbm.Fetch(200, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1));
|
|
|
|
var results = new List<double>();
|
|
for (int run = 0; run < 3; run++)
|
|
{
|
|
var pacf = new Pacf(50, 5);
|
|
foreach (var bar in bars)
|
|
{
|
|
pacf.Update(new TValue(bar.Time, bar.Close));
|
|
}
|
|
results.Add(pacf.Last.Value);
|
|
}
|
|
|
|
// All runs should produce the same result (deterministic)
|
|
for (int i = 1; i < results.Count; i++)
|
|
{
|
|
Assert.Equal(results[0], results[i], Epsilon);
|
|
}
|
|
|
|
// Result should be bounded
|
|
Assert.True(results[0] >= -1.0 && results[0] <= 1.0);
|
|
}
|
|
|
|
#endregion
|
|
}
|