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
579 lines
15 KiB
C#
579 lines
15 KiB
C#
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
|
|
}
|