mirror of
https://github.com/mihakralj/QuanTAlib.git
synced 2026-08-25 05:48:06 +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,170 @@
|
||||
using TradingPlatform.BusinessLayer;
|
||||
|
||||
namespace QuanTAlib.Quantower.Tests;
|
||||
|
||||
public class CcorIndicatorTests
|
||||
{
|
||||
[Fact]
|
||||
public void CcorIndicator_Constructor_SetsDefaults()
|
||||
{
|
||||
var indicator = new CcorIndicator();
|
||||
|
||||
Assert.Equal(20, indicator.Period);
|
||||
Assert.Equal(9.0, indicator.Threshold);
|
||||
Assert.Equal(SourceType.Close, indicator.Source);
|
||||
Assert.True(indicator.ShowColdValues);
|
||||
Assert.Equal("CCOR - Ehlers Correlation Cycle", indicator.Name);
|
||||
Assert.True(indicator.SeparateWindow);
|
||||
Assert.True(indicator.OnBackGround);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CcorIndicator_MinHistoryDepths_EqualsZero()
|
||||
{
|
||||
var indicator = new CcorIndicator();
|
||||
|
||||
Assert.Equal(0, CcorIndicator.MinHistoryDepths);
|
||||
Assert.Equal(0, ((IWatchlistIndicator)indicator).MinHistoryDepths);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CcorIndicator_ShortName_IncludesPeriodAndThreshold()
|
||||
{
|
||||
var indicator = new CcorIndicator { Period = 20, Threshold = 9.0 };
|
||||
|
||||
Assert.True(indicator.ShortName.Contains("CCOR", StringComparison.Ordinal));
|
||||
Assert.True(indicator.ShortName.Contains("20", StringComparison.Ordinal));
|
||||
Assert.True(indicator.ShortName.Contains("9.0", StringComparison.Ordinal));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CcorIndicator_Initialize_CreatesInternalCcor()
|
||||
{
|
||||
var indicator = new CcorIndicator { Period = 20, Threshold = 9.0 };
|
||||
|
||||
// Initialize should not throw
|
||||
indicator.Initialize();
|
||||
|
||||
// After init, line series should exist (Real + Imag + Angle + State)
|
||||
Assert.Equal(4, indicator.LinesSeries.Count);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CcorIndicator_ProcessUpdate_HistoricalBar_ComputesValue()
|
||||
{
|
||||
var indicator = new CcorIndicator { Period = 20, Threshold = 9.0 };
|
||||
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);
|
||||
|
||||
// All 4 line series should have a value
|
||||
for (int s = 0; s < 4; s++)
|
||||
{
|
||||
Assert.Equal(1, indicator.LinesSeries[s].Count);
|
||||
Assert.True(double.IsFinite(indicator.LinesSeries[s].GetValue(0)),
|
||||
$"Line series {s} should be finite");
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CcorIndicator_ProcessUpdate_NewBar_ComputesValue()
|
||||
{
|
||||
var indicator = new CcorIndicator { Period = 20, Threshold = 9.0 };
|
||||
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));
|
||||
|
||||
for (int s = 0; s < 4; s++)
|
||||
{
|
||||
Assert.Equal(2, indicator.LinesSeries[s].Count);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CcorIndicator_ProcessUpdate_NewTick_ProcessesWithoutError()
|
||||
{
|
||||
var indicator = new CcorIndicator { Period = 20, Threshold = 9.0 };
|
||||
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 CcorIndicator_SourceCodeLink_IsValid()
|
||||
{
|
||||
var indicator = new CcorIndicator();
|
||||
|
||||
Assert.False(string.IsNullOrEmpty(indicator.SourceCodeLink));
|
||||
Assert.Contains("Ccor.Quantower.cs", indicator.SourceCodeLink, StringComparison.Ordinal);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CcorIndicator_MultipleHistoricalBars_AllFinite()
|
||||
{
|
||||
var indicator = new CcorIndicator { Period = 10, Threshold = 9.0 };
|
||||
indicator.Initialize();
|
||||
|
||||
var now = DateTime.UtcNow;
|
||||
for (int i = 0; i < 30; 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));
|
||||
}
|
||||
|
||||
for (int s = 0; s < 4; s++)
|
||||
{
|
||||
Assert.Equal(30, indicator.LinesSeries[s].Count);
|
||||
for (int i = 0; i < 30; i++)
|
||||
{
|
||||
Assert.True(double.IsFinite(indicator.LinesSeries[s].GetValue(i)),
|
||||
$"Line series {s} at bar {i} should be finite");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CcorIndicator_CustomPeriod_ReflectedInShortName()
|
||||
{
|
||||
var indicator = new CcorIndicator { Period = 30, Threshold = 5.0 };
|
||||
Assert.Contains("30", indicator.ShortName, StringComparison.Ordinal);
|
||||
Assert.Contains("5.0", indicator.ShortName, StringComparison.Ordinal);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(SourceType.Open)]
|
||||
[InlineData(SourceType.High)]
|
||||
[InlineData(SourceType.Low)]
|
||||
[InlineData(SourceType.Close)]
|
||||
public void CcorIndicator_DifferentSources_DoNotThrow(SourceType sourceType)
|
||||
{
|
||||
var indicator = new CcorIndicator
|
||||
{
|
||||
Period = 20,
|
||||
Threshold = 9.0,
|
||||
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);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,592 @@
|
||||
using Xunit;
|
||||
|
||||
namespace QuanTAlib.Tests;
|
||||
|
||||
public class CcorTests
|
||||
{
|
||||
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, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1)).Close;
|
||||
}
|
||||
|
||||
// ── A) Constructor validation ──
|
||||
|
||||
[Fact]
|
||||
public void Ccor_DefaultConstructor_SetsDefaults()
|
||||
{
|
||||
var ind = new Ccor();
|
||||
Assert.Equal("Ccor(20,9.0)", ind.Name);
|
||||
Assert.Equal(20, ind.WarmupPeriod);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Ccor_CustomPeriod_SetsCorrectName()
|
||||
{
|
||||
var ind = new Ccor(period: 30, threshold: 5.0);
|
||||
Assert.Equal("Ccor(30,5.0)", ind.Name);
|
||||
Assert.Equal(30, ind.WarmupPeriod);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Ccor_ZeroPeriod_Throws()
|
||||
{
|
||||
var ex = Assert.Throws<ArgumentException>(() => new Ccor(period: 0));
|
||||
Assert.Equal("period", ex.ParamName);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Ccor_NegativePeriod_Throws()
|
||||
{
|
||||
var ex = Assert.Throws<ArgumentException>(() => new Ccor(period: -5));
|
||||
Assert.Equal("period", ex.ParamName);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Ccor_ZeroThreshold_Throws()
|
||||
{
|
||||
var ex = Assert.Throws<ArgumentException>(() => new Ccor(period: 20, threshold: 0.0));
|
||||
Assert.Equal("threshold", ex.ParamName);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Ccor_NegativeThreshold_Throws()
|
||||
{
|
||||
var ex = Assert.Throws<ArgumentException>(() => new Ccor(period: 20, threshold: -1.0));
|
||||
Assert.Equal("threshold", ex.ParamName);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Ccor_ChainConstructor_NullSource_Throws()
|
||||
{
|
||||
Assert.Throws<ArgumentNullException>(() => new Ccor(null!, 20, 9.0));
|
||||
}
|
||||
|
||||
// ── B) Basic calculation ──
|
||||
|
||||
[Fact]
|
||||
public void Ccor_Update_ReturnsTValue()
|
||||
{
|
||||
var ind = new Ccor();
|
||||
var result = ind.Update(new TValue(DateTime.UtcNow, 100.0));
|
||||
Assert.IsType<TValue>(result);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Ccor_AfterUpdate_LastIsAccessible()
|
||||
{
|
||||
var ind = new Ccor();
|
||||
_ = ind.Update(new TValue(DateTime.UtcNow, 100.0));
|
||||
Assert.True(double.IsFinite(ind.Last.Value));
|
||||
Assert.Equal("Ccor(20,9.0)", ind.Name);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Ccor_MultiOutput_AllAccessible()
|
||||
{
|
||||
var ind = new Ccor();
|
||||
var series = GetTestSeries(50);
|
||||
foreach (var tv in series)
|
||||
{
|
||||
_ = ind.Update(tv);
|
||||
}
|
||||
|
||||
// All multi-output properties should be accessible and finite
|
||||
Assert.True(double.IsFinite(ind.Real));
|
||||
Assert.True(double.IsFinite(ind.Imag));
|
||||
Assert.True(double.IsFinite(ind.Angle));
|
||||
Assert.Contains(ind.MarketState, new[] { -1, 0, 1 });
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Ccor_Real_BoundedMinusOneToOne()
|
||||
{
|
||||
var ind = new Ccor();
|
||||
var series = GetTestSeries(200);
|
||||
foreach (var tv in series)
|
||||
{
|
||||
_ = ind.Update(tv);
|
||||
Assert.InRange(ind.Real, -1.0, 1.0);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Ccor_Imag_BoundedMinusOneToOne()
|
||||
{
|
||||
var ind = new Ccor();
|
||||
var series = GetTestSeries(200);
|
||||
foreach (var tv in series)
|
||||
{
|
||||
_ = ind.Update(tv);
|
||||
Assert.InRange(ind.Imag, -1.0, 1.0);
|
||||
}
|
||||
}
|
||||
|
||||
// ── C) State + bar correction ──
|
||||
|
||||
[Fact]
|
||||
public void Ccor_IsNew_True_AdvancesState()
|
||||
{
|
||||
var ind = new Ccor(period: 10);
|
||||
var series = GetTestSeries(20);
|
||||
foreach (var tv in series)
|
||||
{
|
||||
_ = ind.Update(tv, isNew: true);
|
||||
}
|
||||
Assert.True(ind.IsHot);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Ccor_IsNew_False_DoesNotAdvance()
|
||||
{
|
||||
var ind = new Ccor(period: 10);
|
||||
var series = GetTestSeries(5);
|
||||
|
||||
// Process 5 bars normally
|
||||
foreach (var tv in series)
|
||||
{
|
||||
_ = ind.Update(tv, isNew: true);
|
||||
}
|
||||
|
||||
// Rewrite last bar with same value — should produce same result each time
|
||||
_ = ind.Update(series[^1], isNew: false);
|
||||
double realAfterFirst = ind.Real;
|
||||
|
||||
_ = ind.Update(series[^1], isNew: false);
|
||||
double realAfterSecond = ind.Real;
|
||||
|
||||
Assert.Equal(realAfterFirst, realAfterSecond, 10);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Ccor_BarCorrection_IterativeUpdatesRestore()
|
||||
{
|
||||
var ind = new Ccor(period: 10);
|
||||
var series = GetTestSeries(30);
|
||||
|
||||
// Process first 25 bars
|
||||
for (int i = 0; i < 25; i++)
|
||||
{
|
||||
_ = ind.Update(series[i]);
|
||||
}
|
||||
|
||||
double realSnapshot = ind.Real;
|
||||
|
||||
// Apply 5 corrections (isNew=false)
|
||||
for (int i = 0; i < 5; i++)
|
||||
{
|
||||
_ = ind.Update(new TValue(series[24].Time, 100.0 + i), isNew: false);
|
||||
}
|
||||
|
||||
// Reapply original — should restore
|
||||
_ = ind.Update(series[24], isNew: false);
|
||||
Assert.Equal(realSnapshot, ind.Real, 10);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Ccor_Reset_ClearsState()
|
||||
{
|
||||
var ind = new Ccor();
|
||||
var series = GetTestSeries(50);
|
||||
foreach (var tv in series)
|
||||
{
|
||||
_ = ind.Update(tv);
|
||||
}
|
||||
|
||||
Assert.True(ind.IsHot);
|
||||
|
||||
ind.Reset();
|
||||
|
||||
Assert.False(ind.IsHot);
|
||||
Assert.Equal(0.0, ind.Real);
|
||||
Assert.Equal(0.0, ind.Imag);
|
||||
Assert.Equal(0.0, ind.Angle);
|
||||
Assert.Equal(0, ind.MarketState);
|
||||
Assert.Equal(default, ind.Last);
|
||||
}
|
||||
|
||||
// ── D) Warmup/convergence ──
|
||||
|
||||
[Fact]
|
||||
public void Ccor_IsHot_FlipsAtWarmupPeriod()
|
||||
{
|
||||
int period = 15;
|
||||
var ind = new Ccor(period: period);
|
||||
var series = GetTestSeries(period + 5);
|
||||
|
||||
for (int i = 0; i < period - 1; i++)
|
||||
{
|
||||
_ = ind.Update(series[i]);
|
||||
Assert.False(ind.IsHot, $"Should not be hot at bar {i + 1}");
|
||||
}
|
||||
|
||||
_ = ind.Update(series[period - 1]);
|
||||
Assert.True(ind.IsHot, $"Should be hot at bar {period}");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Ccor_WarmupPeriod_EqualsPeriod()
|
||||
{
|
||||
var ind = new Ccor(period: 30);
|
||||
Assert.Equal(30, ind.WarmupPeriod);
|
||||
}
|
||||
|
||||
// ── E) Robustness ──
|
||||
|
||||
[Fact]
|
||||
public void Ccor_NaN_UsesLastValid()
|
||||
{
|
||||
var ind = new Ccor(period: 5);
|
||||
var series = GetTestSeries(10);
|
||||
|
||||
for (int i = 0; i < 8; i++)
|
||||
{
|
||||
_ = ind.Update(series[i]);
|
||||
}
|
||||
|
||||
_ = ind.Update(new TValue(DateTime.UtcNow, double.NaN));
|
||||
Assert.True(double.IsFinite(ind.Real));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Ccor_Infinity_UsesLastValid()
|
||||
{
|
||||
var ind = new Ccor(period: 5);
|
||||
var series = GetTestSeries(10);
|
||||
|
||||
for (int i = 0; i < 8; i++)
|
||||
{
|
||||
_ = ind.Update(series[i]);
|
||||
}
|
||||
|
||||
_ = ind.Update(new TValue(DateTime.UtcNow, double.PositiveInfinity));
|
||||
Assert.True(double.IsFinite(ind.Real));
|
||||
Assert.True(double.IsFinite(ind.Imag));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Ccor_BatchNaN_AllFinite()
|
||||
{
|
||||
var series = GetTestSeries(50);
|
||||
var ind = new Ccor(period: 10);
|
||||
|
||||
foreach (var tv in series)
|
||||
{
|
||||
_ = ind.Update(tv);
|
||||
}
|
||||
|
||||
// Inject NaN batch
|
||||
for (int i = 0; i < 5; i++)
|
||||
{
|
||||
_ = ind.Update(new TValue(DateTime.UtcNow, double.NaN));
|
||||
}
|
||||
|
||||
Assert.True(double.IsFinite(ind.Real));
|
||||
Assert.True(double.IsFinite(ind.Imag));
|
||||
Assert.True(double.IsFinite(ind.Angle));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Ccor_EmptyTSeries_ReturnsEmpty()
|
||||
{
|
||||
var ind = new Ccor();
|
||||
var result = ind.Update(new TSeries());
|
||||
Assert.Empty(result);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Ccor_LargeDataset_NoBlowup()
|
||||
{
|
||||
var largeData = TestData.Fetch(10000, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1)).Close;
|
||||
var ind = new Ccor();
|
||||
|
||||
for (int i = 0; i < largeData.Count; i++)
|
||||
{
|
||||
_ = ind.Update(largeData[i]);
|
||||
Assert.True(double.IsFinite(ind.Real), $"Non-finite Real at index {i}");
|
||||
Assert.True(double.IsFinite(ind.Imag), $"Non-finite Imag at index {i}");
|
||||
}
|
||||
}
|
||||
|
||||
// ── F) Consistency (4 API modes match) ──
|
||||
|
||||
[Fact]
|
||||
public void Ccor_FourApiModes_Match()
|
||||
{
|
||||
var series = GetTestSeries(100);
|
||||
int period = 20;
|
||||
double threshold = 9.0;
|
||||
|
||||
// Mode 1: Streaming
|
||||
var ind1 = new Ccor(period, threshold);
|
||||
var streaming = new double[series.Count];
|
||||
for (int i = 0; i < series.Count; i++)
|
||||
{
|
||||
streaming[i] = ind1.Update(series[i]).Value;
|
||||
}
|
||||
|
||||
// Mode 2: Batch(TSeries)
|
||||
var batchResult = Ccor.Batch(series, period, threshold);
|
||||
|
||||
// Mode 3: Batch(Span)
|
||||
double[] srcVals = new double[series.Count];
|
||||
for (int i = 0; i < series.Count; i++)
|
||||
{
|
||||
srcVals[i] = series[i].Value;
|
||||
}
|
||||
double[] spanResult = new double[series.Count];
|
||||
Ccor.Batch(srcVals, spanResult, period, threshold);
|
||||
|
||||
// Mode 4: Eventing
|
||||
var ind4 = new Ccor(period, threshold);
|
||||
var eventResults = new List<double>();
|
||||
ind4.Pub += (object? _, in TValueEventArgs e) => eventResults.Add(e.Value.Value);
|
||||
foreach (var tv in series)
|
||||
{
|
||||
_ = ind4.Update(tv);
|
||||
}
|
||||
|
||||
// Compare all modes
|
||||
for (int i = 0; i < series.Count; i++)
|
||||
{
|
||||
Assert.Equal(streaming[i], batchResult[i].Value, 10);
|
||||
Assert.Equal(streaming[i], spanResult[i], 10);
|
||||
Assert.Equal(streaming[i], eventResults[i], 10);
|
||||
}
|
||||
}
|
||||
|
||||
// ── G) Span API tests ──
|
||||
|
||||
[Fact]
|
||||
public void Ccor_SpanBatch_MismatchedLengths_Throws()
|
||||
{
|
||||
double[] src = new double[10];
|
||||
double[] output = new double[5];
|
||||
var ex = Assert.Throws<ArgumentException>(() => Ccor.Batch(src, output));
|
||||
Assert.Equal("output", ex.ParamName);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Ccor_SpanBatch_ZeroPeriod_Throws()
|
||||
{
|
||||
double[] src = new double[10];
|
||||
double[] output = new double[10];
|
||||
var ex = Assert.Throws<ArgumentException>(() => Ccor.Batch(src, output, period: 0));
|
||||
Assert.Equal("period", ex.ParamName);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Ccor_SpanBatch_ZeroThreshold_Throws()
|
||||
{
|
||||
double[] src = new double[10];
|
||||
double[] output = new double[10];
|
||||
var ex = Assert.Throws<ArgumentException>(() => Ccor.Batch(src, output, period: 20, threshold: 0.0));
|
||||
Assert.Equal("threshold", ex.ParamName);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Ccor_SpanBatch_Empty_NoException()
|
||||
{
|
||||
double[] src = Array.Empty<double>();
|
||||
double[] output = Array.Empty<double>();
|
||||
Ccor.Batch(src, output); // should not throw
|
||||
Assert.Empty(output);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Ccor_SpanBatch_MatchesTSeries()
|
||||
{
|
||||
var series = GetTestSeries(100);
|
||||
int period = 15;
|
||||
|
||||
var batchResult = Ccor.Batch(series, period);
|
||||
|
||||
double[] srcVals = new double[series.Count];
|
||||
for (int i = 0; i < series.Count; i++)
|
||||
{
|
||||
srcVals[i] = series[i].Value;
|
||||
}
|
||||
double[] spanResult = new double[series.Count];
|
||||
Ccor.Batch(srcVals, spanResult, period);
|
||||
|
||||
for (int i = 0; i < series.Count; i++)
|
||||
{
|
||||
Assert.Equal(batchResult[i].Value, spanResult[i], 10);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Ccor_SpanBatch_NaN_Handled()
|
||||
{
|
||||
double[] src = { 100, 101, double.NaN, 103, 104, 105, 106, 107, 108, 109 };
|
||||
double[] output = new double[10];
|
||||
Ccor.Batch(src, output, period: 5);
|
||||
|
||||
for (int i = 0; i < output.Length; i++)
|
||||
{
|
||||
Assert.True(double.IsFinite(output[i]), $"Non-finite at index {i}");
|
||||
}
|
||||
}
|
||||
|
||||
// ── H) Chainability ──
|
||||
|
||||
[Fact]
|
||||
public void Ccor_PubEvent_Fires()
|
||||
{
|
||||
var ind = new Ccor();
|
||||
int count = 0;
|
||||
ind.Pub += (object? _, in TValueEventArgs _) => count++;
|
||||
|
||||
var series = GetTestSeries(10);
|
||||
foreach (var tv in series)
|
||||
{
|
||||
_ = ind.Update(tv);
|
||||
}
|
||||
|
||||
Assert.Equal(10, count);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Ccor_EventChaining_Works()
|
||||
{
|
||||
var source = new Ccor(period: 10);
|
||||
var chained = new Ccor(source, period: 5);
|
||||
|
||||
var series = GetTestSeries(50);
|
||||
foreach (var tv in series)
|
||||
{
|
||||
_ = source.Update(tv);
|
||||
}
|
||||
|
||||
Assert.True(chained.IsHot);
|
||||
Assert.True(double.IsFinite(chained.Real));
|
||||
}
|
||||
|
||||
// ── CCOR-specific tests ──
|
||||
|
||||
[Fact]
|
||||
public void Ccor_ConstantInput_RealIsZero()
|
||||
{
|
||||
var ind = new Ccor(period: 10);
|
||||
for (int i = 0; i < 30; i++)
|
||||
{
|
||||
_ = ind.Update(new TValue(DateTime.UtcNow.AddMinutes(i), 100.0));
|
||||
}
|
||||
|
||||
// Constant price → zero variance in x → correlation = 0
|
||||
Assert.Equal(0.0, ind.Real, 10);
|
||||
Assert.Equal(0.0, ind.Imag, 10);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Ccor_SineWave_DetectsCorrelation()
|
||||
{
|
||||
int period = 20;
|
||||
var ind = new Ccor(period: period);
|
||||
|
||||
// Feed a perfect sine wave of the same period
|
||||
for (int i = 0; i < 100; i++)
|
||||
{
|
||||
double val = 100.0 + 10.0 * Math.Sin(2.0 * Math.PI * i / period);
|
||||
_ = ind.Update(new TValue(DateTime.UtcNow.AddMinutes(i), val));
|
||||
}
|
||||
|
||||
// After warmup, Real correlation with cosine should be significant (not zero)
|
||||
double absReal = Math.Abs(ind.Real);
|
||||
double absImag = Math.Abs(ind.Imag);
|
||||
Assert.True(absReal > 0.1 || absImag > 0.1,
|
||||
$"Sine wave should produce non-trivial correlation: Real={ind.Real:F4}, Imag={ind.Imag:F4}");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Ccor_AngleMonotonic_NeverDecreases()
|
||||
{
|
||||
var ind = new Ccor(period: 15);
|
||||
var series = GetTestSeries(200);
|
||||
double prevAngle = double.MinValue;
|
||||
|
||||
foreach (var tv in series)
|
||||
{
|
||||
_ = ind.Update(tv);
|
||||
Assert.True(ind.Angle >= prevAngle,
|
||||
$"Angle decreased: {ind.Angle:F4} < prev {prevAngle:F4}");
|
||||
prevAngle = ind.Angle;
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Ccor_MarketState_OnlyValidValues()
|
||||
{
|
||||
var ind = new Ccor();
|
||||
var series = GetTestSeries(200);
|
||||
|
||||
foreach (var tv in series)
|
||||
{
|
||||
_ = ind.Update(tv);
|
||||
Assert.Contains(ind.MarketState, new[] { -1, 0, 1 });
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Ccor_DifferentPeriods_ProduceDifferentResults()
|
||||
{
|
||||
var series = GetTestSeries(100);
|
||||
var ind10 = new Ccor(period: 10);
|
||||
var ind30 = new Ccor(period: 30);
|
||||
|
||||
foreach (var tv in series)
|
||||
{
|
||||
_ = ind10.Update(tv);
|
||||
_ = ind30.Update(tv);
|
||||
}
|
||||
|
||||
// Different periods should produce different Real values
|
||||
Assert.NotEqual(ind10.Real, ind30.Real, 5);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Ccor_Prime_SetsState()
|
||||
{
|
||||
var ind = new Ccor(period: 10);
|
||||
var series = GetTestSeries(20);
|
||||
double[] vals = new double[series.Count];
|
||||
for (int i = 0; i < series.Count; i++)
|
||||
{
|
||||
vals[i] = series[i].Value;
|
||||
}
|
||||
|
||||
ind.Prime(vals);
|
||||
Assert.True(ind.IsHot);
|
||||
Assert.True(double.IsFinite(ind.Real));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Ccor_Calculate_ReturnsBothResultsAndIndicator()
|
||||
{
|
||||
var series = GetTestSeries(50);
|
||||
var (results, indicator) = Ccor.Calculate(series);
|
||||
|
||||
Assert.Equal(50, results.Count);
|
||||
Assert.True(indicator.IsHot);
|
||||
Assert.True(double.IsFinite(indicator.Real));
|
||||
Assert.True(double.IsFinite(indicator.Imag));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Ccor_Batch_TSeries_CorrectLength()
|
||||
{
|
||||
var series = GetTestSeries(100);
|
||||
var result = Ccor.Batch(series);
|
||||
Assert.Equal(100, result.Count);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Ccor_Update_TSeries_CorrectLength()
|
||||
{
|
||||
var ind = new Ccor();
|
||||
var series = GetTestSeries(100);
|
||||
var result = ind.Update(series);
|
||||
Assert.Equal(100, result.Count);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,396 @@
|
||||
using Xunit;
|
||||
|
||||
namespace QuanTAlib.Tests;
|
||||
|
||||
/// <summary>
|
||||
/// Validation tests for CCOR - Ehlers Correlation Cycle.
|
||||
/// Since CCOR is a proprietary Ehlers algorithm with no standard library implementations
|
||||
/// (not in TA-Lib, Skender, Tulip, or Ooples), these tests validate mathematical
|
||||
/// properties of Pearson correlation and internal consistency across API modes.
|
||||
/// </summary>
|
||||
public class CcorValidationTests
|
||||
{
|
||||
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 Pearson Correlation Mathematical Properties
|
||||
|
||||
[Fact]
|
||||
public void Ccor_ConstantInput_RealAndImagAreZero()
|
||||
{
|
||||
// Constant price → zero variance in x → correlation undefined → returns 0
|
||||
var ccor = new Ccor(period: 10);
|
||||
|
||||
for (int i = 0; i < 100; i++)
|
||||
{
|
||||
ccor.Update(new TValue(DateTime.UtcNow.AddMinutes(i), 100.0), true);
|
||||
}
|
||||
|
||||
Assert.Equal(0.0, ccor.Real, Tolerance);
|
||||
Assert.Equal(0.0, ccor.Imag, Tolerance);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Ccor_PerfectCosineInput_RealNearOne()
|
||||
{
|
||||
// If price exactly matches the cosine reference, Real correlation → +1
|
||||
int period = 20;
|
||||
var ccor = new Ccor(period: period);
|
||||
|
||||
double twoPiOverN = 2.0 * Math.PI / period;
|
||||
for (int i = 0; i < 200; i++)
|
||||
{
|
||||
double val = Math.Cos(twoPiOverN * (i % period));
|
||||
ccor.Update(new TValue(DateTime.UtcNow.AddMinutes(i), val), true);
|
||||
}
|
||||
|
||||
// After many full cycles, Real should be very close to +1
|
||||
Assert.True(ccor.Real > 0.95,
|
||||
$"Perfect cosine input should give Real ≈ 1.0, got {ccor.Real:F6}");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Ccor_PerfectNegSineInput_ImagHighMagnitude()
|
||||
{
|
||||
// If price has -sin periodicity, Imag correlation magnitude should be near 1.0
|
||||
int period = 20;
|
||||
var ccor = new Ccor(period: period);
|
||||
|
||||
double twoPiOverN = 2.0 * Math.PI / period;
|
||||
for (int i = 0; i < 200; i++)
|
||||
{
|
||||
double val = -Math.Sin(twoPiOverN * (i % period));
|
||||
ccor.Update(new TValue(DateTime.UtcNow.AddMinutes(i), val), true);
|
||||
}
|
||||
|
||||
Assert.True(Math.Abs(ccor.Imag) > 0.90,
|
||||
$"Perfect -sin input should give |Imag| ≈ 1.0, got {ccor.Imag:F6}");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Ccor_RealAndImag_BoundedMinusOneToOne()
|
||||
{
|
||||
// Pearson correlation coefficient is always in [-1, +1]
|
||||
var gbm = new GBM(seed: 42);
|
||||
var bars = gbm.Fetch(1000, StartTime, Step);
|
||||
var ccor = new Ccor(period: 20);
|
||||
|
||||
for (int i = 0; i < bars.Count; i++)
|
||||
{
|
||||
ccor.Update(new TValue(bars[i].Time, bars[i].Close), true);
|
||||
Assert.InRange(ccor.Real, -1.0, 1.0);
|
||||
Assert.InRange(ccor.Imag, -1.0, 1.0);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Ccor_SineWave_RealAndImagAreOrthogonal()
|
||||
{
|
||||
// For a pure sine wave at the indicator's period, the Real (cosine) and Imag (-sine)
|
||||
// correlations should be approximately orthogonal components of a phasor
|
||||
int period = 20;
|
||||
var ccor = new Ccor(period: period);
|
||||
|
||||
for (int i = 0; i < 200; i++)
|
||||
{
|
||||
double val = 100.0 + (10.0 * Math.Sin(2.0 * Math.PI * i / period));
|
||||
ccor.Update(new TValue(DateTime.UtcNow.AddMinutes(i), val), true);
|
||||
}
|
||||
|
||||
// Both should be non-trivial
|
||||
Assert.True(Math.Abs(ccor.Real) > 0.01 || Math.Abs(ccor.Imag) > 0.01,
|
||||
$"Sine wave should produce non-trivial phasor: Real={ccor.Real:F4}, Imag={ccor.Imag:F4}");
|
||||
|
||||
// R² + I² should be near 1 for a pure tone at the matched frequency
|
||||
double magnitude = Math.Sqrt((ccor.Real * ccor.Real) + (ccor.Imag * ccor.Imag));
|
||||
Assert.True(magnitude > 0.5,
|
||||
$"Phasor magnitude should be significant for matched sine: {magnitude:F4}");
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Angle Properties
|
||||
|
||||
[Fact]
|
||||
public void Ccor_Angle_MonotonicallyNonDecreasing()
|
||||
{
|
||||
var gbm = new GBM(seed: 42);
|
||||
var bars = gbm.Fetch(500, StartTime, Step);
|
||||
var ccor = new Ccor(period: 20);
|
||||
double prevAngle = double.MinValue;
|
||||
|
||||
for (int i = 0; i < bars.Count; i++)
|
||||
{
|
||||
ccor.Update(new TValue(bars[i].Time, bars[i].Close), true);
|
||||
Assert.True(ccor.Angle >= prevAngle,
|
||||
$"Angle decreased at bar {i}: {ccor.Angle:F4} < prev {prevAngle:F4}");
|
||||
prevAngle = ccor.Angle;
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Ccor_Angle_AdvancesOnCyclicInput()
|
||||
{
|
||||
// For cyclic input, the angle should advance significantly
|
||||
int period = 20;
|
||||
var ccor = new Ccor(period: period);
|
||||
|
||||
for (int i = 0; i < 200; i++)
|
||||
{
|
||||
double val = 100.0 + (10.0 * Math.Sin(2.0 * Math.PI * i / period));
|
||||
ccor.Update(new TValue(DateTime.UtcNow.AddMinutes(i), val), true);
|
||||
}
|
||||
|
||||
Assert.True(ccor.Angle > 0.0,
|
||||
$"Angle should advance on cyclic input, got {ccor.Angle:F4}");
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Market State Properties
|
||||
|
||||
[Fact]
|
||||
public void Ccor_MarketState_OnlyValidValues()
|
||||
{
|
||||
var gbm = new GBM(seed: 42);
|
||||
var bars = gbm.Fetch(500, StartTime, Step);
|
||||
var ccor = new Ccor(period: 20, threshold: 9.0);
|
||||
|
||||
for (int i = 0; i < bars.Count; i++)
|
||||
{
|
||||
ccor.Update(new TValue(bars[i].Time, bars[i].Close), true);
|
||||
Assert.Contains(ccor.MarketState, new[] { -1, 0, 1 });
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Ccor_MarketState_HasVariation()
|
||||
{
|
||||
// Over enough data, all three states should appear at least once
|
||||
var gbm = new GBM(seed: 42);
|
||||
var bars = gbm.Fetch(2000, StartTime, Step);
|
||||
var ccor = new Ccor(period: 20, threshold: 9.0);
|
||||
var states = new HashSet<int>();
|
||||
|
||||
for (int i = 0; i < bars.Count; i++)
|
||||
{
|
||||
ccor.Update(new TValue(bars[i].Time, bars[i].Close), true);
|
||||
states.Add(ccor.MarketState);
|
||||
}
|
||||
|
||||
Assert.True(states.Count >= 2,
|
||||
$"Expected at least 2 distinct market states, got {states.Count}: {string.Join(",", states)}");
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Deterministic Reproducibility
|
||||
|
||||
[Theory]
|
||||
[InlineData(42)]
|
||||
[InlineData(123)]
|
||||
[InlineData(456)]
|
||||
public void Ccor_DeterministicOutput(int seed)
|
||||
{
|
||||
var gbm1 = new GBM(seed: seed);
|
||||
var bars1 = gbm1.Fetch(200, StartTime, Step);
|
||||
|
||||
var gbm2 = new GBM(seed: seed);
|
||||
var bars2 = gbm2.Fetch(200, StartTime, Step);
|
||||
|
||||
var ccor1 = new Ccor(period: 20, threshold: 9.0);
|
||||
var ccor2 = new Ccor(period: 20, threshold: 9.0);
|
||||
|
||||
for (int i = 0; i < bars1.Count; i++)
|
||||
{
|
||||
var r1 = ccor1.Update(new TValue(bars1[i].Time, bars1[i].Close));
|
||||
var r2 = ccor2.Update(new TValue(bars2[i].Time, bars2[i].Close));
|
||||
|
||||
Assert.Equal(r1.Value, r2.Value, Tolerance);
|
||||
}
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(10)]
|
||||
[InlineData(20)]
|
||||
[InlineData(50)]
|
||||
public void Ccor_AllPeriods_ProduceFiniteOutput(int period)
|
||||
{
|
||||
var gbm = new GBM(seed: 42);
|
||||
var bars = gbm.Fetch(500, StartTime, Step);
|
||||
var ccor = new Ccor(period: period);
|
||||
|
||||
for (int i = 0; i < bars.Count; i++)
|
||||
{
|
||||
var r = ccor.Update(new TValue(bars[i].Time, bars[i].Close));
|
||||
Assert.True(double.IsFinite(r.Value), $"Non-finite at bar {i} with period={period}");
|
||||
Assert.True(double.IsFinite(ccor.Real), $"Non-finite Real at bar {i}");
|
||||
Assert.True(double.IsFinite(ccor.Imag), $"Non-finite Imag at bar {i}");
|
||||
Assert.True(double.IsFinite(ccor.Angle), $"Non-finite Angle at bar {i}");
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Consistency Validation
|
||||
|
||||
[Fact]
|
||||
public void Ccor_BatchMatchesStreaming_OnGBM()
|
||||
{
|
||||
var gbm = new GBM(seed: 42);
|
||||
var bars = gbm.Fetch(500, StartTime, Step);
|
||||
var source = bars.Close;
|
||||
|
||||
// Streaming
|
||||
var ccorStream = new Ccor(period: 20, threshold: 9.0);
|
||||
var streamResults = new double[source.Count];
|
||||
for (int i = 0; i < source.Count; i++)
|
||||
{
|
||||
var r = ccorStream.Update(source[i], true);
|
||||
streamResults[i] = r.Value;
|
||||
}
|
||||
|
||||
// Batch
|
||||
var batchResults = Ccor.Batch(source, 20, 9.0);
|
||||
|
||||
for (int i = 0; i < source.Count; i++)
|
||||
{
|
||||
Assert.Equal(streamResults[i], batchResults[i].Value, Tolerance);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Ccor_SpanMatchesBatch_OnGBM()
|
||||
{
|
||||
var gbm = new GBM(seed: 42);
|
||||
var bars = gbm.Fetch(500, StartTime, Step);
|
||||
var source = bars.Close;
|
||||
|
||||
// TSeries batch
|
||||
var batchResults = Ccor.Batch(source, 20, 9.0);
|
||||
|
||||
// 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];
|
||||
Ccor.Batch(values.AsSpan(), output.AsSpan(), 20, 9.0);
|
||||
|
||||
for (int i = 0; i < source.Count; i++)
|
||||
{
|
||||
Assert.Equal(batchResults[i].Value, output[i], Tolerance);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Ccor_ResetAndReprocess_Matches()
|
||||
{
|
||||
var gbm = new GBM(seed: 42);
|
||||
var bars = gbm.Fetch(200, StartTime, Step);
|
||||
var source = bars.Close;
|
||||
|
||||
var ccor = new Ccor(period: 20, threshold: 9.0);
|
||||
var results1 = ccor.Update(source);
|
||||
|
||||
ccor.Reset();
|
||||
var results2 = ccor.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 Period Sensitivity
|
||||
|
||||
[Fact]
|
||||
public void Ccor_DifferentPeriods_ProduceDifferentResults()
|
||||
{
|
||||
var gbm = new GBM(seed: 42);
|
||||
var bars = gbm.Fetch(200, StartTime, Step);
|
||||
|
||||
var ccor10 = new Ccor(period: 10);
|
||||
var ccor30 = new Ccor(period: 30);
|
||||
double diffEnergy = 0;
|
||||
|
||||
for (int i = 0; i < bars.Count; i++)
|
||||
{
|
||||
var tv = new TValue(bars[i].Time, bars[i].Close);
|
||||
var r10 = ccor10.Update(tv);
|
||||
var r30 = ccor30.Update(tv);
|
||||
|
||||
if (i > 30)
|
||||
{
|
||||
double d = r10.Value - r30.Value;
|
||||
diffEnergy += d * d;
|
||||
}
|
||||
}
|
||||
|
||||
Assert.True(diffEnergy > 1e-6,
|
||||
$"Different periods should produce different outputs, diffEnergy={diffEnergy}");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Ccor_DifferentThresholds_ProduceDifferentMarketStates()
|
||||
{
|
||||
var gbm = new GBM(seed: 42);
|
||||
var bars = gbm.Fetch(500, StartTime, Step);
|
||||
|
||||
var ccorTight = new Ccor(period: 20, threshold: 1.0);
|
||||
var ccorLoose = new Ccor(period: 20, threshold: 50.0);
|
||||
|
||||
int statesDiffer = 0;
|
||||
for (int i = 0; i < bars.Count; i++)
|
||||
{
|
||||
var tv = new TValue(bars[i].Time, bars[i].Close);
|
||||
ccorTight.Update(tv);
|
||||
ccorLoose.Update(tv);
|
||||
|
||||
if (ccorTight.MarketState != ccorLoose.MarketState)
|
||||
{
|
||||
statesDiffer++;
|
||||
}
|
||||
}
|
||||
|
||||
// Real/Imag/Angle are independent of threshold — only MarketState differs
|
||||
Assert.True(statesDiffer > 0,
|
||||
"Different thresholds should produce different market state classifications");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Ccor_Correction_Recomputes()
|
||||
{
|
||||
var ind = new Ccor(period: 20);
|
||||
var t0 = new DateTime(946_684_800_000_000_0L, DateTimeKind.Utc);
|
||||
|
||||
// Build state well past warmup
|
||||
for (int i = 0; i < 100; i++)
|
||||
{
|
||||
ind.Update(new TValue(t0.AddMinutes(i),
|
||||
100.0 + (10.0 * Math.Sin(2.0 * Math.PI * i / 20.0))), isNew: true);
|
||||
}
|
||||
|
||||
// Anchor bar
|
||||
var anchorTime = t0.AddMinutes(100);
|
||||
const double anchorPrice = 105.5;
|
||||
ind.Update(new TValue(anchorTime, anchorPrice), isNew: true);
|
||||
double anchorResult = ind.Last.Value;
|
||||
|
||||
// Correction with a dramatically different price — recompute must yield different result
|
||||
ind.Update(new TValue(anchorTime, anchorPrice * 10.0), isNew: false);
|
||||
Assert.NotEqual(anchorResult, ind.Last.Value);
|
||||
|
||||
// Correction back to original price — must exactly restore original result
|
||||
ind.Update(new TValue(anchorTime, anchorPrice), isNew: false);
|
||||
Assert.Equal(anchorResult, ind.Last.Value, Tolerance);
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
Reference in New Issue
Block a user