mirror of
https://github.com/mihakralj/QuanTAlib.git
synced 2026-08-23 04:58:08 +00:00
docs: remove C# Implementation Considerations sections, clean up temp scripts, reorganize test files
- 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
This commit is contained in:
@@ -0,0 +1,578 @@
|
||||
using Xunit;
|
||||
|
||||
namespace QuanTAlib.Tests;
|
||||
|
||||
public class PacfTests
|
||||
{
|
||||
private const int DefaultPeriod = 20;
|
||||
private const int DefaultLag = 1;
|
||||
private const double Epsilon = 1e-10;
|
||||
|
||||
#region Constructor Validation
|
||||
|
||||
[Fact]
|
||||
public void Constructor_LagLessThanOne_ThrowsArgumentOutOfRangeException()
|
||||
{
|
||||
var ex = Assert.Throws<ArgumentOutOfRangeException>(() => new Pacf(10, 0));
|
||||
Assert.Equal("lag", ex.ParamName);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Constructor_PeriodNotGreaterThanLagPlusOne_ThrowsArgumentOutOfRangeException()
|
||||
{
|
||||
// Period must be > lag + 1, so period=3 with lag=2 is invalid (3 <= 2+1)
|
||||
var ex = Assert.Throws<ArgumentOutOfRangeException>(() => new Pacf(3, 2));
|
||||
Assert.Equal("period", ex.ParamName);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Constructor_ValidParameters_CreatesIndicator()
|
||||
{
|
||||
var pacf = new Pacf(10, 2);
|
||||
Assert.Equal("Pacf(10,2)", pacf.Name);
|
||||
Assert.Equal(10, pacf.WarmupPeriod);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Constructor_DefaultLag_IsOne()
|
||||
{
|
||||
var pacf = new Pacf(10);
|
||||
Assert.Equal("Pacf(10,1)", pacf.Name);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Constructor_NullSource_ThrowsArgumentNullException()
|
||||
{
|
||||
Assert.Throws<ArgumentNullException>(() => new Pacf(null!, 10, 1));
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Basic Calculation
|
||||
|
||||
[Fact]
|
||||
public void Update_ReturnsTValue()
|
||||
{
|
||||
var pacf = new Pacf(DefaultPeriod, DefaultLag);
|
||||
var input = new TValue(DateTime.UtcNow, 100.0);
|
||||
TValue result = pacf.Update(input);
|
||||
Assert.True(result.Time != default);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Update_LastPropertyUpdated()
|
||||
{
|
||||
var pacf = new Pacf(DefaultPeriod, DefaultLag);
|
||||
var input = new TValue(DateTime.UtcNow, 100.0);
|
||||
pacf.Update(input);
|
||||
Assert.Equal(input.Time, pacf.Last.Time);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Update_ConstantSeries_ReturnsZero()
|
||||
{
|
||||
// PACF of a constant series (after warmup) should be 0 because variance = 0
|
||||
var pacf = new Pacf(10, 1);
|
||||
for (int i = 0; i < 20; i++)
|
||||
{
|
||||
pacf.Update(new TValue(DateTime.UtcNow.AddSeconds(i), 50.0));
|
||||
}
|
||||
Assert.Equal(0, pacf.Last.Value);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Update_RandomWalk_PacfDecaysWithLag()
|
||||
{
|
||||
// For random data, higher lags typically have lower PACF
|
||||
var pacfLag1 = new Pacf(100, 1);
|
||||
var pacfLag10 = new Pacf(100, 10);
|
||||
|
||||
var gbm = new GBM(seed: 42);
|
||||
var bars = gbm.Fetch(200, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1));
|
||||
|
||||
foreach (var bar in bars)
|
||||
{
|
||||
pacfLag1.Update(new TValue(bar.Time, bar.Close));
|
||||
pacfLag10.Update(new TValue(bar.Time, bar.Close));
|
||||
}
|
||||
|
||||
Assert.True(pacfLag1.IsHot);
|
||||
Assert.True(pacfLag10.IsHot);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Update_PacfBoundedBetweenMinusOneAndOne()
|
||||
{
|
||||
var pacf = new Pacf(20, 1);
|
||||
var gbm = new GBM(seed: 123);
|
||||
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.Last.Value >= -1.0 && pacf.Last.Value <= 1.0,
|
||||
$"PACF value {pacf.Last.Value} out of bounds");
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region IsNew Parameter (Bar Correction)
|
||||
|
||||
[Fact]
|
||||
public void Update_IsNewTrue_AdvancesState()
|
||||
{
|
||||
var pacf = new Pacf(10, 1);
|
||||
|
||||
// Feed initial values
|
||||
for (int i = 0; i < 15; i++)
|
||||
{
|
||||
pacf.Update(new TValue(DateTime.UtcNow.AddSeconds(i), 100 + i));
|
||||
}
|
||||
|
||||
double valueBeforeNew = pacf.Last.Value;
|
||||
|
||||
// Update with isNew=true advances state
|
||||
pacf.Update(new TValue(DateTime.UtcNow.AddSeconds(15), 200), isNew: true);
|
||||
double valueAfterNew = pacf.Last.Value;
|
||||
|
||||
// Value should change since we added a different value
|
||||
Assert.NotEqual(valueBeforeNew, valueAfterNew);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Update_IsNewFalse_DoesNotAdvanceState()
|
||||
{
|
||||
var pacf = new Pacf(10, 1);
|
||||
|
||||
// Feed initial values
|
||||
for (int i = 0; i < 15; i++)
|
||||
{
|
||||
pacf.Update(new TValue(DateTime.UtcNow.AddSeconds(i), 100 + i));
|
||||
}
|
||||
|
||||
// Update with isNew=true first time
|
||||
pacf.Update(new TValue(DateTime.UtcNow.AddSeconds(15), 150), isNew: true);
|
||||
double valueAfterFirstUpdate = pacf.Last.Value;
|
||||
|
||||
// Update same bar with different value, isNew=false
|
||||
pacf.Update(new TValue(DateTime.UtcNow.AddSeconds(15), 160), isNew: false);
|
||||
|
||||
// Another correction back to original
|
||||
pacf.Update(new TValue(DateTime.UtcNow.AddSeconds(15), 150), isNew: false);
|
||||
double valueAfterSecondCorrection = pacf.Last.Value;
|
||||
|
||||
// Should restore to original value when corrected back
|
||||
Assert.Equal(valueAfterFirstUpdate, valueAfterSecondCorrection, Epsilon);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Update_IterativeCorrections_RestoresCorrectState()
|
||||
{
|
||||
var pacf = new Pacf(10, 1);
|
||||
|
||||
// Feed initial values
|
||||
for (int i = 0; i < 15; i++)
|
||||
{
|
||||
pacf.Update(new TValue(DateTime.UtcNow.AddSeconds(i), 100 + i));
|
||||
}
|
||||
|
||||
// Make multiple corrections
|
||||
pacf.Update(new TValue(DateTime.UtcNow.AddSeconds(15), 200), isNew: true);
|
||||
double afterNew = pacf.Last.Value;
|
||||
|
||||
pacf.Update(new TValue(DateTime.UtcNow.AddSeconds(15), 250), isNew: false);
|
||||
pacf.Update(new TValue(DateTime.UtcNow.AddSeconds(15), 300), isNew: false);
|
||||
pacf.Update(new TValue(DateTime.UtcNow.AddSeconds(15), 200), isNew: false);
|
||||
|
||||
// Should match the value after the first isNew=true update with 200
|
||||
Assert.Equal(afterNew, pacf.Last.Value, Epsilon);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Warmup and IsHot
|
||||
|
||||
[Fact]
|
||||
public void IsHot_FalseBeforeWarmup()
|
||||
{
|
||||
var pacf = new Pacf(20, 1);
|
||||
|
||||
for (int i = 0; i < 19; i++)
|
||||
{
|
||||
pacf.Update(new TValue(DateTime.UtcNow.AddSeconds(i), 100 + i));
|
||||
Assert.False(pacf.IsHot);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void IsHot_TrueAfterWarmup()
|
||||
{
|
||||
var pacf = new Pacf(20, 1);
|
||||
|
||||
for (int i = 0; i < 20; i++)
|
||||
{
|
||||
pacf.Update(new TValue(DateTime.UtcNow.AddSeconds(i), 100 + i));
|
||||
}
|
||||
|
||||
Assert.True(pacf.IsHot);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void WarmupPeriod_MatchesPeriod()
|
||||
{
|
||||
var pacf = new Pacf(25, 3);
|
||||
Assert.Equal(25, pacf.WarmupPeriod);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region NaN and Infinity Handling
|
||||
|
||||
[Fact]
|
||||
public void Update_NaNInput_UsesLastValidValue()
|
||||
{
|
||||
var pacf = new Pacf(10, 1);
|
||||
|
||||
// Feed valid values
|
||||
for (int i = 0; i < 15; i++)
|
||||
{
|
||||
pacf.Update(new TValue(DateTime.UtcNow.AddSeconds(i), 100 + i));
|
||||
}
|
||||
|
||||
// Feed NaN
|
||||
pacf.Update(new TValue(DateTime.UtcNow.AddSeconds(15), double.NaN));
|
||||
|
||||
// Result should still be finite
|
||||
Assert.True(double.IsFinite(pacf.Last.Value));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Update_InfinityInput_UsesLastValidValue()
|
||||
{
|
||||
var pacf = new Pacf(10, 1);
|
||||
|
||||
// Feed valid values
|
||||
for (int i = 0; i < 15; i++)
|
||||
{
|
||||
pacf.Update(new TValue(DateTime.UtcNow.AddSeconds(i), 100 + i));
|
||||
}
|
||||
|
||||
// Feed infinity
|
||||
pacf.Update(new TValue(DateTime.UtcNow.AddSeconds(15), double.PositiveInfinity));
|
||||
|
||||
// Result should still be finite
|
||||
Assert.True(double.IsFinite(pacf.Last.Value));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Update_MultipleNaNs_StillProducesFiniteResult()
|
||||
{
|
||||
var pacf = new Pacf(10, 1);
|
||||
|
||||
// Feed valid values
|
||||
for (int i = 0; i < 15; i++)
|
||||
{
|
||||
pacf.Update(new TValue(DateTime.UtcNow.AddSeconds(i), 100 + i));
|
||||
}
|
||||
|
||||
// Feed multiple NaNs
|
||||
for (int i = 0; i < 5; i++)
|
||||
{
|
||||
pacf.Update(new TValue(DateTime.UtcNow.AddSeconds(15 + i), double.NaN));
|
||||
Assert.True(double.IsFinite(pacf.Last.Value));
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Reset
|
||||
|
||||
[Fact]
|
||||
public void Reset_ClearsState()
|
||||
{
|
||||
var pacf = new Pacf(10, 1);
|
||||
|
||||
// Feed values
|
||||
for (int i = 0; i < 15; i++)
|
||||
{
|
||||
pacf.Update(new TValue(DateTime.UtcNow.AddSeconds(i), 100 + i));
|
||||
}
|
||||
|
||||
Assert.True(pacf.IsHot);
|
||||
|
||||
pacf.Reset();
|
||||
|
||||
Assert.False(pacf.IsHot);
|
||||
Assert.Equal(default, pacf.Last);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Reset_AllowsReinitializationWithSameData()
|
||||
{
|
||||
var pacf = new Pacf(10, 1);
|
||||
var inputs = new List<TValue>();
|
||||
|
||||
// Generate and store values
|
||||
for (int i = 0; i < 20; i++)
|
||||
{
|
||||
inputs.Add(new TValue(DateTime.UtcNow.AddSeconds(i), 100 + i * 0.5));
|
||||
}
|
||||
|
||||
// First pass
|
||||
foreach (var input in inputs)
|
||||
{
|
||||
pacf.Update(input);
|
||||
}
|
||||
|
||||
double firstPassResult = pacf.Last.Value;
|
||||
|
||||
// Reset and second pass
|
||||
pacf.Reset();
|
||||
foreach (var input in inputs)
|
||||
{
|
||||
pacf.Update(input);
|
||||
}
|
||||
|
||||
double secondPassResult = pacf.Last.Value;
|
||||
|
||||
Assert.Equal(firstPassResult, secondPassResult, Epsilon);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Prime
|
||||
|
||||
[Fact]
|
||||
public void Prime_InitializesStateCorrectly()
|
||||
{
|
||||
var pacf1 = new Pacf(10, 1);
|
||||
var pacf2 = new Pacf(10, 1);
|
||||
|
||||
double[] primeData = [100, 101, 102, 103, 104, 105, 106, 107, 108, 109, 110];
|
||||
|
||||
// Method 1: Use Prime
|
||||
pacf1.Prime(primeData);
|
||||
|
||||
// Method 2: Update individually
|
||||
foreach (double val in primeData)
|
||||
{
|
||||
pacf2.Update(new TValue(DateTime.UtcNow, val));
|
||||
}
|
||||
|
||||
Assert.Equal(pacf2.Last.Value, pacf1.Last.Value, Epsilon);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Event Chaining
|
||||
|
||||
[Fact]
|
||||
public void ChainedConstructor_ReceivesUpdates()
|
||||
{
|
||||
var source = new TSeries();
|
||||
var pacf = new Pacf(source, 10, 1);
|
||||
|
||||
// Feed values through source
|
||||
for (int i = 0; i < 15; i++)
|
||||
{
|
||||
source.Add(new TValue(DateTime.UtcNow.AddSeconds(i), 100 + i));
|
||||
}
|
||||
|
||||
Assert.True(pacf.IsHot);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region AllModes Consistency (Batch vs Streaming vs Static)
|
||||
|
||||
[Fact]
|
||||
public void AllModes_ProduceSameResult()
|
||||
{
|
||||
const int period = 14;
|
||||
const int lag = 1;
|
||||
const int dataLen = 100;
|
||||
const int compareLen = 50;
|
||||
|
||||
var gbm = new GBM(seed: 42);
|
||||
var bars = gbm.Fetch(dataLen, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1));
|
||||
var tSeries = new TSeries();
|
||||
foreach (var bar in bars)
|
||||
{
|
||||
tSeries.Add(new TValue(bar.Time, bar.Close));
|
||||
}
|
||||
|
||||
// Mode 1: Streaming (Update one at a time)
|
||||
var streaming = new Pacf(period, lag);
|
||||
foreach (var tv in tSeries)
|
||||
{
|
||||
streaming.Update(tv);
|
||||
}
|
||||
|
||||
// Mode 2: Batch via Update(TSeries)
|
||||
var batchIndicator = new Pacf(period, lag);
|
||||
var batchResult = batchIndicator.Update(tSeries);
|
||||
|
||||
// Mode 3: Static Calculate
|
||||
var staticResult = Pacf.Batch(tSeries, period, lag);
|
||||
|
||||
// Mode 4: Span-based Batch
|
||||
double[] sourceArray = new double[dataLen];
|
||||
double[] spanResult = new double[dataLen];
|
||||
for (int i = 0; i < dataLen; i++)
|
||||
{
|
||||
sourceArray[i] = tSeries[i].Value;
|
||||
}
|
||||
|
||||
Pacf.Batch(sourceArray, spanResult, period, lag);
|
||||
|
||||
// Compare last 'compareLen' values (after warmup settles)
|
||||
int startIdx = dataLen - compareLen;
|
||||
for (int i = startIdx; i < dataLen; i++)
|
||||
{
|
||||
double batchVal = batchResult[i].Value;
|
||||
double staticVal = staticResult[i].Value;
|
||||
double spanVal = spanResult[i];
|
||||
|
||||
// Batch and static should match exactly
|
||||
Assert.Equal(batchVal, staticVal, Epsilon);
|
||||
|
||||
// Span should match batch
|
||||
Assert.Equal(batchVal, spanVal, Epsilon);
|
||||
}
|
||||
|
||||
// Streaming last should match batch last
|
||||
Assert.Equal(batchResult[^1].Value, streaming.Last.Value, Epsilon);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Span Batch Validation
|
||||
|
||||
[Fact]
|
||||
public void Batch_MismatchedLengths_ThrowsArgumentException()
|
||||
{
|
||||
double[] source = new double[100];
|
||||
double[] output = new double[50];
|
||||
|
||||
var ex = Assert.Throws<ArgumentException>(() => Pacf.Batch(source, output, 10, 1));
|
||||
Assert.Equal("output", ex.ParamName);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Batch_InvalidLag_ThrowsArgumentOutOfRangeException()
|
||||
{
|
||||
double[] source = new double[100];
|
||||
double[] output = new double[100];
|
||||
|
||||
var ex = Assert.Throws<ArgumentOutOfRangeException>(() => Pacf.Batch(source, output, 10, 0));
|
||||
Assert.Equal("lag", ex.ParamName);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Batch_InvalidPeriod_ThrowsArgumentOutOfRangeException()
|
||||
{
|
||||
double[] source = new double[100];
|
||||
double[] output = new double[100];
|
||||
|
||||
var ex = Assert.Throws<ArgumentOutOfRangeException>(() => Pacf.Batch(source, output, 3, 2));
|
||||
Assert.Equal("period", ex.ParamName);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Batch_EmptyInput_ReturnsEmpty()
|
||||
{
|
||||
double[] source = [];
|
||||
double[] output = [];
|
||||
|
||||
// Should not throw
|
||||
Pacf.Batch(source, output, 10, 1);
|
||||
|
||||
// Verify output is empty as expected
|
||||
Assert.Empty(output);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Batch_ResultsWithinBounds()
|
||||
{
|
||||
var gbm = new GBM(seed: 42);
|
||||
var bars = gbm.Fetch(200, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1));
|
||||
|
||||
double[] source = bars.Select(b => b.Close).ToArray();
|
||||
double[] output = new double[200];
|
||||
|
||||
Pacf.Batch(source, output, 20, 1);
|
||||
|
||||
foreach (double val in output)
|
||||
{
|
||||
Assert.True(val >= -1.0 && val <= 1.0,
|
||||
$"PACF value {val} out of bounds [-1, 1]");
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region PACF-Specific Tests
|
||||
|
||||
[Fact]
|
||||
public void Pacf_Lag1_EqualsAcfLag1()
|
||||
{
|
||||
// For lag 1, PACF equals ACF (by definition φ_11 = r_1)
|
||||
var pacf = new Pacf(DefaultPeriod, 1);
|
||||
var acf = new Acf(DefaultPeriod, 1);
|
||||
|
||||
var gbm = new GBM(seed: 42);
|
||||
var bars = gbm.Fetch(50, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1));
|
||||
|
||||
foreach (var bar in bars)
|
||||
{
|
||||
var tv = new TValue(bar.Time, bar.Close);
|
||||
pacf.Update(tv);
|
||||
acf.Update(tv);
|
||||
}
|
||||
|
||||
// PACF at lag 1 should equal ACF at lag 1
|
||||
Assert.Equal(acf.Last.Value, pacf.Last.Value, 6);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(1)]
|
||||
[InlineData(2)]
|
||||
[InlineData(5)]
|
||||
[InlineData(10)]
|
||||
public void Update_DifferentLags_ProducesResults(int lag)
|
||||
{
|
||||
int period = lag + 10; // Ensure period > lag + 1
|
||||
var pacf = new Pacf(period, lag);
|
||||
|
||||
var gbm = new GBM(seed: 42);
|
||||
var bars = gbm.Fetch(50, 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);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Event Publication
|
||||
|
||||
[Fact]
|
||||
public void Update_PublishesEvent()
|
||||
{
|
||||
var pacf = new Pacf(DefaultPeriod, DefaultLag);
|
||||
bool eventFired = false;
|
||||
|
||||
pacf.Pub += (object? sender, in TValueEventArgs args) => { eventFired = true; };
|
||||
|
||||
pacf.Update(new TValue(DateTime.UtcNow, 100.0));
|
||||
|
||||
Assert.True(eventFired);
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
@@ -0,0 +1,351 @@
|
||||
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
|
||||
}
|
||||
Reference in New Issue
Block a user