mirror of
https://github.com/mihakralj/QuanTAlib.git
synced 2026-08-23 13:08:04 +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,284 @@
|
||||
using TradingPlatform.BusinessLayer;
|
||||
using QuanTAlib;
|
||||
|
||||
namespace QuanTAlib.Tests;
|
||||
|
||||
public class CviIndicatorTests
|
||||
{
|
||||
[Fact]
|
||||
public void CviIndicator_Constructor_SetsDefaults()
|
||||
{
|
||||
var indicator = new CviIndicator();
|
||||
|
||||
Assert.Equal(10, indicator.RocLength);
|
||||
Assert.Equal(10, indicator.SmoothLength);
|
||||
Assert.True(indicator.ShowColdValues);
|
||||
Assert.Equal("CVI - Chaikin's Volatility", indicator.Name);
|
||||
Assert.True(indicator.SeparateWindow);
|
||||
Assert.True(indicator.OnBackGround);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CviIndicator_ShortName_IncludesParameters()
|
||||
{
|
||||
var indicator = new CviIndicator { RocLength = 14, SmoothLength = 20 };
|
||||
Assert.Contains("CVI", indicator.ShortName, StringComparison.Ordinal);
|
||||
Assert.Contains("14", indicator.ShortName, StringComparison.Ordinal);
|
||||
Assert.Contains("20", indicator.ShortName, StringComparison.Ordinal);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CviIndicator_MinHistoryDepths_EqualsZero()
|
||||
{
|
||||
var indicator = new CviIndicator();
|
||||
|
||||
Assert.Equal(0, CviIndicator.MinHistoryDepths);
|
||||
Assert.Equal(0, ((IWatchlistIndicator)indicator).MinHistoryDepths);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CviIndicator_Initialize_CreatesInternalCvi()
|
||||
{
|
||||
var indicator = new CviIndicator();
|
||||
|
||||
// Initialize should not throw
|
||||
indicator.Initialize();
|
||||
|
||||
// After init, line series should exist
|
||||
Assert.Single(indicator.LinesSeries);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CviIndicator_ProcessUpdate_HistoricalBar_ComputesValue()
|
||||
{
|
||||
var indicator = new CviIndicator { RocLength = 5, SmoothLength = 5 };
|
||||
indicator.Initialize();
|
||||
|
||||
// Add historical data with varying high-low ranges
|
||||
var now = DateTime.UtcNow;
|
||||
for (int i = 0; i < 30; i++)
|
||||
{
|
||||
double basePrice = 100 + i;
|
||||
double range = 2 + (i % 5); // Varying ranges
|
||||
indicator.HistoricalData.AddBar(now.AddMinutes(i), basePrice, basePrice + range, basePrice - range, basePrice + 1, 1000);
|
||||
|
||||
// Process update for each bar to simulate history loading
|
||||
var args = new UpdateArgs(UpdateReason.HistoricalBar);
|
||||
indicator.ProcessUpdate(args);
|
||||
}
|
||||
|
||||
// Line series should have a value
|
||||
double val = indicator.LinesSeries[0].GetValue(0);
|
||||
Assert.True(double.IsFinite(val));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CviIndicator_ProcessUpdate_NewBar_ComputesValue()
|
||||
{
|
||||
var indicator = new CviIndicator { RocLength = 5, SmoothLength = 5 };
|
||||
indicator.Initialize();
|
||||
|
||||
var now = DateTime.UtcNow;
|
||||
for (int i = 0; i < 30; i++)
|
||||
{
|
||||
double basePrice = 100 + i;
|
||||
indicator.HistoricalData.AddBar(now.AddMinutes(i), basePrice, basePrice + 5, basePrice - 5, basePrice + 2, 1000);
|
||||
}
|
||||
|
||||
indicator.ProcessUpdate(new UpdateArgs(UpdateReason.HistoricalBar));
|
||||
|
||||
// Add new bar with larger range
|
||||
indicator.HistoricalData.AddBar(now.AddMinutes(30), 120, 135, 105, 125, 1500);
|
||||
indicator.ProcessUpdate(new UpdateArgs(UpdateReason.NewBar));
|
||||
|
||||
Assert.Equal(2, indicator.LinesSeries[0].Count);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CviIndicator_DifferentRocLengths_Work()
|
||||
{
|
||||
int[] rocLengths = { 5, 10, 14, 20 };
|
||||
|
||||
foreach (var rocLength in rocLengths)
|
||||
{
|
||||
var indicator = new CviIndicator { RocLength = rocLength, SmoothLength = 10 };
|
||||
indicator.Initialize();
|
||||
|
||||
var now = DateTime.UtcNow;
|
||||
for (int i = 0; i < 50; i++)
|
||||
{
|
||||
double basePrice = 100 + i;
|
||||
double range = 3 + (i % 4);
|
||||
indicator.HistoricalData.AddBar(now.AddMinutes(i), basePrice, basePrice + range, basePrice - range, basePrice + 1, 1000);
|
||||
indicator.ProcessUpdate(new UpdateArgs(UpdateReason.HistoricalBar));
|
||||
}
|
||||
|
||||
double val = indicator.LinesSeries[0].GetValue(0);
|
||||
Assert.True(double.IsFinite(val), $"ROC length {rocLength} should produce finite value");
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CviIndicator_DifferentSmoothLengths_Work()
|
||||
{
|
||||
int[] smoothLengths = { 5, 10, 14, 20 };
|
||||
|
||||
foreach (var smoothLength in smoothLengths)
|
||||
{
|
||||
var indicator = new CviIndicator { RocLength = 10, SmoothLength = smoothLength };
|
||||
indicator.Initialize();
|
||||
|
||||
var now = DateTime.UtcNow;
|
||||
for (int i = 0; i < 50; i++)
|
||||
{
|
||||
double basePrice = 100 + i;
|
||||
double range = 3 + (i % 4);
|
||||
indicator.HistoricalData.AddBar(now.AddMinutes(i), basePrice, basePrice + range, basePrice - range, basePrice + 1, 1000);
|
||||
indicator.ProcessUpdate(new UpdateArgs(UpdateReason.HistoricalBar));
|
||||
}
|
||||
|
||||
double val = indicator.LinesSeries[0].GetValue(0);
|
||||
Assert.True(double.IsFinite(val), $"Smooth length {smoothLength} should produce finite value");
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CviIndicator_RocLength_CanBeChanged()
|
||||
{
|
||||
var indicator = new CviIndicator();
|
||||
Assert.Equal(10, indicator.RocLength);
|
||||
|
||||
indicator.RocLength = 14;
|
||||
Assert.Equal(14, indicator.RocLength);
|
||||
|
||||
indicator.RocLength = 20;
|
||||
Assert.Equal(20, indicator.RocLength);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CviIndicator_SmoothLength_CanBeChanged()
|
||||
{
|
||||
var indicator = new CviIndicator();
|
||||
Assert.Equal(10, indicator.SmoothLength);
|
||||
|
||||
indicator.SmoothLength = 14;
|
||||
Assert.Equal(14, indicator.SmoothLength);
|
||||
|
||||
indicator.SmoothLength = 20;
|
||||
Assert.Equal(20, indicator.SmoothLength);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CviIndicator_ShowColdValues_CanBeToggled()
|
||||
{
|
||||
var indicator = new CviIndicator();
|
||||
Assert.True(indicator.ShowColdValues);
|
||||
|
||||
indicator.ShowColdValues = false;
|
||||
Assert.False(indicator.ShowColdValues);
|
||||
|
||||
indicator.ShowColdValues = true;
|
||||
Assert.True(indicator.ShowColdValues);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CviIndicator_SourceCodeLink_IsValid()
|
||||
{
|
||||
var indicator = new CviIndicator();
|
||||
Assert.Contains("github.com", indicator.SourceCodeLink, StringComparison.Ordinal);
|
||||
Assert.Contains("Cvi.Quantower.cs", indicator.SourceCodeLink, StringComparison.Ordinal);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CviIndicator_ExpandingVolatility_ProducesPositiveValues()
|
||||
{
|
||||
var indicator = new CviIndicator { RocLength = 5, SmoothLength = 5 };
|
||||
indicator.Initialize();
|
||||
|
||||
var now = DateTime.UtcNow;
|
||||
|
||||
// First 20 bars: small range
|
||||
for (int i = 0; i < 20; i++)
|
||||
{
|
||||
double basePrice = 100;
|
||||
indicator.HistoricalData.AddBar(now.AddMinutes(i), basePrice, basePrice + 1, basePrice - 1, basePrice, 1000);
|
||||
indicator.ProcessUpdate(new UpdateArgs(UpdateReason.HistoricalBar));
|
||||
}
|
||||
|
||||
// Next 15 bars: expanding range
|
||||
for (int i = 20; i < 35; i++)
|
||||
{
|
||||
double basePrice = 100;
|
||||
double range = 1 + (i - 20) * 0.5; // Gradually increasing range
|
||||
indicator.HistoricalData.AddBar(now.AddMinutes(i), basePrice, basePrice + range, basePrice - range, basePrice, 1000);
|
||||
indicator.ProcessUpdate(new UpdateArgs(UpdateReason.HistoricalBar));
|
||||
}
|
||||
|
||||
double val = indicator.LinesSeries[0].GetValue(0);
|
||||
Assert.True(double.IsFinite(val), "Expanding volatility should produce finite value");
|
||||
// With expanding ranges, CVI should trend positive
|
||||
Assert.True(val > 0, "Expanding volatility should produce positive CVI");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CviIndicator_ContractingVolatility_ProducesNegativeValues()
|
||||
{
|
||||
var indicator = new CviIndicator { RocLength = 5, SmoothLength = 5 };
|
||||
indicator.Initialize();
|
||||
|
||||
var now = DateTime.UtcNow;
|
||||
|
||||
// First 20 bars: large range
|
||||
for (int i = 0; i < 20; i++)
|
||||
{
|
||||
double basePrice = 100;
|
||||
indicator.HistoricalData.AddBar(now.AddMinutes(i), basePrice, basePrice + 10, basePrice - 10, basePrice, 1000);
|
||||
indicator.ProcessUpdate(new UpdateArgs(UpdateReason.HistoricalBar));
|
||||
}
|
||||
|
||||
// Next 15 bars: contracting range
|
||||
for (int i = 20; i < 35; i++)
|
||||
{
|
||||
double basePrice = 100;
|
||||
double range = Math.Max(1, 10 - (i - 20) * 0.5); // Gradually decreasing range
|
||||
indicator.HistoricalData.AddBar(now.AddMinutes(i), basePrice, basePrice + range, basePrice - range, basePrice, 1000);
|
||||
indicator.ProcessUpdate(new UpdateArgs(UpdateReason.HistoricalBar));
|
||||
}
|
||||
|
||||
double val = indicator.LinesSeries[0].GetValue(0);
|
||||
Assert.True(double.IsFinite(val), "Contracting volatility should produce finite value");
|
||||
// With contracting ranges, CVI should trend negative
|
||||
Assert.True(val < 0, "Contracting volatility should produce negative CVI");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CviIndicator_UsesHighLowRange()
|
||||
{
|
||||
var indicator1 = new CviIndicator { RocLength = 5, SmoothLength = 5 };
|
||||
var indicator2 = new CviIndicator { RocLength = 5, SmoothLength = 5 };
|
||||
indicator1.Initialize();
|
||||
indicator2.Initialize();
|
||||
|
||||
var now = DateTime.UtcNow;
|
||||
|
||||
// Same OHLC structure but different ranges
|
||||
for (int i = 0; i < 30; i++)
|
||||
{
|
||||
// Indicator 1: narrow range
|
||||
indicator1.HistoricalData.AddBar(now.AddMinutes(i), 100, 102, 98, 101, 1000);
|
||||
indicator1.ProcessUpdate(new UpdateArgs(UpdateReason.HistoricalBar));
|
||||
|
||||
// Indicator 2: wide range (same open/close, different high/low)
|
||||
indicator2.HistoricalData.AddBar(now.AddMinutes(i), 100, 110, 90, 101, 1000);
|
||||
indicator2.ProcessUpdate(new UpdateArgs(UpdateReason.HistoricalBar));
|
||||
}
|
||||
|
||||
double val1 = indicator1.LinesSeries[0].GetValue(0);
|
||||
double val2 = indicator2.LinesSeries[0].GetValue(0);
|
||||
|
||||
Assert.True(double.IsFinite(val1));
|
||||
Assert.True(double.IsFinite(val2));
|
||||
// With constant but different ranges, the absolute values may differ
|
||||
// but both should be close to 0 (no rate of change)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,500 @@
|
||||
namespace QuanTAlib.Tests;
|
||||
using Xunit;
|
||||
|
||||
public class CviTests
|
||||
{
|
||||
private const double Tolerance = 1e-10;
|
||||
|
||||
private static TBarSeries GenerateTestData(int count = 100)
|
||||
{
|
||||
var gbm = new GBM(seed: 42);
|
||||
return gbm.Fetch(count, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Constructor_ValidatesInput()
|
||||
{
|
||||
Assert.Throws<ArgumentException>(() => new Cvi(0, 10));
|
||||
Assert.Throws<ArgumentException>(() => new Cvi(-1, 10));
|
||||
Assert.Throws<ArgumentException>(() => new Cvi(10, 0));
|
||||
Assert.Throws<ArgumentException>(() => new Cvi(10, -1));
|
||||
|
||||
var valid = new Cvi(10, 10);
|
||||
Assert.Equal(10, valid.RocLength);
|
||||
Assert.Equal(10, valid.SmoothLength);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void WarmupPeriod_IsCorrect()
|
||||
{
|
||||
var cvi = new Cvi(10, 10);
|
||||
Assert.Equal(20, cvi.WarmupPeriod); // smoothLength + rocLength
|
||||
Assert.True(cvi.WarmupPeriod > 0);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Properties_Accessible()
|
||||
{
|
||||
var cvi = new Cvi(14, 10);
|
||||
Assert.Equal(14, cvi.RocLength);
|
||||
Assert.Equal(10, cvi.SmoothLength);
|
||||
Assert.Equal("Cvi(14,10)", cvi.Name);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void BasicCalculation_DoesNotCrash()
|
||||
{
|
||||
var cvi = new Cvi(5, 5);
|
||||
var bars = GenerateTestData(100);
|
||||
|
||||
for (int i = 0; i < bars.Count; i++)
|
||||
{
|
||||
var result = cvi.Update(bars[i]);
|
||||
Assert.True(double.IsFinite(result.Value));
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Calc_ReturnsValue()
|
||||
{
|
||||
var cvi = new Cvi(10, 10);
|
||||
var bars = GenerateTestData(30);
|
||||
|
||||
for (int i = 0; i < bars.Count; i++)
|
||||
{
|
||||
var result = cvi.Update(bars[i]);
|
||||
Assert.True(double.IsFinite(result.Value));
|
||||
}
|
||||
|
||||
Assert.True(cvi.IsHot);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Calc_IsNew_AcceptsParameter()
|
||||
{
|
||||
var cvi = new Cvi(10, 10);
|
||||
var bars = GenerateTestData(5);
|
||||
|
||||
var result1 = cvi.Update(bars[0], isNew: true);
|
||||
var result2 = cvi.Update(bars[1], isNew: true);
|
||||
var result3 = cvi.Update(bars[2], isNew: false);
|
||||
|
||||
Assert.True(double.IsFinite(result1.Value));
|
||||
Assert.True(double.IsFinite(result2.Value));
|
||||
Assert.True(double.IsFinite(result3.Value));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Calc_IsNew_False_UpdatesValue()
|
||||
{
|
||||
var cvi = new Cvi(5, 5);
|
||||
var bars = GenerateTestData(20);
|
||||
|
||||
for (int i = 0; i < 15; i++)
|
||||
{
|
||||
cvi.Update(bars[i], isNew: true);
|
||||
}
|
||||
|
||||
var baseline = cvi.Update(bars[15], isNew: true);
|
||||
|
||||
// Create a bar with very different High-Low range
|
||||
var modifiedBar = new TBar(
|
||||
bars[15].Time,
|
||||
bars[15].Open,
|
||||
bars[15].High + 10, // Increase high
|
||||
bars[15].Low - 10, // Decrease low
|
||||
bars[15].Close,
|
||||
bars[15].Volume
|
||||
);
|
||||
var updated = cvi.Update(modifiedBar, isNew: false);
|
||||
|
||||
Assert.NotEqual(baseline.Value, updated.Value);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void IsHot_BecomesTrueAfterWarmup()
|
||||
{
|
||||
int rocLength = 10;
|
||||
int smoothLength = 10;
|
||||
var cvi = new Cvi(rocLength, smoothLength);
|
||||
var bars = GenerateTestData(30);
|
||||
|
||||
int warmup = smoothLength + rocLength; // 20
|
||||
|
||||
for (int i = 0; i < warmup - 1; i++)
|
||||
{
|
||||
cvi.Update(bars[i]);
|
||||
Assert.False(cvi.IsHot);
|
||||
}
|
||||
|
||||
cvi.Update(bars[warmup - 1]);
|
||||
Assert.True(cvi.IsHot);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Reset_Works()
|
||||
{
|
||||
var cvi = new Cvi(10, 10);
|
||||
var bars = GenerateTestData(30);
|
||||
|
||||
for (int i = 0; i < bars.Count; i++)
|
||||
{
|
||||
cvi.Update(bars[i]);
|
||||
}
|
||||
Assert.True(cvi.IsHot);
|
||||
|
||||
cvi.Reset();
|
||||
Assert.False(cvi.IsHot);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SingleValue_ReturnsZero()
|
||||
{
|
||||
var cvi = new Cvi(5, 5);
|
||||
var bars = GenerateTestData(1);
|
||||
var result = cvi.Update(bars[0]);
|
||||
|
||||
// First value has no ROC data yet, should be 0
|
||||
Assert.Equal(0.0, result.Value);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void IterativeCorrections_RestoreToOriginalState()
|
||||
{
|
||||
var cvi = new Cvi(10, 10);
|
||||
var bars = GenerateTestData(50);
|
||||
|
||||
TValue lastValue = default;
|
||||
for (int i = 0; i < bars.Count; i++)
|
||||
{
|
||||
lastValue = cvi.Update(bars[i], isNew: true);
|
||||
}
|
||||
double originalValue = lastValue.Value;
|
||||
|
||||
// Apply a correction with very different range
|
||||
var modifiedBar = new TBar(
|
||||
bars[bars.Count - 1].Time,
|
||||
100, 200, 50, 150, 1000
|
||||
);
|
||||
var correctedValue = cvi.Update(modifiedBar, isNew: false);
|
||||
Assert.NotEqual(originalValue, correctedValue.Value);
|
||||
|
||||
// Restore original value
|
||||
var restoredValue = cvi.Update(bars[bars.Count - 1], isNew: false);
|
||||
Assert.Equal(originalValue, restoredValue.Value, 1e-9);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void IsNew_Consistency()
|
||||
{
|
||||
var cvi = new Cvi(10, 10);
|
||||
var bars = GenerateTestData(25);
|
||||
|
||||
for (int i = 0; i < 20; i++)
|
||||
{
|
||||
cvi.Update(bars[i], isNew: true);
|
||||
}
|
||||
|
||||
var result1 = cvi.Update(bars[20], isNew: true);
|
||||
_ = cvi.Update(bars[21], isNew: false);
|
||||
var result3 = cvi.Update(bars[20], isNew: false);
|
||||
|
||||
Assert.Equal(result1.Value, result3.Value, Tolerance);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void NaN_Input_UsesLastValidValue()
|
||||
{
|
||||
var cvi = new Cvi(5, 5);
|
||||
var bars = GenerateTestData(20);
|
||||
|
||||
for (int i = 0; i < 15; i++)
|
||||
{
|
||||
cvi.Update(bars[i]);
|
||||
}
|
||||
|
||||
// Update with NaN value (treated as pre-calculated range)
|
||||
var resultNan = cvi.Update(new TValue(DateTime.UtcNow, double.NaN));
|
||||
Assert.True(double.IsFinite(resultNan.Value));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Infinity_Input_UsesLastValidValue()
|
||||
{
|
||||
var cvi = new Cvi(5, 5);
|
||||
var bars = GenerateTestData(20);
|
||||
|
||||
for (int i = 0; i < 15; i++)
|
||||
{
|
||||
cvi.Update(bars[i]);
|
||||
}
|
||||
|
||||
var resultInf = cvi.Update(new TValue(DateTime.UtcNow, double.PositiveInfinity));
|
||||
Assert.True(double.IsFinite(resultInf.Value));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void NegativeRange_UsesLastValidValue()
|
||||
{
|
||||
var cvi = new Cvi(5, 5);
|
||||
var bars = GenerateTestData(20);
|
||||
|
||||
for (int i = 0; i < 15; i++)
|
||||
{
|
||||
cvi.Update(bars[i]);
|
||||
}
|
||||
|
||||
// Negative range is invalid for High-Low
|
||||
var resultNeg = cvi.Update(new TValue(DateTime.UtcNow, -5.0));
|
||||
Assert.True(double.IsFinite(resultNeg.Value));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void LargeDataset_Performance()
|
||||
{
|
||||
var cvi = new Cvi(14, 10);
|
||||
var bars = GenerateTestData(5000);
|
||||
|
||||
for (int i = 0; i < bars.Count; i++)
|
||||
{
|
||||
var result = cvi.Update(bars[i]);
|
||||
Assert.True(double.IsFinite(result.Value));
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void TBarSeries_Update_Works()
|
||||
{
|
||||
int rocLength = 10;
|
||||
int smoothLength = 10;
|
||||
var cvi = new Cvi(rocLength, smoothLength);
|
||||
var bars = GenerateTestData(100);
|
||||
|
||||
var result = cvi.Update(bars);
|
||||
|
||||
Assert.Equal(bars.Count, result.Count);
|
||||
Assert.True(double.IsFinite(result[result.Count - 1].Value));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void TSeries_Update_MatchesStreaming()
|
||||
{
|
||||
int rocLength = 10;
|
||||
int smoothLength = 10;
|
||||
var cviStream = new Cvi(rocLength, smoothLength);
|
||||
var cviBatch = new Cvi(rocLength, smoothLength);
|
||||
var bars = GenerateTestData(100);
|
||||
|
||||
// Streaming mode
|
||||
for (int i = 0; i < bars.Count; i++)
|
||||
{
|
||||
cviStream.Update(bars[i]);
|
||||
}
|
||||
|
||||
// Batch mode with TBarSeries
|
||||
var result = cviBatch.Update(bars);
|
||||
|
||||
Assert.Equal(cviStream.Last.Value, result[result.Count - 1].Value, 1e-9);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void BatchCalc_MatchesIterativeCalc()
|
||||
{
|
||||
var cvi = new Cvi(10, 10);
|
||||
var bars = GenerateTestData(200);
|
||||
|
||||
// Streaming
|
||||
for (int i = 0; i < bars.Count; i++)
|
||||
{
|
||||
cvi.Update(bars[i]);
|
||||
}
|
||||
var iterativeResult = cvi.Last.Value;
|
||||
|
||||
// Batch via static method
|
||||
var batchResult = Cvi.Batch(bars, 10, 10);
|
||||
|
||||
Assert.Equal(iterativeResult, batchResult[batchResult.Count - 1].Value, 1e-8);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void StaticBatch_Works()
|
||||
{
|
||||
var bars = GenerateTestData(100);
|
||||
|
||||
var result = Cvi.Batch(bars, 14, 10);
|
||||
|
||||
Assert.Equal(100, result.Count);
|
||||
Assert.True(double.IsFinite(result[result.Count - 1].Value));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void StaticBatch_ValidatesInput()
|
||||
{
|
||||
var ts = new TSeries();
|
||||
for (int i = 0; i < 10; i++)
|
||||
{
|
||||
ts.Add(new TValue(DateTime.UtcNow.AddMinutes(i), 100 + i));
|
||||
}
|
||||
|
||||
Assert.Throws<ArgumentException>(() => Cvi.Batch(ts, 0, 10));
|
||||
Assert.Throws<ArgumentException>(() => Cvi.Batch(ts, -1, 10));
|
||||
Assert.Throws<ArgumentException>(() => Cvi.Batch(ts, 10, 0));
|
||||
Assert.Throws<ArgumentException>(() => Cvi.Batch(ts, 10, -1));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Batch_NaN_Safe()
|
||||
{
|
||||
var values = new double[] { 1.0, 1.2, 1.1, double.NaN, 1.3, 1.4 };
|
||||
var output = new double[values.Length];
|
||||
|
||||
Cvi.Batch(values, output, 2, 2);
|
||||
|
||||
Assert.True(output.Length == 6);
|
||||
for (int i = 0; i < output.Length; i++)
|
||||
{
|
||||
Assert.True(double.IsFinite(output[i]));
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ConstantRange_ZeroVolatility()
|
||||
{
|
||||
var cvi = new Cvi(10, 10);
|
||||
|
||||
// Feed constant high-low range
|
||||
for (int i = 0; i < 30; i++)
|
||||
{
|
||||
var bar = new TBar(
|
||||
DateTime.UtcNow.AddMinutes(i).Ticks,
|
||||
100.0, 105.0, 95.0, 102.0, 1000.0 // Constant 10-point range
|
||||
);
|
||||
cvi.Update(bar);
|
||||
}
|
||||
|
||||
// Constant range should result in zero or near-zero CVI (no rate of change)
|
||||
Assert.True(Math.Abs(cvi.Last.Value) < 1.0, "Constant range should have near-zero CVI");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ExpandingVolatility_PositiveValue()
|
||||
{
|
||||
var cvi = new Cvi(5, 5);
|
||||
|
||||
// Start with small range, expand over time
|
||||
for (int i = 0; i < 20; i++)
|
||||
{
|
||||
double range = 5 + i * 0.5; // Expanding range
|
||||
var bar = new TBar(
|
||||
DateTime.UtcNow.AddMinutes(i).Ticks,
|
||||
100.0, 100.0 + range / 2, 100.0 - range / 2, 100.0, 1000.0
|
||||
);
|
||||
cvi.Update(bar);
|
||||
}
|
||||
|
||||
// Expanding volatility should produce positive CVI
|
||||
Assert.True(cvi.Last.Value > 0, "Expanding volatility should produce positive CVI");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ContractingVolatility_NegativeValue()
|
||||
{
|
||||
var cvi = new Cvi(5, 5);
|
||||
|
||||
// Start with large range, contract over time
|
||||
for (int i = 0; i < 20; i++)
|
||||
{
|
||||
double range = 20 - i * 0.5; // Contracting range
|
||||
if (range < 1)
|
||||
{
|
||||
range = 1;
|
||||
}
|
||||
var bar = new TBar(
|
||||
DateTime.UtcNow.AddMinutes(i).Ticks,
|
||||
100.0, 100.0 + range / 2, 100.0 - range / 2, 100.0, 1000.0
|
||||
);
|
||||
cvi.Update(bar);
|
||||
}
|
||||
|
||||
// Contracting volatility should produce negative CVI
|
||||
Assert.True(cvi.Last.Value < 0, "Contracting volatility should produce negative CVI");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void DifferentParameters_ProduceDistinctValues()
|
||||
{
|
||||
var bars = GenerateTestData(50);
|
||||
|
||||
var cvi1 = new Cvi(10, 10);
|
||||
var cvi2 = new Cvi(14, 10);
|
||||
var cvi3 = new Cvi(10, 14);
|
||||
|
||||
for (int i = 0; i < bars.Count; i++)
|
||||
{
|
||||
cvi1.Update(bars[i]);
|
||||
cvi2.Update(bars[i]);
|
||||
cvi3.Update(bars[i]);
|
||||
}
|
||||
|
||||
Assert.True(double.IsFinite(cvi1.Last.Value));
|
||||
Assert.True(double.IsFinite(cvi2.Last.Value));
|
||||
Assert.True(double.IsFinite(cvi3.Last.Value));
|
||||
// Different parameters should produce different values
|
||||
Assert.NotEqual(cvi1.Last.Value, cvi2.Last.Value);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void TValueUpdate_TreatsValueAsRange()
|
||||
{
|
||||
var cvi = new Cvi(5, 5);
|
||||
|
||||
// Feed pre-calculated range values via TValue
|
||||
for (int i = 0; i < 20; i++)
|
||||
{
|
||||
var result = cvi.Update(new TValue(DateTime.UtcNow.AddMinutes(i), 5.0 + i * 0.1));
|
||||
Assert.True(double.IsFinite(result.Value));
|
||||
}
|
||||
|
||||
Assert.True(cvi.IsHot);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Chainability_Works()
|
||||
{
|
||||
var cvi = new Cvi(10, 10);
|
||||
var sma = new Sma(5);
|
||||
var bars = GenerateTestData(100);
|
||||
|
||||
for (int i = 0; i < bars.Count; i++)
|
||||
{
|
||||
var cviResult = cvi.Update(bars[i]);
|
||||
sma.Update(cviResult);
|
||||
}
|
||||
|
||||
Assert.True(sma.IsHot);
|
||||
Assert.True(double.IsFinite(sma.Last.Value));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SpanBatch_MatchesOutputLength()
|
||||
{
|
||||
var values = new double[] { 1.0, 1.2, 1.1, 1.3, 1.4, 1.2, 1.5, 1.3, 1.6, 1.4 };
|
||||
var output = new double[values.Length];
|
||||
|
||||
Cvi.Batch(values, output, 3, 3);
|
||||
|
||||
Assert.Equal(values.Length, output.Length);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SpanBatch_ValidatesArguments()
|
||||
{
|
||||
var source = new double[] { 1.0, 1.2, 1.1 };
|
||||
var outputShort = new double[2];
|
||||
var outputCorrect = new double[3];
|
||||
|
||||
Assert.Throws<ArgumentException>(() => Cvi.Batch(source, outputShort, 2, 2));
|
||||
Assert.Throws<ArgumentException>(() => Cvi.Batch(source, outputCorrect, 0, 2));
|
||||
Assert.Throws<ArgumentException>(() => Cvi.Batch(source, outputCorrect, 2, 0));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,624 @@
|
||||
// OoplesFinance: CalculateChandeVolatilityIndexDynamicAverageIndicator exists but implements
|
||||
// a different algorithm (Chande Volatility Index Dynamic Average / VIDA) rather than the
|
||||
// Chaikin Volatility Index (EMA of High-Low range, then ROC). The two share the "CVI"
|
||||
// abbreviation but are mathematically distinct. Numeric equality is not expected.
|
||||
using OoplesFinance.StockIndicators;
|
||||
using OoplesFinance.StockIndicators.Models;
|
||||
using Tulip;
|
||||
|
||||
namespace QuanTAlib.Test;
|
||||
|
||||
using QuanTAlib.Tests;
|
||||
using Xunit;
|
||||
|
||||
/// <summary>
|
||||
/// Validation tests for CVI (Chaikin's Volatility).
|
||||
/// CVI measures the rate of change of EMA-smoothed high-low range.
|
||||
/// Formula: CVI = ((EMA_t - EMA_{t-rocLength}) / EMA_{t-rocLength}) × 100
|
||||
/// where EMA is applied to (High - Low) range.
|
||||
/// </summary>
|
||||
public class CviValidationTests
|
||||
{
|
||||
private static TBarSeries GenerateTestData(int count = 100)
|
||||
{
|
||||
var gbm = new GBM(seed: 42);
|
||||
return gbm.Fetch(count, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1));
|
||||
}
|
||||
|
||||
// === Mathematical Validation ===
|
||||
|
||||
/// <summary>
|
||||
/// Validates the EMA alpha formula: α = 2 / (smoothLength + 1)
|
||||
/// </summary>
|
||||
[Theory]
|
||||
[InlineData(10, 0.181818181818182)] // 2/(10+1) = 0.1818...
|
||||
[InlineData(14, 0.133333333333333)] // 2/(14+1) = 0.1333...
|
||||
[InlineData(20, 0.095238095238095)] // 2/(20+1) = 0.0952...
|
||||
public void Cvi_EmaAlpha_IsCorrect(int smoothLength, double expectedAlpha)
|
||||
{
|
||||
double alpha = 2.0 / (smoothLength + 1);
|
||||
Assert.Equal(expectedAlpha, alpha, 10);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Validates ROC formula: ((current - prior) / prior) × 100
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void Cvi_RocFormula_IsCorrect()
|
||||
{
|
||||
// Manual ROC calculation
|
||||
double currentEma = 10.0;
|
||||
double priorEma = 8.0;
|
||||
double expectedRoc = ((currentEma - priorEma) / priorEma) * 100.0;
|
||||
|
||||
Assert.Equal(25.0, expectedRoc, 10); // (10-8)/8 * 100 = 25%
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Validates that constant high-low range produces zero CVI after warmup.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void Cvi_ConstantRange_ProducesZeroCvi()
|
||||
{
|
||||
var cvi = new Cvi(10, 10);
|
||||
|
||||
// Feed constant range bars
|
||||
for (int i = 0; i < 30; i++)
|
||||
{
|
||||
var bar = new TBar(
|
||||
DateTime.UtcNow.AddMinutes(i).Ticks,
|
||||
100.0, 105.0, 95.0, 102.0, 1000.0 // Constant 10-point range
|
||||
);
|
||||
cvi.Update(bar);
|
||||
}
|
||||
|
||||
// Constant range means EMA_t = EMA_{t-rocLength}, so ROC = 0
|
||||
Assert.Equal(0.0, cvi.Last.Value, 5);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Validates expanding range produces positive CVI.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void Cvi_ExpandingRange_ProducesPositiveCvi()
|
||||
{
|
||||
var cvi = new Cvi(5, 5);
|
||||
|
||||
// Gradually expanding range
|
||||
for (int i = 0; i < 20; i++)
|
||||
{
|
||||
double range = 5 + i * 0.5; // Expanding from 5 to 14.5
|
||||
var bar = new TBar(
|
||||
DateTime.UtcNow.AddMinutes(i).Ticks,
|
||||
100.0, 100.0 + range / 2, 100.0 - range / 2, 100.0, 1000.0
|
||||
);
|
||||
cvi.Update(bar);
|
||||
}
|
||||
|
||||
// Expanding range should produce positive CVI (EMA increasing)
|
||||
Assert.True(cvi.Last.Value > 0, "Expanding range should produce positive CVI");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Validates contracting range produces negative CVI.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void Cvi_ContractingRange_ProducesNegativeCvi()
|
||||
{
|
||||
var cvi = new Cvi(5, 5);
|
||||
|
||||
// Gradually contracting range
|
||||
for (int i = 0; i < 20; i++)
|
||||
{
|
||||
double range = 20 - i * 0.5; // Contracting from 20 to 10.5
|
||||
if (range < 1)
|
||||
{
|
||||
range = 1;
|
||||
}
|
||||
var bar = new TBar(
|
||||
DateTime.UtcNow.AddMinutes(i).Ticks,
|
||||
100.0, 100.0 + range / 2, 100.0 - range / 2, 100.0, 1000.0
|
||||
);
|
||||
cvi.Update(bar);
|
||||
}
|
||||
|
||||
// Contracting range should produce negative CVI (EMA decreasing)
|
||||
Assert.True(cvi.Last.Value < 0, "Contracting range should produce negative CVI");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Validates manual CVI calculation matches implementation.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void Cvi_ManualCalculation_MatchesImplementation()
|
||||
{
|
||||
int rocLength = 3;
|
||||
int smoothLength = 3;
|
||||
double alpha = 2.0 / (smoothLength + 1); // 0.5
|
||||
|
||||
// Fixed range values
|
||||
double[] ranges = { 10.0, 12.0, 11.0, 13.0, 15.0, 14.0, 16.0, 18.0, 17.0, 19.0 };
|
||||
|
||||
// Calculate EMA manually
|
||||
double[] emas = new double[ranges.Length];
|
||||
emas[0] = ranges[0];
|
||||
for (int i = 1; i < ranges.Length; i++)
|
||||
{
|
||||
emas[i] = (ranges[i] - emas[i - 1]) * alpha + emas[i - 1];
|
||||
}
|
||||
|
||||
// Calculate ROC for last point
|
||||
int lastIdx = ranges.Length - 1;
|
||||
double oldEma = emas[lastIdx - rocLength];
|
||||
double currentEma = emas[lastIdx];
|
||||
double expectedCvi = ((currentEma - oldEma) / oldEma) * 100.0;
|
||||
|
||||
// Calculate using indicator
|
||||
var cvi = new Cvi(rocLength, smoothLength);
|
||||
for (int i = 0; i < ranges.Length; i++)
|
||||
{
|
||||
cvi.Update(new TValue(DateTime.UtcNow.AddMinutes(i), ranges[i]));
|
||||
}
|
||||
|
||||
Assert.Equal(expectedCvi, cvi.Last.Value, 8);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Validates EMA smoothing property: EMA responds to recent values more.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void Cvi_EmaSmoothingProperty_RecentValuesWeightedMore()
|
||||
{
|
||||
var cvi = new Cvi(5, 5);
|
||||
|
||||
// Feed stable values then spike
|
||||
for (int i = 0; i < 15; i++)
|
||||
{
|
||||
cvi.Update(new TValue(DateTime.UtcNow.AddMinutes(i), 10.0));
|
||||
}
|
||||
double preSpikeValue = cvi.Last.Value;
|
||||
|
||||
// Single spike
|
||||
cvi.Update(new TValue(DateTime.UtcNow.AddMinutes(15), 20.0));
|
||||
double postSpikeValue = cvi.Last.Value;
|
||||
|
||||
// EMA should respond to spike (increasing CVI since range doubled)
|
||||
Assert.True(postSpikeValue > preSpikeValue,
|
||||
"EMA should respond to recent value changes");
|
||||
}
|
||||
|
||||
// === Consistency Tests ===
|
||||
|
||||
/// <summary>
|
||||
/// Validates streaming and batch produce identical results.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void Cvi_StreamingMatchesBatch()
|
||||
{
|
||||
var bars = GenerateTestData(100);
|
||||
|
||||
// Streaming calculation
|
||||
var streamingCvi = new Cvi(10, 10);
|
||||
for (int i = 0; i < bars.Count; i++)
|
||||
{
|
||||
streamingCvi.Update(bars[i]);
|
||||
}
|
||||
|
||||
// Batch calculation
|
||||
var batchResult = Cvi.Batch(bars, 10, 10);
|
||||
|
||||
// Compare last values
|
||||
Assert.Equal(batchResult.Last.Value, streamingCvi.Last.Value, 8);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Validates TBarSeries input matches TBar streaming.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void Cvi_TBarSeriesInput_MatchesStreaming()
|
||||
{
|
||||
var bars = GenerateTestData(100);
|
||||
|
||||
// Streaming
|
||||
var streamingCvi = new Cvi(10, 10);
|
||||
for (int i = 0; i < bars.Count; i++)
|
||||
{
|
||||
streamingCvi.Update(bars[i]);
|
||||
}
|
||||
|
||||
// TBarSeries batch
|
||||
var batchCvi = new Cvi(10, 10);
|
||||
var batchResult = batchCvi.Update(bars);
|
||||
|
||||
Assert.Equal(batchResult.Last.Value, streamingCvi.Last.Value, 10);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Validates Span batch matches streaming.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void Cvi_SpanBatch_MatchesStreaming()
|
||||
{
|
||||
var bars = GenerateTestData(100);
|
||||
|
||||
// Extract ranges from bars
|
||||
var ranges = new double[bars.Count];
|
||||
for (int i = 0; i < bars.Count; i++)
|
||||
{
|
||||
ranges[i] = bars[i].High - bars[i].Low;
|
||||
}
|
||||
|
||||
// Streaming
|
||||
var streamingCvi = new Cvi(10, 10);
|
||||
for (int i = 0; i < bars.Count; i++)
|
||||
{
|
||||
streamingCvi.Update(new TValue(bars.Times[i], ranges[i]));
|
||||
}
|
||||
|
||||
// Span batch
|
||||
var output = new double[ranges.Length];
|
||||
Cvi.Batch(ranges, output, 10, 10);
|
||||
|
||||
Assert.Equal(output[^1], streamingCvi.Last.Value, 10);
|
||||
}
|
||||
|
||||
// === Parameter Sensitivity ===
|
||||
|
||||
/// <summary>
|
||||
/// Validates shorter rocLength produces more volatile CVI.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void Cvi_ShorterRocLength_MoreVolatile()
|
||||
{
|
||||
var bars = GenerateTestData(100);
|
||||
|
||||
var cviShort = new Cvi(5, 10); // rocLength = 5
|
||||
var cviLong = new Cvi(20, 10); // rocLength = 20
|
||||
|
||||
var shortResults = new List<double>();
|
||||
var longResults = new List<double>();
|
||||
|
||||
for (int i = 0; i < bars.Count; i++)
|
||||
{
|
||||
cviShort.Update(bars[i]);
|
||||
cviLong.Update(bars[i]);
|
||||
|
||||
if (cviShort.IsHot && cviLong.IsHot)
|
||||
{
|
||||
shortResults.Add(cviShort.Last.Value);
|
||||
longResults.Add(cviLong.Last.Value);
|
||||
}
|
||||
}
|
||||
|
||||
// Shorter rocLength should generally produce more volatile CVI values
|
||||
// (comparing values over fewer periods)
|
||||
Assert.True(shortResults.Count > 0, "Should have hot results");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Validates shorter smoothLength produces faster response.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void Cvi_ShorterSmoothLength_FasterResponse()
|
||||
{
|
||||
var cviShort = new Cvi(10, 5); // smoothLength = 5
|
||||
var cviLong = new Cvi(10, 20); // smoothLength = 20
|
||||
|
||||
// Feed stable values
|
||||
for (int i = 0; i < 30; i++)
|
||||
{
|
||||
cviShort.Update(new TValue(DateTime.UtcNow.AddMinutes(i), 10.0));
|
||||
cviLong.Update(new TValue(DateTime.UtcNow.AddMinutes(i), 10.0));
|
||||
}
|
||||
|
||||
double preShortValue = cviShort.Last.Value;
|
||||
double preLongValue = cviLong.Last.Value;
|
||||
|
||||
// Spike in range
|
||||
cviShort.Update(new TValue(DateTime.UtcNow.AddMinutes(30), 20.0));
|
||||
cviLong.Update(new TValue(DateTime.UtcNow.AddMinutes(30), 20.0));
|
||||
|
||||
double changeShort = Math.Abs(cviShort.Last.Value - preShortValue);
|
||||
double changeLong = Math.Abs(cviLong.Last.Value - preLongValue);
|
||||
|
||||
// Shorter smoothLength should show larger immediate change
|
||||
Assert.True(changeShort > changeLong,
|
||||
"Shorter smoothLength should respond faster to changes");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Validates different parameter combinations produce different results.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void Cvi_DifferentParameters_ProduceDifferentResults()
|
||||
{
|
||||
var bars = GenerateTestData(50);
|
||||
|
||||
var cvi1 = new Cvi(10, 10);
|
||||
var cvi2 = new Cvi(14, 10);
|
||||
var cvi3 = new Cvi(10, 14);
|
||||
|
||||
for (int i = 0; i < bars.Count; i++)
|
||||
{
|
||||
cvi1.Update(bars[i]);
|
||||
cvi2.Update(bars[i]);
|
||||
cvi3.Update(bars[i]);
|
||||
}
|
||||
|
||||
// Different parameters should produce different values
|
||||
Assert.NotEqual(cvi1.Last.Value, cvi2.Last.Value);
|
||||
Assert.NotEqual(cvi1.Last.Value, cvi3.Last.Value);
|
||||
}
|
||||
|
||||
// === Edge Cases ===
|
||||
|
||||
/// <summary>
|
||||
/// Validates handling of very small ranges.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void Cvi_VerySmallRanges_HandledCorrectly()
|
||||
{
|
||||
var cvi = new Cvi(5, 5);
|
||||
|
||||
for (int i = 0; i < 20; i++)
|
||||
{
|
||||
// Very small range (0.0001)
|
||||
var bar = new TBar(
|
||||
DateTime.UtcNow.AddMinutes(i).Ticks,
|
||||
100.0, 100.00005, 99.99995, 100.0, 1000.0
|
||||
);
|
||||
cvi.Update(bar);
|
||||
}
|
||||
|
||||
Assert.True(double.IsFinite(cvi.Last.Value));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Validates handling of very large ranges.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void Cvi_VeryLargeRanges_HandledCorrectly()
|
||||
{
|
||||
var cvi = new Cvi(5, 5);
|
||||
|
||||
for (int i = 0; i < 20; i++)
|
||||
{
|
||||
// Large range
|
||||
var bar = new TBar(
|
||||
DateTime.UtcNow.AddMinutes(i).Ticks,
|
||||
100.0, 200.0, 50.0, 150.0, 1000.0
|
||||
);
|
||||
cvi.Update(bar);
|
||||
}
|
||||
|
||||
Assert.True(double.IsFinite(cvi.Last.Value));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Validates handling of alternating large/small ranges.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void Cvi_AlternatingRanges_HandledCorrectly()
|
||||
{
|
||||
var cvi = new Cvi(5, 5);
|
||||
|
||||
for (int i = 0; i < 20; i++)
|
||||
{
|
||||
double range = (i % 2 == 0) ? 5.0 : 20.0;
|
||||
var bar = new TBar(
|
||||
DateTime.UtcNow.AddMinutes(i).Ticks,
|
||||
100.0, 100.0 + range / 2, 100.0 - range / 2, 100.0, 1000.0
|
||||
);
|
||||
cvi.Update(bar);
|
||||
}
|
||||
|
||||
Assert.True(double.IsFinite(cvi.Last.Value));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Validates warmup period calculation.
|
||||
/// </summary>
|
||||
[Theory]
|
||||
[InlineData(10, 10, 20)]
|
||||
[InlineData(14, 10, 24)]
|
||||
[InlineData(5, 20, 25)]
|
||||
public void Cvi_WarmupPeriod_IsCorrect(int rocLength, int smoothLength, int expectedWarmup)
|
||||
{
|
||||
var cvi = new Cvi(rocLength, smoothLength);
|
||||
Assert.Equal(expectedWarmup, cvi.WarmupPeriod);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Validates output range is reasonable for typical market data.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void Cvi_OutputRange_IsReasonable()
|
||||
{
|
||||
var bars = GenerateTestData(100);
|
||||
|
||||
var cvi = new Cvi(10, 10);
|
||||
for (int i = 0; i < bars.Count; i++)
|
||||
{
|
||||
cvi.Update(bars[i]);
|
||||
}
|
||||
|
||||
// CVI is a percentage ROC, typically between -100% and +100% for normal markets
|
||||
// Extreme values possible but rare
|
||||
Assert.True(cvi.Last.Value > -500, "CVI should be > -500%");
|
||||
Assert.True(cvi.Last.Value < 500, "CVI should be < +500%");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Validates CVI sign indicates volatility direction.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void Cvi_Sign_IndicatesVolatilityDirection()
|
||||
{
|
||||
// Test expanding volatility
|
||||
var cviExpanding = new Cvi(5, 5);
|
||||
for (int i = 0; i < 15; i++)
|
||||
{
|
||||
double range = 5 + i; // Expanding
|
||||
cviExpanding.Update(new TValue(DateTime.UtcNow.AddMinutes(i), range));
|
||||
}
|
||||
|
||||
// Test contracting volatility
|
||||
var cviContracting = new Cvi(5, 5);
|
||||
for (int i = 0; i < 15; i++)
|
||||
{
|
||||
double range = 20 - i; // Contracting
|
||||
if (range < 1)
|
||||
{
|
||||
range = 1;
|
||||
}
|
||||
cviContracting.Update(new TValue(DateTime.UtcNow.AddMinutes(i), range));
|
||||
}
|
||||
|
||||
Assert.True(cviExpanding.Last.Value > 0, "Expanding volatility should produce positive CVI");
|
||||
Assert.True(cviContracting.Last.Value < 0, "Contracting volatility should produce negative CVI");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Validates bar correction works correctly.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void Cvi_BarCorrection_WorksCorrectly()
|
||||
{
|
||||
var cvi = new Cvi(5, 5);
|
||||
var bars = GenerateTestData(20);
|
||||
|
||||
// Feed initial bars
|
||||
for (int i = 0; i < 15; i++)
|
||||
{
|
||||
cvi.Update(bars[i], isNew: true);
|
||||
}
|
||||
|
||||
// Add new bar
|
||||
cvi.Update(bars[15], isNew: true);
|
||||
double afterNew = cvi.Last.Value;
|
||||
|
||||
// Correct with different range
|
||||
var correctedBar = new TBar(
|
||||
bars[15].Time,
|
||||
100, 200, 50, 150, 1000 // Very different range
|
||||
);
|
||||
cvi.Update(correctedBar, isNew: false);
|
||||
double afterCorrection = cvi.Last.Value;
|
||||
|
||||
// Restore original
|
||||
cvi.Update(bars[15], isNew: false);
|
||||
double afterRestore = cvi.Last.Value;
|
||||
|
||||
Assert.NotEqual(afterNew, afterCorrection);
|
||||
Assert.Equal(afterNew, afterRestore, 10);
|
||||
}
|
||||
|
||||
// === Tulip Cross-Validation ===
|
||||
|
||||
/// <summary>
|
||||
/// Structural validation against Tulip <c>cvi</c> indicator.
|
||||
/// Algorithm variant: Tulip <c>cvi</c> uses a single <c>period</c> for both the EMA
|
||||
/// smoothing window and the ROC lookback, while QuanTAlib uses separate
|
||||
/// <c>rocLength</c> and <c>smoothLength</c> parameters.
|
||||
/// Direct numeric equality is not asserted; test documents the difference and
|
||||
/// verifies both implementations produce finite, bounded output on the same data.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void Cvi_Tulip_StructuralVariant_BothFinite()
|
||||
{
|
||||
const int period = 10;
|
||||
var bars = GenerateTestData(200);
|
||||
double[] highData = new double[bars.Count];
|
||||
double[] lowData = new double[bars.Count];
|
||||
for (int i = 0; i < bars.Count; i++)
|
||||
{
|
||||
highData[i] = bars[i].High;
|
||||
lowData[i] = bars[i].Low;
|
||||
}
|
||||
|
||||
// QuanTAlib CVI — rocLength=period, smoothLength=period (closest equivalent)
|
||||
_ = Cvi.Batch(bars, rocLength: period, smoothLength: period);
|
||||
|
||||
// Tulip cvi — single period covers both EMA smoothing and ROC lookback
|
||||
var tulipIndicator = Tulip.Indicators.cvi;
|
||||
double[][] inputs = { highData, lowData };
|
||||
double[] options = { period };
|
||||
int lookback = tulipIndicator.Start(options);
|
||||
double[][] outputs = { new double[highData.Length - lookback] };
|
||||
tulipIndicator.Run(inputs, options, outputs);
|
||||
double[] tResult = outputs[0];
|
||||
|
||||
// Structural check: both produce finite output (algorithm variants differ in seeding)
|
||||
Assert.True(tResult.Length > 0, "Tulip cvi must produce output");
|
||||
foreach (double v in tResult)
|
||||
{
|
||||
Assert.True(double.IsFinite(v), $"Tulip cvi produced non-finite value: {v}");
|
||||
}
|
||||
|
||||
// QuanTAlib IsHot lives on the indicator, not on TValue
|
||||
var cviIndicator = new Cvi(rocLength: period, smoothLength: period);
|
||||
foreach (var bar in bars) { cviIndicator.Update(bar); }
|
||||
Assert.True(cviIndicator.IsHot, "QuanTAlib Cvi must be hot after sufficient bars");
|
||||
}
|
||||
|
||||
// ── Cross-library: OoplesFinance ────────────────────────────────────
|
||||
|
||||
/// <summary>
|
||||
/// Structural validation against Ooples <c>CalculateChandeVolatilityIndexDynamicAverageIndicator</c>.
|
||||
/// NOTE: Ooples "CVI" is the Chande Volatility Index Dynamic Average (VIDA) — an adaptive
|
||||
/// moving average that uses CVI as its volatility measure. QuanTAlib CVI is Chaikin's
|
||||
/// Volatility Index: EMA(High-Low range) rate-of-change over rocLength bars. These are
|
||||
/// different algorithms sharing the "CVI" abbreviation. Numeric equality is not expected.
|
||||
/// Both must produce finite output on the same OHLCV data.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void Cvi_OoplesStructuralVariant_BothFinite()
|
||||
{
|
||||
const int length = 10;
|
||||
var bars = GenerateTestData(200);
|
||||
|
||||
var ooplesData = new List<TickerData>();
|
||||
foreach (var bar in bars)
|
||||
{
|
||||
ooplesData.Add(new TickerData
|
||||
{
|
||||
Date = new DateTime(bar.Time, DateTimeKind.Utc),
|
||||
Open = bar.Open,
|
||||
High = bar.High,
|
||||
Low = bar.Low,
|
||||
Close = bar.Close,
|
||||
Volume = bar.Volume
|
||||
});
|
||||
}
|
||||
|
||||
var stockData = new StockData(ooplesData);
|
||||
var oResult = stockData.CalculateChandeVolatilityIndexDynamicAverageIndicator(length: length);
|
||||
var oValues = oResult.OutputValues.Values.First();
|
||||
|
||||
var cvi = new Cvi(rocLength: length, smoothLength: length);
|
||||
foreach (var bar in bars) { cvi.Update(bar); }
|
||||
|
||||
int finiteCount = 0;
|
||||
int warmup = length * 2;
|
||||
for (int i = warmup; i < Math.Min(oValues.Count, bars.Count); i++)
|
||||
{
|
||||
if (double.IsFinite(oValues[i])) { finiteCount++; }
|
||||
}
|
||||
|
||||
Assert.True(oValues.Count > 0, "Ooples CVI (VIDA) must produce output");
|
||||
Assert.True(finiteCount > 50, $"Expected >50 finite Ooples CVI values, got {finiteCount}");
|
||||
Assert.True(cvi.IsHot, "QuanTAlib CVI must be hot after 200 bars");
|
||||
}
|
||||
|
||||
// === Helper Methods ===
|
||||
|
||||
private static double Variance(List<double> values)
|
||||
{
|
||||
if (values.Count == 0)
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
double mean = values.Average();
|
||||
return values.Average(v => Math.Pow(v - mean, 2));
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user