Files
Miha Kralj 060649192f 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
2026-03-12 12:34:16 -07:00

242 lines
7.0 KiB
C#

using Xunit;
using Xunit.Abstractions;
namespace QuanTAlib.Tests;
public sealed class CtiValidationTests : IDisposable
{
private readonly ValidationTestData _testData;
private readonly ITestOutputHelper _output;
private bool _disposed;
public CtiValidationTests(ITestOutputHelper output)
{
_output = output;
_testData = new ValidationTestData();
}
public void Dispose()
{
Dispose(true);
}
private void Dispose(bool disposing)
{
if (_disposed)
{
return;
}
_disposed = true;
if (disposing)
{
_testData?.Dispose();
}
}
[Fact]
public void Validate_Streaming_Batch_Span_Agree()
{
int period = 20;
// Streaming
var streaming = new Cti(period);
var streamValues = new List<double>(_testData.Data.Count);
foreach (var item in _testData.Data)
{
streamValues.Add(streaming.Update(item).Value);
}
// Batch (TSeries)
TSeries batchSeries = Cti.Batch(_testData.Data, period);
// Span
double[] src = _testData.RawData.ToArray();
double[] spanOutput = new double[src.Length];
Cti.Batch(src.AsSpan(), spanOutput.AsSpan(), period);
// Batch and span should be identical (same code path through RingBuffer)
// Streaming uses O(1) incremental updates with ResyncInterval=1000
int start = Math.Max(0, src.Length - 200);
for (int i = start; i < src.Length; i++)
{
Assert.Equal(batchSeries[i].Value, spanOutput[i], 12);
Assert.Equal(batchSeries[i].Value, streamValues[i], 4);
}
_output.WriteLine("CTI validation: streaming, batch, and span outputs agree within tolerance.");
}
[Fact]
public void Validate_PerfectCorrelation_Ascending()
{
// Arithmetic sequence: each element is exactly i+1
// Expected: Pearson r = 1.0 exactly (perfect positive linear correlation)
int period = 15;
var cti = new Cti(period);
double lastValue = 0.0;
for (int i = 1; i <= 50; i++)
{
cti.Update(new TValue(DateTime.UtcNow, i * 1.0));
if (cti.IsHot)
{
lastValue = cti.Last.Value;
}
}
Assert.Equal(1.0, lastValue, 10);
_output.WriteLine($"CTI ascending sequence: {lastValue:F15}");
}
[Fact]
public void Validate_PerfectCorrelation_Descending()
{
// Descending arithmetic sequence → perfect negative correlation → CTI = -1.0
int period = 15;
var cti = new Cti(period);
double lastValue = 0.0;
for (int i = 50; i >= 1; i--)
{
cti.Update(new TValue(DateTime.UtcNow, i * 1.0));
if (cti.IsHot)
{
lastValue = cti.Last.Value;
}
}
Assert.Equal(-1.0, lastValue, 10);
_output.WriteLine($"CTI descending sequence: {lastValue:F15}");
}
[Fact]
public void Validate_ConstantInput_ReturnsZero()
{
// Constant price: variance = 0 → denomY = 0 → return 0
int period = 10;
var cti = new Cti(period);
for (int i = 0; i < 30; i++)
{
cti.Update(new TValue(DateTime.UtcNow, 100.0));
}
Assert.True(cti.IsHot);
Assert.Equal(0.0, cti.Last.Value, 10);
}
[Fact]
public void Validate_Output_AlwaysBounded()
{
// With random GBM data, output must stay in [-1, +1]
var gbm = new GBM(startPrice: 100.0, mu: 0.0, sigma: 0.5, seed: 999);
var bars = gbm.Fetch(2000, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1));
foreach (int period in new[] { 5, 10, 20, 50, 100 })
{
TSeries batch = Cti.Batch(bars.Close, period);
foreach (var tv in batch)
{
Assert.InRange(tv.Value, -1.0, 1.0);
}
}
}
[Fact]
public void Validate_Batch_Calculate_Agree()
{
int period = 14;
var gbm = new GBM(startPrice: 100.0, mu: 0.02, sigma: 0.2, seed: 77);
var bars = gbm.Fetch(200, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1));
TSeries source = bars.Close;
TSeries batchResult = Cti.Batch(source, period);
var (calcResult, _) = Cti.Calculate(source, period);
for (int i = period; i < source.Count; i++)
{
Assert.Equal(batchResult[i].Value, calcResult[i].Value, 10);
}
}
[Fact]
public void Validate_BarCorrection_Consistency()
{
// After bar correction restores original value, result must equal baseline
int period = 10;
var gbm = new GBM(startPrice: 100.0, mu: 0.02, sigma: 0.15, seed: 31);
var bars = gbm.Fetch(50, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1));
TSeries source = bars.Close;
var cti = new Cti(period);
for (int i = 0; i < source.Count - 1; i++)
{
cti.Update(source[i], isNew: true);
}
// Final bar
cti.Update(source[^1], isNew: true);
double baseline = cti.Last.Value;
// Correct and revert
cti.Update(new TValue(source[^1].Time, 99999.0), isNew: false);
cti.Update(new TValue(source[^1].Time, source[^1].Value), isNew: false);
Assert.Equal(baseline, cti.Last.Value, 7);
}
[Fact]
public void Validate_DifferentPeriods_Produce_Different_Results()
{
var gbm = new GBM(startPrice: 100.0, mu: 0.02, sigma: 0.2, seed: 55);
var bars = gbm.Fetch(200, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1));
TSeries source = bars.Close;
TSeries r5 = Cti.Batch(source, 5);
TSeries r20 = Cti.Batch(source, 20);
TSeries r50 = Cti.Batch(source, 50);
// Different periods should generally not produce identical results
double sum5 = 0, sum20 = 0, sum50 = 0;
for (int i = 50; i < source.Count; i++)
{
sum5 += r5[i].Value;
sum20 += r20[i].Value;
sum50 += r50[i].Value;
}
// Sums at different periods should differ
Assert.NotEqual(sum5, sum20);
Assert.NotEqual(sum20, sum50);
}
[Fact]
public void Validate_Reset_Reprocess_Deterministic()
{
int period = 15;
var gbm = new GBM(startPrice: 100.0, mu: 0.02, sigma: 0.2, seed: 13);
var bars = gbm.Fetch(100, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1));
TSeries source = bars.Close;
var cti = new Cti(period);
double[] first = new double[source.Count];
for (int i = 0; i < source.Count; i++)
{
first[i] = cti.Update(source[i]).Value;
}
cti.Reset();
double[] second = new double[source.Count];
for (int i = 0; i < source.Count; i++)
{
second[i] = cti.Update(source[i]).Value;
}
for (int i = 0; i < source.Count; i++)
{
Assert.Equal(first[i], second[i], 15);
}
}
}