2026-02-11 14:46:56 -08:00
|
|
|
|
using Skender.Stock.Indicators;
|
|
|
|
|
|
using TALib;
|
2026-02-10 21:33:16 -08:00
|
|
|
|
using Xunit;
|
|
|
|
|
|
using Xunit.Abstractions;
|
|
|
|
|
|
|
|
|
|
|
|
namespace QuanTAlib.Tests;
|
|
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
2026-02-11 14:46:56 -08:00
|
|
|
|
/// Validation tests for CCI (Commodity Channel Index) against external libraries.
|
2026-02-10 21:33:16 -08:00
|
|
|
|
/// CCI = (Typical Price - SMA of TP) / (0.015 × Mean Deviation)
|
|
|
|
|
|
/// where TP = (High + Low + Close) / 3
|
2026-02-11 14:46:56 -08:00
|
|
|
|
///
|
|
|
|
|
|
/// TALib, Tulip, Skender, and Ooples all implement CCI.
|
2026-02-10 21:33:16 -08:00
|
|
|
|
/// </summary>
|
2026-02-11 14:46:56 -08:00
|
|
|
|
public sealed class CciValidationTests(ITestOutputHelper output) : IDisposable
|
2026-02-10 21:33:16 -08:00
|
|
|
|
{
|
2026-02-11 14:46:56 -08:00
|
|
|
|
private readonly ValidationTestData _testData = new();
|
|
|
|
|
|
private readonly ITestOutputHelper _output = output;
|
2026-02-10 21:33:16 -08:00
|
|
|
|
private bool _disposed;
|
|
|
|
|
|
|
2026-02-11 14:46:56 -08:00
|
|
|
|
private const int TestPeriod = 20;
|
2026-02-10 21:33:16 -08:00
|
|
|
|
|
|
|
|
|
|
public void Dispose()
|
|
|
|
|
|
{
|
2026-02-11 14:46:56 -08:00
|
|
|
|
Dispose(disposing: true);
|
2026-02-10 21:33:16 -08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
private void Dispose(bool disposing)
|
|
|
|
|
|
{
|
2026-02-11 14:46:56 -08:00
|
|
|
|
if (_disposed) { return; }
|
2026-02-10 21:33:16 -08:00
|
|
|
|
_disposed = true;
|
2026-02-11 14:46:56 -08:00
|
|
|
|
if (disposing) { _testData?.Dispose(); }
|
2026-02-10 21:33:16 -08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-02-11 14:46:56 -08:00
|
|
|
|
#region TALib Validation
|
2026-02-10 21:33:16 -08:00
|
|
|
|
|
|
|
|
|
|
[Fact]
|
2026-02-11 14:46:56 -08:00
|
|
|
|
public void Cci_MatchesTalib_Batch()
|
2026-02-10 21:33:16 -08:00
|
|
|
|
{
|
|
|
|
|
|
double[] high = _testData.Bars.Select(b => b.High).ToArray();
|
|
|
|
|
|
double[] low = _testData.Bars.Select(b => b.Low).ToArray();
|
|
|
|
|
|
double[] close = _testData.Bars.Select(b => b.Close).ToArray();
|
|
|
|
|
|
|
2026-02-11 14:46:56 -08:00
|
|
|
|
// QuanTAlib CCI
|
|
|
|
|
|
var qResult = Cci.Batch(_testData.Bars, TestPeriod);
|
2026-02-10 21:33:16 -08:00
|
|
|
|
|
2026-02-11 14:46:56 -08:00
|
|
|
|
// TALib CCI
|
|
|
|
|
|
double[] tOutput = new double[high.Length];
|
|
|
|
|
|
var retCode = TALib.Functions.Cci<double>(high, low, close, 0..^0, tOutput, out var outRange, TestPeriod);
|
2026-02-27 12:50:05 -08:00
|
|
|
|
Assert.Equal(TALib.Core.RetCode.Success, retCode);
|
2026-02-10 21:33:16 -08:00
|
|
|
|
|
2026-02-11 14:46:56 -08:00
|
|
|
|
int lookback = TALib.Functions.CciLookback(TestPeriod);
|
2026-02-10 21:33:16 -08:00
|
|
|
|
|
2026-02-11 14:46:56 -08:00
|
|
|
|
int count = qResult.Count;
|
|
|
|
|
|
int start = Math.Max(0, count - ValidationHelper.DefaultVerificationCount);
|
|
|
|
|
|
var (offset, length) = outRange.GetOffsetAndLength(tOutput.Length);
|
|
|
|
|
|
|
|
|
|
|
|
for (int i = start; i < count; i++)
|
2026-02-10 21:33:16 -08:00
|
|
|
|
{
|
2026-02-11 14:46:56 -08:00
|
|
|
|
if (i < lookback) { continue; }
|
|
|
|
|
|
int tIndex = i - offset;
|
|
|
|
|
|
if (tIndex < 0 || tIndex >= length) { continue; }
|
|
|
|
|
|
|
|
|
|
|
|
Assert.True(
|
|
|
|
|
|
Math.Abs(qResult[i].Value - tOutput[tIndex]) <= ValidationHelper.TalibTolerance,
|
|
|
|
|
|
$"Mismatch at index {i}: QuanTAlib={qResult[i].Value:G17}, TALib={tOutput[tIndex]:G17}");
|
|
|
|
|
|
}
|
|
|
|
|
|
_output.WriteLine("CCI Batch validated successfully against TALib");
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
[Fact]
|
|
|
|
|
|
public void Cci_MatchesTalib_Streaming()
|
|
|
|
|
|
{
|
|
|
|
|
|
double[] high = _testData.Bars.Select(b => b.High).ToArray();
|
|
|
|
|
|
double[] low = _testData.Bars.Select(b => b.Low).ToArray();
|
|
|
|
|
|
double[] close = _testData.Bars.Select(b => b.Close).ToArray();
|
|
|
|
|
|
|
|
|
|
|
|
// QuanTAlib CCI (streaming)
|
|
|
|
|
|
var cci = new Cci(TestPeriod);
|
|
|
|
|
|
var qResults = new List<double>();
|
|
|
|
|
|
foreach (var bar in _testData.Bars)
|
|
|
|
|
|
{
|
|
|
|
|
|
qResults.Add(cci.Update(bar).Value);
|
2026-02-10 21:33:16 -08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-02-11 14:46:56 -08:00
|
|
|
|
// TALib CCI
|
|
|
|
|
|
double[] tOutput = new double[high.Length];
|
|
|
|
|
|
var retCode = TALib.Functions.Cci<double>(high, low, close, 0..^0, tOutput, out var outRange, TestPeriod);
|
2026-02-27 12:50:05 -08:00
|
|
|
|
Assert.Equal(TALib.Core.RetCode.Success, retCode);
|
2026-02-10 21:33:16 -08:00
|
|
|
|
|
2026-02-11 14:46:56 -08:00
|
|
|
|
int lookback = TALib.Functions.CciLookback(TestPeriod);
|
|
|
|
|
|
|
|
|
|
|
|
int count = qResults.Count;
|
|
|
|
|
|
int start = Math.Max(0, count - ValidationHelper.DefaultVerificationCount);
|
|
|
|
|
|
var (offset, length) = outRange.GetOffsetAndLength(tOutput.Length);
|
|
|
|
|
|
|
|
|
|
|
|
for (int i = start; i < count; i++)
|
2026-02-10 21:33:16 -08:00
|
|
|
|
{
|
2026-02-11 14:46:56 -08:00
|
|
|
|
if (i < lookback) { continue; }
|
|
|
|
|
|
int tIndex = i - offset;
|
|
|
|
|
|
if (tIndex < 0 || tIndex >= length) { continue; }
|
|
|
|
|
|
|
|
|
|
|
|
Assert.True(
|
|
|
|
|
|
Math.Abs(qResults[i] - tOutput[tIndex]) <= ValidationHelper.TalibTolerance,
|
|
|
|
|
|
$"Mismatch at index {i}: QuanTAlib={qResults[i]:G17}, TALib={tOutput[tIndex]:G17}");
|
2026-02-10 21:33:16 -08:00
|
|
|
|
}
|
2026-02-11 14:46:56 -08:00
|
|
|
|
_output.WriteLine("CCI Streaming validated successfully against TALib");
|
2026-02-10 21:33:16 -08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
[Theory]
|
|
|
|
|
|
[InlineData(5)]
|
|
|
|
|
|
[InlineData(10)]
|
|
|
|
|
|
[InlineData(14)]
|
|
|
|
|
|
[InlineData(20)]
|
|
|
|
|
|
[InlineData(50)]
|
2026-02-11 14:46:56 -08:00
|
|
|
|
public void Cci_MatchesTalib_DifferentPeriods(int period)
|
2026-02-10 21:33:16 -08:00
|
|
|
|
{
|
|
|
|
|
|
double[] high = _testData.Bars.Select(b => b.High).ToArray();
|
|
|
|
|
|
double[] low = _testData.Bars.Select(b => b.Low).ToArray();
|
|
|
|
|
|
double[] close = _testData.Bars.Select(b => b.Close).ToArray();
|
|
|
|
|
|
|
2026-02-11 14:46:56 -08:00
|
|
|
|
var qResult = Cci.Batch(_testData.Bars, period);
|
|
|
|
|
|
|
|
|
|
|
|
double[] tOutput = new double[high.Length];
|
|
|
|
|
|
var retCode = TALib.Functions.Cci<double>(high, low, close, 0..^0, tOutput, out var outRange, period);
|
2026-02-27 12:50:05 -08:00
|
|
|
|
Assert.Equal(TALib.Core.RetCode.Success, retCode);
|
2026-02-11 14:46:56 -08:00
|
|
|
|
|
|
|
|
|
|
int lookback = TALib.Functions.CciLookback(period);
|
|
|
|
|
|
|
|
|
|
|
|
int count = qResult.Count;
|
|
|
|
|
|
int start = Math.Max(0, count - ValidationHelper.DefaultVerificationCount);
|
|
|
|
|
|
var (offset, length) = outRange.GetOffsetAndLength(tOutput.Length);
|
|
|
|
|
|
|
|
|
|
|
|
for (int i = start; i < count; i++)
|
|
|
|
|
|
{
|
|
|
|
|
|
if (i < lookback) { continue; }
|
|
|
|
|
|
int tIndex = i - offset;
|
|
|
|
|
|
if (tIndex < 0 || tIndex >= length) { continue; }
|
|
|
|
|
|
|
|
|
|
|
|
Assert.True(
|
|
|
|
|
|
Math.Abs(qResult[i].Value - tOutput[tIndex]) <= ValidationHelper.TalibTolerance,
|
|
|
|
|
|
$"Period {period}, index {i}: QuanTAlib={qResult[i].Value:G17}, TALib={tOutput[tIndex]:G17}");
|
|
|
|
|
|
}
|
|
|
|
|
|
_output.WriteLine($"CCI period={period} validated against TALib");
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
#endregion
|
|
|
|
|
|
|
|
|
|
|
|
#region Tulip Validation
|
|
|
|
|
|
|
|
|
|
|
|
[Fact]
|
|
|
|
|
|
public void Cci_MatchesTulip_Batch()
|
|
|
|
|
|
{
|
|
|
|
|
|
double[] high = _testData.Bars.Select(b => b.High).ToArray();
|
|
|
|
|
|
double[] low = _testData.Bars.Select(b => b.Low).ToArray();
|
|
|
|
|
|
double[] close = _testData.Bars.Select(b => b.Close).ToArray();
|
|
|
|
|
|
|
|
|
|
|
|
var qResult = Cci.Batch(_testData.Bars, TestPeriod);
|
|
|
|
|
|
|
|
|
|
|
|
// Tulip CCI
|
|
|
|
|
|
var cciIndicator = Tulip.Indicators.cci;
|
|
|
|
|
|
double[][] inputs = [high, low, close];
|
|
|
|
|
|
double[] options = [TestPeriod];
|
|
|
|
|
|
int lookback = cciIndicator.Start(options);
|
|
|
|
|
|
double[][] outputs = [new double[high.Length - lookback]];
|
|
|
|
|
|
|
|
|
|
|
|
cciIndicator.Run(inputs, options, outputs);
|
|
|
|
|
|
double[] tulipResult = outputs[0];
|
|
|
|
|
|
|
|
|
|
|
|
// Compare after warmup
|
|
|
|
|
|
for (int i = 0; i < tulipResult.Length; i++)
|
|
|
|
|
|
{
|
|
|
|
|
|
int qIdx = i + lookback;
|
|
|
|
|
|
Assert.Equal(tulipResult[i], qResult[qIdx].Value, 1e-6);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
_output.WriteLine("CCI Batch validated successfully against Tulip");
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
[Fact]
|
|
|
|
|
|
public void Cci_MatchesTulip_Streaming()
|
|
|
|
|
|
{
|
|
|
|
|
|
double[] high = _testData.Bars.Select(b => b.High).ToArray();
|
|
|
|
|
|
double[] low = _testData.Bars.Select(b => b.Low).ToArray();
|
|
|
|
|
|
double[] close = _testData.Bars.Select(b => b.Close).ToArray();
|
|
|
|
|
|
|
|
|
|
|
|
// QuanTAlib streaming
|
|
|
|
|
|
var cci = new Cci(TestPeriod);
|
|
|
|
|
|
var qResults = new List<double>();
|
|
|
|
|
|
foreach (var bar in _testData.Bars)
|
|
|
|
|
|
{
|
|
|
|
|
|
qResults.Add(cci.Update(bar).Value);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Tulip CCI
|
|
|
|
|
|
var cciIndicator = Tulip.Indicators.cci;
|
|
|
|
|
|
double[][] inputs = [high, low, close];
|
|
|
|
|
|
double[] options = [TestPeriod];
|
|
|
|
|
|
int lookback = cciIndicator.Start(options);
|
|
|
|
|
|
double[][] outputs = [new double[high.Length - lookback]];
|
|
|
|
|
|
|
|
|
|
|
|
cciIndicator.Run(inputs, options, outputs);
|
|
|
|
|
|
double[] tulipResult = outputs[0];
|
|
|
|
|
|
|
|
|
|
|
|
for (int i = 0; i < tulipResult.Length; i++)
|
|
|
|
|
|
{
|
|
|
|
|
|
int qIdx = i + lookback;
|
|
|
|
|
|
Assert.Equal(tulipResult[i], qResults[qIdx], 1e-6);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
_output.WriteLine("CCI Streaming validated successfully against Tulip");
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
[Theory]
|
|
|
|
|
|
[InlineData(5)]
|
|
|
|
|
|
[InlineData(10)]
|
|
|
|
|
|
[InlineData(14)]
|
|
|
|
|
|
[InlineData(50)]
|
|
|
|
|
|
public void Cci_MatchesTulip_DifferentPeriods(int period)
|
|
|
|
|
|
{
|
|
|
|
|
|
double[] high = _testData.Bars.Select(b => b.High).ToArray();
|
|
|
|
|
|
double[] low = _testData.Bars.Select(b => b.Low).ToArray();
|
|
|
|
|
|
double[] close = _testData.Bars.Select(b => b.Close).ToArray();
|
|
|
|
|
|
|
|
|
|
|
|
var qResult = Cci.Batch(_testData.Bars, period);
|
|
|
|
|
|
|
2026-02-10 21:33:16 -08:00
|
|
|
|
var cciIndicator = Tulip.Indicators.cci;
|
|
|
|
|
|
double[][] inputs = [high, low, close];
|
|
|
|
|
|
double[] options = [period];
|
|
|
|
|
|
int lookback = cciIndicator.Start(options);
|
|
|
|
|
|
double[][] outputs = [new double[high.Length - lookback]];
|
|
|
|
|
|
|
|
|
|
|
|
cciIndicator.Run(inputs, options, outputs);
|
|
|
|
|
|
double[] tulipResult = outputs[0];
|
|
|
|
|
|
|
|
|
|
|
|
for (int i = 0; i < tulipResult.Length; i++)
|
|
|
|
|
|
{
|
|
|
|
|
|
int qIdx = i + lookback;
|
|
|
|
|
|
Assert.Equal(tulipResult[i], qResult[qIdx].Value, 1e-6);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
#endregion
|
|
|
|
|
|
|
2026-02-11 14:46:56 -08:00
|
|
|
|
#region Skender Validation
|
2026-02-10 21:33:16 -08:00
|
|
|
|
|
|
|
|
|
|
[Fact]
|
2026-02-11 14:46:56 -08:00
|
|
|
|
public void Cci_MatchesSkender_Batch()
|
|
|
|
|
|
{
|
|
|
|
|
|
var qResult = Cci.Batch(_testData.Bars, TestPeriod);
|
|
|
|
|
|
|
|
|
|
|
|
var sResult = _testData.SkenderQuotes.GetCci(TestPeriod).ToList();
|
|
|
|
|
|
|
|
|
|
|
|
// Compare last 100 records
|
|
|
|
|
|
ValidationHelper.VerifyData(qResult, sResult, (s) => s.Cci);
|
|
|
|
|
|
|
|
|
|
|
|
_output.WriteLine("CCI Batch validated successfully against Skender");
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
[Fact]
|
|
|
|
|
|
public void Cci_MatchesSkender_Streaming()
|
|
|
|
|
|
{
|
|
|
|
|
|
var cci = new Cci(TestPeriod);
|
|
|
|
|
|
var qResults = new List<double>();
|
|
|
|
|
|
foreach (var bar in _testData.Bars)
|
|
|
|
|
|
{
|
|
|
|
|
|
qResults.Add(cci.Update(bar).Value);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
var sResult = _testData.SkenderQuotes.GetCci(TestPeriod).ToList();
|
|
|
|
|
|
|
|
|
|
|
|
int count = qResults.Count;
|
|
|
|
|
|
int start = Math.Max(0, count - ValidationHelper.DefaultVerificationCount);
|
|
|
|
|
|
|
|
|
|
|
|
for (int i = start; i < count; i++)
|
|
|
|
|
|
{
|
|
|
|
|
|
if (sResult[i].Cci is null) { continue; }
|
|
|
|
|
|
Assert.True(
|
|
|
|
|
|
Math.Abs(qResults[i] - sResult[i].Cci!.Value) <= ValidationHelper.SkenderTolerance,
|
|
|
|
|
|
$"Mismatch at index {i}: QuanTAlib={qResults[i]:G17}, Skender={sResult[i].Cci:G17}");
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
_output.WriteLine("CCI Streaming validated successfully against Skender");
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
[Theory]
|
|
|
|
|
|
[InlineData(5)]
|
|
|
|
|
|
[InlineData(14)]
|
|
|
|
|
|
[InlineData(50)]
|
|
|
|
|
|
public void Cci_MatchesSkender_DifferentPeriods(int period)
|
|
|
|
|
|
{
|
|
|
|
|
|
var qResult = Cci.Batch(_testData.Bars, period);
|
|
|
|
|
|
|
|
|
|
|
|
var sResult = _testData.SkenderQuotes.GetCci(period).ToList();
|
|
|
|
|
|
|
|
|
|
|
|
ValidationHelper.VerifyData(qResult, sResult, (s) => s.Cci);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
#endregion
|
|
|
|
|
|
|
|
|
|
|
|
// NOTE: Ooples CCI validation removed — OoplesFinance.StockIndicators uses a
|
|
|
|
|
|
// fundamentally different internal mean-deviation calculation that diverges up to
|
|
|
|
|
|
// ~10.6 from the standard CCI formula. TALib, Tulip, and Skender all match at 1e-6+,
|
|
|
|
|
|
// confirming QuanTAlib's CCI correctness via the standard algorithm.
|
|
|
|
|
|
|
|
|
|
|
|
#region Mathematical Validation
|
|
|
|
|
|
|
|
|
|
|
|
[Fact]
|
|
|
|
|
|
public void Cci_ManualCalculation_MatchesExpected()
|
2026-02-10 21:33:16 -08:00
|
|
|
|
{
|
|
|
|
|
|
int period = 5;
|
|
|
|
|
|
|
|
|
|
|
|
var bars = new TBarSeries();
|
|
|
|
|
|
var baseTime = DateTime.UtcNow.Ticks;
|
|
|
|
|
|
var timeStep = TimeSpan.FromMinutes(1).Ticks;
|
|
|
|
|
|
|
|
|
|
|
|
double[] highs = [22, 24, 23, 25, 26, 27, 26, 28, 27, 29];
|
|
|
|
|
|
double[] lows = [20, 22, 21, 23, 24, 25, 24, 26, 25, 27];
|
|
|
|
|
|
double[] closes = [21, 23, 22, 24, 25, 26, 25, 27, 26, 28];
|
|
|
|
|
|
|
|
|
|
|
|
for (int i = 0; i < highs.Length; i++)
|
|
|
|
|
|
{
|
|
|
|
|
|
bars.Add(new TBar(
|
|
|
|
|
|
baseTime + (i * timeStep),
|
|
|
|
|
|
21.0 + i, // open
|
|
|
|
|
|
highs[i],
|
|
|
|
|
|
lows[i],
|
|
|
|
|
|
closes[i],
|
|
|
|
|
|
1000)); // volume
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
var cci = new Cci(period);
|
|
|
|
|
|
var qResult = cci.Update(bars);
|
|
|
|
|
|
|
|
|
|
|
|
// Manual calculation for last value (index 9)
|
|
|
|
|
|
double tp5 = (27.0 + 25.0 + 26.0) / 3.0;
|
|
|
|
|
|
double tp6 = (26.0 + 24.0 + 25.0) / 3.0;
|
|
|
|
|
|
double tp7 = (28.0 + 26.0 + 27.0) / 3.0;
|
|
|
|
|
|
double tp8 = (27.0 + 25.0 + 26.0) / 3.0;
|
|
|
|
|
|
double tp9 = (29.0 + 27.0 + 28.0) / 3.0;
|
|
|
|
|
|
|
|
|
|
|
|
double smaTP = (tp5 + tp6 + tp7 + tp8 + tp9) / 5.0;
|
|
|
|
|
|
double meanDev = (Math.Abs(tp5 - smaTP) + Math.Abs(tp6 - smaTP) + Math.Abs(tp7 - smaTP) + Math.Abs(tp8 - smaTP) + Math.Abs(tp9 - smaTP)) / 5.0;
|
|
|
|
|
|
double expectedCci = (tp9 - smaTP) / (0.015 * meanDev);
|
|
|
|
|
|
|
|
|
|
|
|
Assert.Equal(expectedCci, qResult[9].Value, 1e-10);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
[Fact]
|
2026-02-11 14:46:56 -08:00
|
|
|
|
public void Cci_FlatMarket_HandlesGracefully()
|
2026-02-10 21:33:16 -08:00
|
|
|
|
{
|
|
|
|
|
|
var bars = new TBarSeries();
|
|
|
|
|
|
var baseTime = DateTime.UtcNow.Ticks;
|
|
|
|
|
|
var timeStep = TimeSpan.FromMinutes(1).Ticks;
|
|
|
|
|
|
|
|
|
|
|
|
for (int i = 0; i < 30; i++)
|
|
|
|
|
|
{
|
2026-02-11 14:46:56 -08:00
|
|
|
|
bars.Add(new TBar(baseTime + (i * timeStep), 100, 100, 100, 100, 1000));
|
2026-02-10 21:33:16 -08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
var cci = new Cci(10);
|
|
|
|
|
|
var result = cci.Update(bars);
|
|
|
|
|
|
|
2026-02-11 14:46:56 -08:00
|
|
|
|
// In flat market, deviation = 0 → should handle gracefully
|
2026-02-10 21:33:16 -08:00
|
|
|
|
for (int i = 10; i < result.Count; i++)
|
|
|
|
|
|
{
|
|
|
|
|
|
Assert.True(double.IsFinite(result[i].Value) || result[i].Value == 0,
|
|
|
|
|
|
$"CCI at index {i} should be finite or zero, got {result[i].Value}");
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
[Fact]
|
2026-02-11 14:46:56 -08:00
|
|
|
|
public void Batch_MatchesStreaming_IdenticalResults()
|
2026-02-10 21:33:16 -08:00
|
|
|
|
{
|
2026-02-11 14:46:56 -08:00
|
|
|
|
// Batch
|
|
|
|
|
|
var batchResult = Cci.Batch(_testData.Bars, TestPeriod);
|
2026-02-10 21:33:16 -08:00
|
|
|
|
|
2026-02-11 14:46:56 -08:00
|
|
|
|
// Streaming
|
|
|
|
|
|
var cci = new Cci(TestPeriod);
|
|
|
|
|
|
var streamingResults = new List<double>();
|
|
|
|
|
|
foreach (var bar in _testData.Bars)
|
2026-02-10 21:33:16 -08:00
|
|
|
|
{
|
2026-02-11 14:46:56 -08:00
|
|
|
|
streamingResults.Add(cci.Update(bar).Value);
|
2026-02-10 21:33:16 -08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-02-11 14:46:56 -08:00
|
|
|
|
Assert.Equal(batchResult.Count, streamingResults.Count);
|
|
|
|
|
|
int count = batchResult.Count;
|
|
|
|
|
|
int start = Math.Max(0, count - ValidationHelper.DefaultVerificationCount);
|
|
|
|
|
|
for (int i = start; i < count; i++)
|
|
|
|
|
|
{
|
|
|
|
|
|
Assert.Equal(batchResult[i].Value, streamingResults[i], 1e-10);
|
|
|
|
|
|
}
|
|
|
|
|
|
_output.WriteLine("CCI Batch vs Streaming consistency validated");
|
2026-02-10 21:33:16 -08:00
|
|
|
|
}
|
|
|
|
|
|
|
feat: add new indicators (Decay, Edecay, MinusDi, MinusDm, PlusDi, PlusDm, Maxindex, Minindex, Sarext) and update pine scripts, core libs, validation tests, and python bindings
2026-03-09 13:45:46 -07:00
|
|
|
|
[Fact]
|
|
|
|
|
|
public void Cci_Correction_Recomputes()
|
|
|
|
|
|
{
|
|
|
|
|
|
var ind = new Cci(20);
|
|
|
|
|
|
long t0 = TimeSpan.TicksPerSecond;
|
|
|
|
|
|
|
|
|
|
|
|
// Build state well past warmup
|
|
|
|
|
|
for (int i = 0; i < 50; i++)
|
|
|
|
|
|
{
|
2026-03-11 03:35:12 +00:00
|
|
|
|
double p = 100.0 + (i * 0.5);
|
|
|
|
|
|
ind.Update(new TBar(t0 + (i * TimeSpan.TicksPerSecond), p, p + 1, p - 1, p, 1000));
|
feat: add new indicators (Decay, Edecay, MinusDi, MinusDm, PlusDi, PlusDm, Maxindex, Minindex, Sarext) and update pine scripts, core libs, validation tests, and python bindings
2026-03-09 13:45:46 -07:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Anchor bar
|
2026-03-11 03:35:12 +00:00
|
|
|
|
long anchorTime = t0 + (50 * TimeSpan.TicksPerSecond);
|
feat: add new indicators (Decay, Edecay, MinusDi, MinusDm, PlusDi, PlusDm, Maxindex, Minindex, Sarext) and update pine scripts, core libs, validation tests, and python bindings
2026-03-09 13:45:46 -07:00
|
|
|
|
var anchorBar = new TBar(anchorTime, 125.0, 126.0, 124.0, 125.0, 1000);
|
|
|
|
|
|
ind.Update(anchorBar, isNew: true);
|
|
|
|
|
|
double anchorResult = ind.Last.Value;
|
|
|
|
|
|
|
|
|
|
|
|
// Correction with dramatically different bar
|
|
|
|
|
|
var corruptBar = new TBar(anchorTime, 1250.0, 1260.0, 1240.0, 1250.0, 1000);
|
|
|
|
|
|
ind.Update(corruptBar, isNew: false);
|
|
|
|
|
|
Assert.NotEqual(anchorResult, ind.Last.Value);
|
|
|
|
|
|
|
|
|
|
|
|
// Correction back to original — must exactly restore
|
|
|
|
|
|
ind.Update(anchorBar, isNew: false);
|
|
|
|
|
|
Assert.Equal(anchorResult, ind.Last.Value, 1e-9);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-02-10 21:33:16 -08:00
|
|
|
|
#endregion
|
|
|
|
|
|
}
|