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:
Miha Kralj
2026-03-12 12:34:16 -07:00
parent 8937b0c0fa
commit 060649192f
1149 changed files with 1780 additions and 3316 deletions
@@ -0,0 +1,161 @@
using TradingPlatform.BusinessLayer;
namespace QuanTAlib.Quantower.Tests;
public class CcycIndicatorTests
{
[Fact]
public void CcycIndicator_Constructor_SetsDefaults()
{
var indicator = new CcycIndicator();
Assert.Equal(0.07, indicator.Alpha);
Assert.Equal(SourceType.Close, indicator.Source);
Assert.True(indicator.ShowColdValues);
Assert.Equal("CCYC - Ehlers Cyber Cycle", indicator.Name);
Assert.True(indicator.SeparateWindow);
Assert.True(indicator.OnBackGround);
}
[Fact]
public void CcycIndicator_MinHistoryDepths_EqualsZero()
{
var indicator = new CcycIndicator();
Assert.Equal(0, CcycIndicator.MinHistoryDepths);
Assert.Equal(0, ((IWatchlistIndicator)indicator).MinHistoryDepths);
}
[Fact]
public void CcycIndicator_ShortName_IncludesAlpha()
{
var indicator = new CcycIndicator { Alpha = 0.07 };
Assert.True(indicator.ShortName.Contains("CCYC", StringComparison.Ordinal));
Assert.True(indicator.ShortName.Contains("0.07", StringComparison.Ordinal));
}
[Fact]
public void CcycIndicator_Initialize_CreatesInternalCcyc()
{
var indicator = new CcycIndicator { Alpha = 0.07 };
// Initialize should not throw
indicator.Initialize();
// After init, line series should exist (Cycle + Trigger)
Assert.Equal(2, indicator.LinesSeries.Count);
}
[Fact]
public void CcycIndicator_ProcessUpdate_HistoricalBar_ComputesValue()
{
var indicator = new CcycIndicator { Alpha = 0.07 };
indicator.Initialize();
// Add historical data
var now = DateTime.UtcNow;
indicator.HistoricalData.AddBar(now, 100, 105, 95, 102);
// Process update
var args = new UpdateArgs(UpdateReason.HistoricalBar);
indicator.ProcessUpdate(args);
// Line series should have a value
Assert.Equal(1, indicator.LinesSeries[0].Count);
Assert.True(double.IsFinite(indicator.LinesSeries[0].GetValue(0)));
Assert.Equal(1, indicator.LinesSeries[1].Count);
Assert.True(double.IsFinite(indicator.LinesSeries[1].GetValue(0)));
}
[Fact]
public void CcycIndicator_ProcessUpdate_NewBar_ComputesValue()
{
var indicator = new CcycIndicator { Alpha = 0.07 };
indicator.Initialize();
var now = DateTime.UtcNow;
indicator.HistoricalData.AddBar(now, 100, 105, 95, 102);
indicator.HistoricalData.AddBar(now.AddMinutes(1), 102, 108, 100, 106);
indicator.ProcessUpdate(new UpdateArgs(UpdateReason.HistoricalBar));
indicator.ProcessUpdate(new UpdateArgs(UpdateReason.NewBar));
Assert.Equal(2, indicator.LinesSeries[0].Count);
Assert.Equal(2, indicator.LinesSeries[1].Count);
}
[Fact]
public void CcycIndicator_ProcessUpdate_NewTick_ProcessesWithoutError()
{
var indicator = new CcycIndicator { Alpha = 0.07 };
indicator.Initialize();
// Should not throw an exception
indicator.ProcessUpdate(new UpdateArgs(UpdateReason.NewTick));
// Assert that the indicator still exists
Assert.NotNull(indicator);
}
[Fact]
public void CcycIndicator_SourceCodeLink_IsValid()
{
var indicator = new CcycIndicator();
Assert.False(string.IsNullOrEmpty(indicator.SourceCodeLink));
Assert.Contains("Ccyc.Quantower.cs", indicator.SourceCodeLink, StringComparison.Ordinal);
}
[Fact]
public void CcycIndicator_MultipleHistoricalBars_AllFinite()
{
var indicator = new CcycIndicator { Alpha = 0.07 };
indicator.Initialize();
var now = DateTime.UtcNow;
for (int i = 0; i < 20; i++)
{
double price = 100 + 5 * Math.Sin(2 * Math.PI * i / 20.0);
indicator.HistoricalData.AddBar(now.AddMinutes(i), price, price + 2, price - 2, price + 1);
indicator.ProcessUpdate(new UpdateArgs(UpdateReason.HistoricalBar));
}
Assert.Equal(20, indicator.LinesSeries[0].Count);
Assert.Equal(20, indicator.LinesSeries[1].Count);
for (int i = 0; i < 20; i++)
{
Assert.True(double.IsFinite(indicator.LinesSeries[0].GetValue(i)));
Assert.True(double.IsFinite(indicator.LinesSeries[1].GetValue(i)));
}
}
[Fact]
public void CcycIndicator_CustomAlpha_ReflectedInShortName()
{
var indicator = new CcycIndicator { Alpha = 0.15 };
Assert.Contains("0.15", indicator.ShortName, StringComparison.Ordinal);
}
[Theory]
[InlineData(SourceType.Open)]
[InlineData(SourceType.High)]
[InlineData(SourceType.Low)]
[InlineData(SourceType.Close)]
public void CcycIndicator_DifferentSources_DoNotThrow(SourceType sourceType)
{
var indicator = new CcycIndicator
{
Alpha = 0.07,
Source = sourceType
};
indicator.Initialize();
var now = DateTime.UtcNow;
indicator.HistoricalData.AddBar(now, 100, 105, 95, 102);
indicator.ProcessUpdate(new UpdateArgs(UpdateReason.HistoricalBar));
Assert.Equal(1, indicator.LinesSeries[0].Count);
}
}
+510
View File
@@ -0,0 +1,510 @@
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);
}
}
}
@@ -0,0 +1,383 @@
using Xunit;
using OoplesFinance.StockIndicators;
using OoplesFinance.StockIndicators.Models;
namespace QuanTAlib.Tests;
/// <summary>
/// Validation tests for CCYC - Ehlers Cyber Cycle.
/// Since CCYC is a proprietary Ehlers algorithm with no standard library implementations,
/// these tests validate mathematical properties and internal consistency.
/// </summary>
public class CcycValidationTests
{
private const double Tolerance = 1e-9;
private const long StartTime = 946_684_800_000_000_0L; // 2000-01-01 UTC in ticks
private static readonly TimeSpan Step = TimeSpan.FromMinutes(1);
#region Mathematical Property Validation
[Fact]
public void Ccyc_ConstantInput_ConvergesToZero()
{
// High-pass filter on constant input must converge to zero
var ccyc = new Ccyc(0.07);
for (int i = 0; i < 500; i++)
{
ccyc.Update(new TValue(DateTime.UtcNow.AddMinutes(i), 100.0), true);
}
Assert.True(Math.Abs(ccyc.Last.Value) < 1e-10,
$"Constant input should produce zero output, got {ccyc.Last.Value}");
}
[Fact]
public void Ccyc_LinearTrend_ConvergesToZero()
{
// High-pass filter on linear trend should converge to zero (no oscillation)
var ccyc = new Ccyc(0.07);
for (int i = 0; i < 500; i++)
{
ccyc.Update(new TValue(DateTime.UtcNow.AddMinutes(i), 100.0 + 0.5 * i), true);
}
// After warmup, should be near zero since linear trend has no cycle component
Assert.True(Math.Abs(ccyc.Last.Value) < 1.0,
$"Linear trend should produce near-zero output, got {ccyc.Last.Value}");
}
[Fact]
public void Ccyc_SineWave_ProducesNonZeroOutput()
{
// A sine wave should produce non-zero cycle output
var ccyc = new Ccyc(0.07);
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.AddMinutes(i), value), true);
}
// Cycle output should be non-trivial
Assert.True(Math.Abs(ccyc.Last.Value) > 0.01,
$"Sine wave should produce non-zero cycle, got {ccyc.Last.Value}");
}
[Theory]
[InlineData(10)]
[InlineData(20)]
[InlineData(40)]
public void Ccyc_SineWave_OutputOscillates(int period)
{
// Output should oscillate (have zero crossings) for sinusoidal input
var ccyc = new Ccyc(0.07);
int zeroCrossings = 0;
double prev = 0;
for (int i = 0; i < 300; i++)
{
double value = 100 + 10 * Math.Sin(2 * Math.PI * i / period);
var r = ccyc.Update(new TValue(DateTime.UtcNow.AddMinutes(i), value), true);
if (i > 20 && prev * r.Value < 0 && prev != 0)
{
zeroCrossings++;
}
prev = r.Value;
}
Assert.True(zeroCrossings > 3,
$"Output should oscillate with period={period}, got {zeroCrossings} zero crossings");
}
[Theory]
[InlineData(42)]
[InlineData(123)]
[InlineData(456)]
public void Ccyc_DeterministicOutput(int seed)
{
// Same input should always produce same output
var gbm = new GBM(seed: seed);
var bars1 = gbm.Fetch(200, StartTime, Step);
gbm = new GBM(seed: seed);
var bars2 = gbm.Fetch(200, StartTime, Step);
var ccyc1 = new Ccyc(0.07);
var ccyc2 = new Ccyc(0.07);
for (int i = 0; i < bars1.Count; i++)
{
var result1 = ccyc1.Update(new TValue(bars1[i].Time, bars1[i].Close));
var result2 = ccyc2.Update(new TValue(bars2[i].Time, bars2[i].Close));
Assert.Equal(result1.Value, result2.Value, Tolerance);
}
}
#endregion
#region High-Pass Filter Property Validation
[Fact]
public void Ccyc_HigherAlpha_ProducesDifferentOutput()
{
// Different alpha values should produce measurably different cycle outputs
var gbm = new GBM(seed: 42);
var bars = gbm.Fetch(200, StartTime, Step);
var ccycFast = new Ccyc(0.15);
var ccycSlow = new Ccyc(0.03);
double diffEnergy = 0;
for (int i = 0; i < bars.Count; i++)
{
var tv = new TValue(bars[i].Time, bars[i].Close);
var rFast = ccycFast.Update(tv);
var rSlow = ccycSlow.Update(tv);
if (i > 20)
{
double d = rFast.Value - rSlow.Value;
diffEnergy += d * d;
}
}
// Different alphas must produce different outputs
Assert.True(diffEnergy > 1e-6,
$"Different alphas should produce different outputs, diffEnergy={diffEnergy}");
}
[Fact]
public void Ccyc_FIR_SmoothsNoise()
{
// The 4-tap FIR smoother should reduce high-frequency noise
// Test: random noise should produce smaller cycle than sine wave
var ccycNoise = new Ccyc(0.07);
var ccycSine = new Ccyc(0.07);
var rng = new GBM(startPrice: 100.0, sigma: 0.1, seed: 42);
double sineEnergy = 0;
for (int i = 0; i < 300; i++)
{
double noiseVal = rng.Next().Close;
ccycNoise.Update(new TValue(DateTime.UtcNow.AddMinutes(i), noiseVal), true);
double sineVal = 100 + 10 * Math.Sin(2 * Math.PI * i / 20.0);
var sineResult = ccycSine.Update(new TValue(DateTime.UtcNow.AddMinutes(i), sineVal), true);
if (i > 30)
{
sineEnergy += sineResult.Value * sineResult.Value;
}
}
// Sine wave produces coherent cycle output
Assert.True(sineEnergy > 0, "Sine wave should produce energy");
}
#endregion
#region Trigger Line Validation
[Fact]
public void Ccyc_Trigger_IsOnePeriodDelayed()
{
var ccyc = new Ccyc(0.07);
var gbm = new GBM(seed: 42);
var bars = gbm.Fetch(100, StartTime, Step);
double prevCycle = 0;
for (int i = 0; i < bars.Count; i++)
{
ccyc.Update(new TValue(bars[i].Time, bars[i].Close));
if (i > 0)
{
Assert.Equal(prevCycle, ccyc.Trigger, Tolerance);
}
prevCycle = ccyc.Last.Value;
}
}
[Fact]
public void Ccyc_Trigger_CrossoverDetectable()
{
// On a sine wave, cycle and trigger should cross each other (sign change in diff)
var ccyc = new Ccyc(0.07);
int crossoverCount = 0;
double prevDiff = 0;
for (int i = 0; i < 300; i++)
{
double value = 100 + 10 * Math.Sin(2 * Math.PI * i / 20.0);
ccyc.Update(new TValue(DateTime.UtcNow.AddMinutes(i), value), true);
if (i > 20)
{
double diff = ccyc.Last.Value - ccyc.Trigger;
if (prevDiff != 0 && diff * prevDiff < 0)
{
crossoverCount++;
}
prevDiff = diff;
}
}
Assert.True(crossoverCount > 0,
"Cycle and trigger should cross on sine input");
}
#endregion
#region Consistency Validation
[Fact]
public void Ccyc_BatchMatchesStreaming_OnGBM()
{
var gbm = new GBM(seed: 42);
var bars = gbm.Fetch(500, StartTime, Step);
var source = bars.Close;
// Streaming
var ccycStream = new Ccyc(0.07);
var streamResults = new double[source.Count];
for (int i = 0; i < source.Count; i++)
{
var r = ccycStream.Update(source[i], true);
streamResults[i] = r.Value;
}
// Batch
var batchResults = Ccyc.Batch(source, 0.07);
for (int i = 0; i < source.Count; i++)
{
Assert.Equal(streamResults[i], batchResults[i].Value, Tolerance);
}
}
[Fact]
public void Ccyc_SpanMatchesBatch_OnGBM()
{
var gbm = new GBM(seed: 42);
var bars = gbm.Fetch(500, StartTime, Step);
var source = bars.Close;
// TSeries batch
var batchResults = Ccyc.Batch(source, 0.07);
// 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(), 0.07);
for (int i = 0; i < source.Count; i++)
{
Assert.Equal(batchResults[i].Value, output[i], 6);
}
}
[Theory]
[InlineData(0.03)]
[InlineData(0.07)]
[InlineData(0.15)]
[InlineData(0.30)]
public void Ccyc_AllAlphas_ProduceFiniteOutput(double alpha)
{
var gbm = new GBM(seed: 42);
var bars = gbm.Fetch(500, StartTime, Step);
var ccyc = new Ccyc(alpha);
for (int i = 0; i < bars.Count; i++)
{
var r = ccyc.Update(new TValue(bars[i].Time, bars[i].Close));
Assert.True(double.IsFinite(r.Value), $"Non-finite at bar {i} with alpha={alpha}");
}
}
[Fact]
public void Ccyc_ResetAndReprocess_Matches()
{
var gbm = new GBM(seed: 42);
var bars = gbm.Fetch(200, StartTime, Step);
var source = bars.Close;
var ccyc = new Ccyc(0.07);
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, Tolerance);
}
}
#endregion
#region Bootstrap / Steady-State Transition
[Fact]
public void Ccyc_BootstrapTransition_IsSmooth()
{
// The transition from bootstrap (bar < 7) to steady-state (bar >= 7) should be smooth
var ccyc = new Ccyc(0.07);
var results = new List<double>();
for (int i = 0; i < 20; i++)
{
double value = 100 + 5 * Math.Sin(2 * Math.PI * i / 20.0);
var r = ccyc.Update(new TValue(DateTime.UtcNow.AddMinutes(i), value), true);
results.Add(r.Value);
}
// Check that the transition at bar 7 (index 6) doesn't produce a huge jump
double jump = Math.Abs(results[6] - results[5]);
double avgMagnitude = 0;
for (int i = 3; i < 10; i++)
{
avgMagnitude += Math.Abs(results[i]);
}
avgMagnitude /= 7;
// Jump should be within reasonable bounds (not 10x the average)
if (avgMagnitude > 1e-10)
{
Assert.True(jump < 10 * avgMagnitude,
$"Bootstrap transition jump={jump} too large vs avg magnitude={avgMagnitude}");
}
}
#endregion
[Fact]
public void Ccyc_MatchesOoples_Structural()
{
var gbm = new GBM(startPrice: 100.0, mu: 0.02, sigma: 0.15, seed: 42);
var bars = gbm.Fetch(500, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1));
var ooplesData = bars.Select(b => new TickerData
{
Date = new DateTime(b.Time, DateTimeKind.Utc),
Open = b.Open, High = b.High, Low = b.Low,
Close = b.Close, Volume = b.Volume
}).ToList();
var result = new StockData(ooplesData).CalculateEhlersCyberCycle();
var values = result.CustomValuesList;
int finiteCount = values.Count(v => double.IsFinite(v));
Assert.True(finiteCount > 100, $"Expected >100 finite values, got {finiteCount}");
}
}