mirror of
https://github.com/mihakralj/QuanTAlib.git
synced 2026-07-27 17:27:43 +00:00
1910fdca93
- Remove global.json (SDK pinning unnecessary) - Remove nuget.config, move MyGet source to .csproj RestoreAdditionalProjectSources - Gitignore ndepend/ entirely, move badges to docs/img/ - Update README.md and docs/ndepend.md badge paths - Add NDepend project property to QuanTAlib.slnx - Expand .editorconfig ReSharper/diagnostic suppressions - Use ArgumentOutOfRangeException instead of ArgumentException - Use discard _ for unused event sender parameters - Remove quantalib.code-workspace and sonar-suppressions.json - Add filter signature SVGs
511 lines
17 KiB
C#
511 lines
17 KiB
C#
using Xunit;
|
|
|
|
namespace QuanTAlib.Tests;
|
|
|
|
public class CcycTests
|
|
{
|
|
private const long StartTime = 946_684_800_000_000_0L; // 2000-01-01 UTC in ticks
|
|
private static readonly TimeSpan Step = TimeSpan.FromMinutes(1);
|
|
private static readonly GBM TestData = new(startPrice: 100, mu: 0.05, sigma: 0.5, seed: 42);
|
|
|
|
private static TSeries GetTestSeries(int count = 500)
|
|
{
|
|
return TestData.Fetch(count, StartTime, Step).Close;
|
|
}
|
|
|
|
// ═══════════════════════════════════════════════════════════════════
|
|
// A) Constructor Defaults
|
|
// ═══════════════════════════════════════════════════════════════════
|
|
|
|
[Fact]
|
|
public void Ccyc_DefaultAlpha_NoThrow()
|
|
{
|
|
var ccyc = new Ccyc();
|
|
Assert.NotNull(ccyc);
|
|
Assert.Equal(7, ccyc.WarmupPeriod);
|
|
}
|
|
|
|
[Fact]
|
|
public void Ccyc_CustomAlpha_NoThrow()
|
|
{
|
|
var ccyc = new Ccyc(alpha: 0.15);
|
|
Assert.NotNull(ccyc);
|
|
}
|
|
|
|
[Fact]
|
|
public void Ccyc_AlphaZero_Throws()
|
|
{
|
|
Assert.Throws<ArgumentException>(() => new Ccyc(alpha: 0.0));
|
|
}
|
|
|
|
[Fact]
|
|
public void Ccyc_AlphaOne_Throws()
|
|
{
|
|
Assert.Throws<ArgumentException>(() => new Ccyc(alpha: 1.0));
|
|
}
|
|
|
|
[Fact]
|
|
public void Ccyc_AlphaNegative_Throws()
|
|
{
|
|
Assert.Throws<ArgumentException>(() => new Ccyc(alpha: -0.1));
|
|
}
|
|
|
|
[Fact]
|
|
public void Ccyc_AlphaAboveOne_Throws()
|
|
{
|
|
Assert.Throws<ArgumentException>(() => new Ccyc(alpha: 1.5));
|
|
}
|
|
|
|
[Fact]
|
|
public void Ccyc_Name_ContainsAlpha()
|
|
{
|
|
var ccyc = new Ccyc(0.07);
|
|
Assert.Contains("0.07", ccyc.Name, StringComparison.Ordinal);
|
|
}
|
|
|
|
[Fact]
|
|
public void Ccyc_WarmupPeriod_IsSeven()
|
|
{
|
|
var ccyc = new Ccyc();
|
|
Assert.Equal(7, ccyc.WarmupPeriod);
|
|
}
|
|
|
|
// ═══════════════════════════════════════════════════════════════════
|
|
// B) Basic Calculation
|
|
// ═══════════════════════════════════════════════════════════════════
|
|
|
|
[Fact]
|
|
public void Ccyc_SingleValue_ReturnsFinite()
|
|
{
|
|
var ccyc = new Ccyc();
|
|
var result = ccyc.Update(new TValue(DateTime.UtcNow, 100));
|
|
Assert.True(double.IsFinite(result.Value));
|
|
}
|
|
|
|
[Fact]
|
|
public void Ccyc_MultipleValues_AllFinite()
|
|
{
|
|
var ccyc = new Ccyc();
|
|
var source = GetTestSeries();
|
|
var results = ccyc.Update(source);
|
|
for (int i = 0; i < results.Count; i++)
|
|
{
|
|
Assert.True(double.IsFinite(results[i].Value), $"Non-finite at index {i}");
|
|
}
|
|
}
|
|
|
|
[Fact]
|
|
public void Ccyc_OutputNotZeroWhenHot()
|
|
{
|
|
var ccyc = new Ccyc();
|
|
var source = GetTestSeries(200);
|
|
var results = ccyc.Update(source);
|
|
|
|
// After warmup, at least some values should be non-zero
|
|
bool anyNonZero = false;
|
|
for (int i = ccyc.WarmupPeriod; i < results.Count; i++)
|
|
{
|
|
if (Math.Abs(results[i].Value) > 1e-10)
|
|
{
|
|
anyNonZero = true;
|
|
break;
|
|
}
|
|
}
|
|
Assert.True(anyNonZero, "All post-warmup values are zero");
|
|
}
|
|
|
|
[Fact]
|
|
public void Ccyc_IsOscillator_ChangesSigns()
|
|
{
|
|
var ccyc = new Ccyc();
|
|
var source = GetTestSeries(200);
|
|
var results = ccyc.Update(source);
|
|
|
|
bool hasPositive = false;
|
|
bool hasNegative = false;
|
|
for (int i = ccyc.WarmupPeriod; i < results.Count; i++)
|
|
{
|
|
if (results[i].Value > 0)
|
|
{
|
|
hasPositive = true;
|
|
}
|
|
|
|
if (results[i].Value < 0)
|
|
{
|
|
hasNegative = true;
|
|
}
|
|
|
|
if (hasPositive && hasNegative)
|
|
{
|
|
break;
|
|
}
|
|
}
|
|
Assert.True(hasPositive && hasNegative, "Cycle should oscillate around zero");
|
|
}
|
|
|
|
// ═══════════════════════════════════════════════════════════════════
|
|
// C) State Management / Bar Correction
|
|
// ═══════════════════════════════════════════════════════════════════
|
|
|
|
[Fact]
|
|
public void Ccyc_BarCorrection_RestoresState()
|
|
{
|
|
var ccyc = new Ccyc();
|
|
var source = GetTestSeries(50);
|
|
for (int i = 0; i < source.Count; i++)
|
|
{
|
|
ccyc.Update(source[i], true);
|
|
}
|
|
|
|
// Get state after all bars
|
|
double lastVal = ccyc.Last.Value;
|
|
|
|
// Simulate bar correction: update with isNew=false
|
|
var correctedTv = new TValue(DateTime.UtcNow, 999.0);
|
|
ccyc.Update(correctedTv, false);
|
|
_ = ccyc.Last.Value;
|
|
|
|
// Now redo with original last value using isNew=false
|
|
ccyc.Update(source[^1], false);
|
|
double restoredVal = ccyc.Last.Value;
|
|
|
|
Assert.Equal(lastVal, restoredVal, 10);
|
|
}
|
|
|
|
[Fact]
|
|
public void Ccyc_Reset_ClearsState()
|
|
{
|
|
var ccyc = new Ccyc();
|
|
var source = GetTestSeries(100);
|
|
ccyc.Update(source);
|
|
|
|
// Verify hot
|
|
Assert.True(ccyc.IsHot);
|
|
|
|
ccyc.Reset();
|
|
|
|
// After reset, should not be hot
|
|
Assert.False(ccyc.IsHot);
|
|
}
|
|
|
|
// ═══════════════════════════════════════════════════════════════════
|
|
// D) Warmup
|
|
// ═══════════════════════════════════════════════════════════════════
|
|
|
|
[Fact]
|
|
public void Ccyc_NotHot_BeforeWarmup()
|
|
{
|
|
var ccyc = new Ccyc();
|
|
for (int i = 0; i < 6; i++)
|
|
{
|
|
ccyc.Update(new TValue(DateTime.UtcNow.AddDays(i), 100 + i), true);
|
|
Assert.False(ccyc.IsHot, $"Should not be hot at bar {i + 1}");
|
|
}
|
|
}
|
|
|
|
[Fact]
|
|
public void Ccyc_IsHot_AtWarmup()
|
|
{
|
|
var ccyc = new Ccyc();
|
|
for (int i = 0; i < 7; i++)
|
|
{
|
|
ccyc.Update(new TValue(DateTime.UtcNow.AddDays(i), 100 + i), true);
|
|
}
|
|
Assert.True(ccyc.IsHot);
|
|
}
|
|
|
|
// ═══════════════════════════════════════════════════════════════════
|
|
// E) Robustness
|
|
// ═══════════════════════════════════════════════════════════════════
|
|
|
|
[Fact]
|
|
public void Ccyc_NaN_HandledGracefully()
|
|
{
|
|
var ccyc = new Ccyc();
|
|
for (int i = 0; i < 10; i++)
|
|
{
|
|
ccyc.Update(new TValue(DateTime.UtcNow.AddDays(i), 100 + i), true);
|
|
}
|
|
_ = ccyc.Last.Value;
|
|
|
|
// Feed NaN
|
|
ccyc.Update(new TValue(DateTime.UtcNow.AddDays(10), double.NaN), true);
|
|
Assert.True(double.IsFinite(ccyc.Last.Value));
|
|
}
|
|
|
|
[Fact]
|
|
public void Ccyc_Infinity_HandledGracefully()
|
|
{
|
|
var ccyc = new Ccyc();
|
|
for (int i = 0; i < 10; i++)
|
|
{
|
|
ccyc.Update(new TValue(DateTime.UtcNow.AddDays(i), 100 + i), true);
|
|
}
|
|
|
|
ccyc.Update(new TValue(DateTime.UtcNow.AddDays(10), double.PositiveInfinity), true);
|
|
Assert.True(double.IsFinite(ccyc.Last.Value));
|
|
}
|
|
|
|
[Fact]
|
|
public void Ccyc_EmptyTSeries_ReturnsEmpty()
|
|
{
|
|
var ccyc = new Ccyc();
|
|
_ = ccyc.Update(new TSeries());
|
|
Assert.True(true); // No throw
|
|
}
|
|
|
|
[Fact]
|
|
public void Ccyc_LargeDataset_NoBlowup()
|
|
{
|
|
var ccyc = new Ccyc();
|
|
var source = TestData.Fetch(10000, StartTime, Step).Close;
|
|
var results = ccyc.Update(source);
|
|
for (int i = 0; i < results.Count; i++)
|
|
{
|
|
Assert.True(double.IsFinite(results[i].Value), $"Non-finite at {i}");
|
|
}
|
|
}
|
|
|
|
// ═══════════════════════════════════════════════════════════════════
|
|
// F) Consistency (4-API-mode)
|
|
// ═══════════════════════════════════════════════════════════════════
|
|
|
|
[Fact]
|
|
public void Ccyc_StreamingMatchesBatch()
|
|
{
|
|
var source = GetTestSeries(200);
|
|
|
|
// Streaming
|
|
var ccycStreaming = new Ccyc();
|
|
for (int i = 0; i < source.Count; i++)
|
|
{
|
|
ccycStreaming.Update(source[i], true);
|
|
}
|
|
|
|
// Batch
|
|
var batchResults = Ccyc.Batch(source);
|
|
|
|
Assert.Equal(source.Count, batchResults.Count);
|
|
|
|
// The batch method creates a fresh indicator and calls Update(TSeries),
|
|
// which processes sequentially — should match streaming exactly
|
|
var ccyc2 = new Ccyc();
|
|
var results2 = ccyc2.Update(source);
|
|
Assert.Equal(batchResults.Count, results2.Count);
|
|
for (int i = 0; i < batchResults.Count; i++)
|
|
{
|
|
Assert.Equal(batchResults[i].Value, results2[i].Value, 10);
|
|
}
|
|
}
|
|
|
|
[Fact]
|
|
public void Ccyc_SpanBatchMatchesTSeriesBatch()
|
|
{
|
|
var source = GetTestSeries(200);
|
|
var batchResults = Ccyc.Batch(source);
|
|
|
|
// Span batch
|
|
double[] values = new double[source.Count];
|
|
for (int i = 0; i < source.Count; i++)
|
|
{
|
|
values[i] = source[i].Value;
|
|
}
|
|
|
|
double[] output = new double[values.Length];
|
|
Ccyc.Batch(values.AsSpan(), output.AsSpan());
|
|
|
|
// Compare
|
|
for (int i = 0; i < batchResults.Count; i++)
|
|
{
|
|
Assert.Equal(batchResults[i].Value, output[i], 6);
|
|
}
|
|
}
|
|
|
|
[Fact]
|
|
public void Ccyc_CalculateReturnsIndicator()
|
|
{
|
|
var source = GetTestSeries(100);
|
|
var (results, indicator) = Ccyc.Calculate(source);
|
|
|
|
Assert.NotNull(indicator);
|
|
Assert.Equal(source.Count, results.Count);
|
|
Assert.True(indicator.IsHot);
|
|
}
|
|
|
|
// ═══════════════════════════════════════════════════════════════════
|
|
// G) Span API
|
|
// ═══════════════════════════════════════════════════════════════════
|
|
|
|
[Fact]
|
|
public void Ccyc_SpanBatch_LengthMismatch_Throws()
|
|
{
|
|
double[] src = [1, 2, 3];
|
|
double[] outShort = new double[2];
|
|
Assert.Throws<ArgumentException>(() => Ccyc.Batch(src.AsSpan(), outShort.AsSpan()));
|
|
}
|
|
|
|
[Fact]
|
|
public void Ccyc_SpanBatch_InvalidAlpha_Throws()
|
|
{
|
|
double[] src = [1, 2, 3];
|
|
double[] output = new double[3];
|
|
Assert.Throws<ArgumentException>(() => Ccyc.Batch(src.AsSpan(), output.AsSpan(), alpha: 0.0));
|
|
}
|
|
|
|
[Fact]
|
|
public void Ccyc_SpanBatch_EmptyInput_NoThrow()
|
|
{
|
|
double[] src = [];
|
|
double[] output = [];
|
|
Ccyc.Batch(src.AsSpan(), output.AsSpan());
|
|
Assert.True(true); // No throw
|
|
}
|
|
|
|
// ═══════════════════════════════════════════════════════════════════
|
|
// H) Chainability
|
|
// ═══════════════════════════════════════════════════════════════════
|
|
|
|
[Fact]
|
|
public void Ccyc_Chainable_ReceivesValues()
|
|
{
|
|
var source = GetTestSeries(100);
|
|
var ema = new Ema(10);
|
|
var ccyc = new Ccyc(ema, alpha: 0.07);
|
|
|
|
for (int i = 0; i < source.Count; i++)
|
|
{
|
|
ema.Update(source[i], true);
|
|
}
|
|
|
|
Assert.True(ccyc.IsHot, "Chained CCYC should become hot");
|
|
Assert.True(double.IsFinite(ccyc.Last.Value));
|
|
}
|
|
|
|
// ═══════════════════════════════════════════════════════════════════
|
|
// I) CCYC-Specific
|
|
// ═══════════════════════════════════════════════════════════════════
|
|
|
|
[Fact]
|
|
public void Ccyc_Trigger_IsDelayedCycle()
|
|
{
|
|
var ccyc = new Ccyc();
|
|
var source = GetTestSeries(50);
|
|
|
|
double prevCycle = 0;
|
|
for (int i = 0; i < source.Count; i++)
|
|
{
|
|
ccyc.Update(source[i], true);
|
|
if (i > 0)
|
|
{
|
|
// Trigger should equal previous cycle value
|
|
Assert.Equal(prevCycle, ccyc.Trigger, 10);
|
|
}
|
|
prevCycle = ccyc.Last.Value;
|
|
}
|
|
}
|
|
|
|
[Fact]
|
|
public void Ccyc_ConstantInput_ConvergesToZero()
|
|
{
|
|
var ccyc = new Ccyc();
|
|
for (int i = 0; i < 200; i++)
|
|
{
|
|
ccyc.Update(new TValue(DateTime.UtcNow.AddDays(i), 100.0), true);
|
|
}
|
|
|
|
// High-pass filter on constant → 0
|
|
Assert.True(Math.Abs(ccyc.Last.Value) < 1e-6, $"Expected near-zero, got {ccyc.Last.Value}");
|
|
}
|
|
|
|
[Fact]
|
|
public void Ccyc_SineWave_DetectsCycle()
|
|
{
|
|
var ccyc = new Ccyc();
|
|
int period = 20;
|
|
|
|
for (int i = 0; i < 200; i++)
|
|
{
|
|
double value = 100 + 10 * Math.Sin(2 * Math.PI * i / period);
|
|
ccyc.Update(new TValue(DateTime.UtcNow.AddDays(i), value), true);
|
|
}
|
|
|
|
// On a sine wave, the cycle output should have significant amplitude
|
|
Assert.True(Math.Abs(ccyc.Last.Value) > 0.01, "Cycle should detect sine wave");
|
|
}
|
|
|
|
[Fact]
|
|
public void Ccyc_DifferentAlphas_ProduceDifferentOutputs()
|
|
{
|
|
var source = GetTestSeries(200);
|
|
var resultsFast = Ccyc.Batch(source, alpha: 0.15);
|
|
var resultsSlow = Ccyc.Batch(source, alpha: 0.03);
|
|
|
|
bool anyDiff = false;
|
|
for (int i = 20; i < source.Count; i++)
|
|
{
|
|
if (Math.Abs(resultsFast[i].Value - resultsSlow[i].Value) > 1e-10)
|
|
{
|
|
anyDiff = true;
|
|
break;
|
|
}
|
|
}
|
|
Assert.True(anyDiff, "Different alphas should produce different outputs");
|
|
}
|
|
|
|
[Fact]
|
|
public void Ccyc_Prime_SetsState()
|
|
{
|
|
var ccyc = new Ccyc();
|
|
double[] primeData = new double[50];
|
|
for (int i = 0; i < 50; i++)
|
|
{
|
|
primeData[i] = 100 + 5 * Math.Sin(2 * Math.PI * i / 20.0);
|
|
}
|
|
|
|
ccyc.Prime(primeData.AsSpan());
|
|
Assert.True(ccyc.IsHot);
|
|
Assert.True(double.IsFinite(ccyc.Last.Value));
|
|
}
|
|
|
|
[Fact]
|
|
public void Ccyc_Bootstrap_DiffersFromSteadyState()
|
|
{
|
|
// First 6 bars use bootstrap; bar 7+ use IIR
|
|
var ccyc = new Ccyc();
|
|
var values = new double[] { 100, 102, 99, 101, 103, 98, 100, 104, 97 };
|
|
var results = new List<double>();
|
|
|
|
for (int i = 0; i < values.Length; i++)
|
|
{
|
|
var r = ccyc.Update(new TValue(DateTime.UtcNow.AddDays(i), values[i]), true);
|
|
results.Add(r.Value);
|
|
}
|
|
|
|
// All values should be finite
|
|
foreach (var v in results)
|
|
{
|
|
Assert.True(double.IsFinite(v));
|
|
}
|
|
|
|
// At bar 7 (index 6), we enter steady state — should still be finite
|
|
Assert.True(double.IsFinite(results[6]));
|
|
}
|
|
|
|
[Fact]
|
|
public void Ccyc_ResetAndReprocess_MatchesOriginal()
|
|
{
|
|
var source = GetTestSeries(100);
|
|
var ccyc = new Ccyc();
|
|
|
|
var results1 = ccyc.Update(source);
|
|
ccyc.Reset();
|
|
var results2 = ccyc.Update(source);
|
|
|
|
Assert.Equal(results1.Count, results2.Count);
|
|
for (int i = 0; i < results1.Count; i++)
|
|
{
|
|
Assert.Equal(results1[i].Value, results2[i].Value, 10);
|
|
}
|
|
}
|
|
}
|