docs: remove C# Implementation Considerations sections, clean up temp scripts, reorganize test files

- Remove 'C# Implementation Considerations' sections from 34 indicator .md files
- Delete 29 temp PowerShell scripts (_fix_mojibake.ps1, _hex_scan.ps1, etc.)
- Move test files into tests/ subdirectories for consistent project structure
- Add trader-focused bullet points to indicator documentation
This commit is contained in:
Miha Kralj
2026-03-12 12:34:16 -07:00
parent 8937b0c0fa
commit 060649192f
1149 changed files with 1780 additions and 3316 deletions
@@ -0,0 +1,215 @@
using TradingPlatform.BusinessLayer;
using QuanTAlib;
namespace QuanTAlib.Tests;
public class CcvIndicatorTests
{
[Fact]
public void CcvIndicator_Constructor_SetsDefaults()
{
var indicator = new CcvIndicator();
Assert.Equal(20, indicator.Period);
Assert.Equal(1, indicator.Method);
Assert.Equal(SourceType.Close, indicator.Source);
Assert.True(indicator.ShowColdValues);
Assert.Equal("CCV - Close-to-Close Volatility", indicator.Name);
Assert.True(indicator.SeparateWindow);
Assert.True(indicator.OnBackGround);
}
[Fact]
public void CcvIndicator_ShortName_IncludesParameters()
{
var indicator = new CcvIndicator { Period = 14, Method = 2 };
Assert.Contains("CCV", indicator.ShortName, StringComparison.Ordinal);
Assert.Contains("14", indicator.ShortName, StringComparison.Ordinal);
Assert.Contains("2", indicator.ShortName, StringComparison.Ordinal);
}
[Fact]
public void CcvIndicator_MinHistoryDepths_EqualsZero()
{
var indicator = new CcvIndicator();
Assert.Equal(0, CcvIndicator.MinHistoryDepths);
Assert.Equal(0, ((IWatchlistIndicator)indicator).MinHistoryDepths);
}
[Fact]
public void CcvIndicator_Initialize_CreatesInternalCcv()
{
var indicator = new CcvIndicator();
// Initialize should not throw
indicator.Initialize();
// After init, line series should exist
Assert.Single(indicator.LinesSeries);
}
[Fact]
public void CcvIndicator_ProcessUpdate_HistoricalBar_ComputesValue()
{
var indicator = new CcvIndicator { Period = 5 };
indicator.Initialize();
// Add historical data with volatility
var now = DateTime.UtcNow;
for (int i = 0; i < 30; i++)
{
double basePrice = 100 + i * 2 + (i % 2 == 0 ? 5 : -5); // Add some volatility
indicator.HistoricalData.AddBar(now.AddMinutes(i), basePrice, basePrice + 5, basePrice - 5, basePrice + 2, 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));
Assert.True(val >= 0); // CCV should be non-negative
}
[Fact]
public void CcvIndicator_ProcessUpdate_NewBar_ComputesValue()
{
var indicator = new CcvIndicator { Period = 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
indicator.HistoricalData.AddBar(now.AddMinutes(30), 120, 128, 115, 125, 1500);
indicator.ProcessUpdate(new UpdateArgs(UpdateReason.NewBar));
Assert.Equal(2, indicator.LinesSeries[0].Count);
}
[Fact]
public void CcvIndicator_DifferentPeriods_Work()
{
int[] periods = { 5, 10, 20, 50 };
foreach (var period in periods)
{
var indicator = new CcvIndicator { Period = period };
indicator.Initialize();
var now = DateTime.UtcNow;
for (int i = 0; i < 60; i++)
{
double basePrice = 100 + i + (i % 3 == 0 ? 10 : -5); // Add volatility
indicator.HistoricalData.AddBar(now.AddMinutes(i), basePrice, basePrice + 5, basePrice - 5, basePrice + 2, 1000);
indicator.ProcessUpdate(new UpdateArgs(UpdateReason.HistoricalBar));
}
double val = indicator.LinesSeries[0].GetValue(0);
Assert.True(double.IsFinite(val), $"Period {period} should produce finite value");
Assert.True(val >= 0, $"Period {period} should produce non-negative CCV");
}
}
[Fact]
public void CcvIndicator_DifferentMethods_Work()
{
int[] methods = { 1, 2, 3 }; // SMA, EMA, WMA
foreach (var method in methods)
{
var indicator = new CcvIndicator { Method = method };
indicator.Initialize();
var now = DateTime.UtcNow;
for (int i = 0; i < 50; 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));
}
double val = indicator.LinesSeries[0].GetValue(0);
Assert.True(double.IsFinite(val), $"Method {method} should produce finite value");
Assert.True(val >= 0, $"Method {method} should produce non-negative CCV");
}
}
[Fact]
public void CcvIndicator_DifferentSourceTypes_Work()
{
SourceType[] sources = { SourceType.Close, SourceType.High, SourceType.Low, SourceType.HL2, SourceType.HLC3 };
foreach (var source in sources)
{
var indicator = new CcvIndicator { Source = source };
indicator.Initialize();
var now = DateTime.UtcNow;
for (int i = 0; i < 40; 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));
}
double val = indicator.LinesSeries[0].GetValue(0);
Assert.True(double.IsFinite(val), $"Source {source} should produce finite value");
}
}
[Fact]
public void CcvIndicator_Period_CanBeChanged()
{
var indicator = new CcvIndicator();
Assert.Equal(20, indicator.Period);
indicator.Period = 14;
Assert.Equal(14, indicator.Period);
indicator.Period = 50;
Assert.Equal(50, indicator.Period);
}
[Fact]
public void CcvIndicator_Method_CanBeChanged()
{
var indicator = new CcvIndicator();
Assert.Equal(1, indicator.Method);
indicator.Method = 2;
Assert.Equal(2, indicator.Method);
indicator.Method = 3;
Assert.Equal(3, indicator.Method);
}
[Fact]
public void CcvIndicator_ShowColdValues_CanBeToggled()
{
var indicator = new CcvIndicator();
Assert.True(indicator.ShowColdValues);
indicator.ShowColdValues = false;
Assert.False(indicator.ShowColdValues);
indicator.ShowColdValues = true;
Assert.True(indicator.ShowColdValues);
}
[Fact]
public void CcvIndicator_SourceCodeLink_IsValid()
{
var indicator = new CcvIndicator();
Assert.Contains("github.com", indicator.SourceCodeLink, StringComparison.Ordinal);
Assert.Contains("Ccv.Quantower.cs", indicator.SourceCodeLink, StringComparison.Ordinal);
}
}
+436
View File
@@ -0,0 +1,436 @@
namespace QuanTAlib.Tests;
using Xunit;
public class CcvTests
{
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 Ccv(0));
Assert.Throws<ArgumentException>(() => new Ccv(-1));
Assert.Throws<ArgumentException>(() => new Ccv(20, 0));
Assert.Throws<ArgumentException>(() => new Ccv(20, 4));
Assert.Throws<ArgumentException>(() => new Ccv(20, -1));
var valid = new Ccv(10, 1);
Assert.Equal(10, valid.Period);
Assert.Equal(1, valid.Method);
}
[Fact]
public void WarmupPeriod_IsCorrect()
{
var ccv = new Ccv(20);
Assert.Equal(21, ccv.WarmupPeriod); // period + 1
Assert.True(ccv.WarmupPeriod > 0);
}
[Fact]
public void Properties_Accessible()
{
var ccv = new Ccv(20, 2);
Assert.Equal(20, ccv.Period);
Assert.Equal(2, ccv.Method);
Assert.Equal("Ccv(20,2)", ccv.Name);
}
[Fact]
public void BasicCalculation_DoesNotCrash()
{
var ccv = new Ccv(5);
var bars = GenerateTestData(100);
var times = bars.Times;
var close = bars.CloseValues;
for (int i = 0; i < bars.Count; i++)
{
var result = ccv.Update(new TValue(times[i], close[i]));
Assert.True(double.IsFinite(result.Value));
}
}
[Fact]
public void Calc_ReturnsValue()
{
var ccv = new Ccv(10);
for (int i = 0; i < 15; i++)
{
var result = ccv.Update(new TValue(DateTime.UtcNow, 100 + i));
Assert.True(double.IsFinite(result.Value));
}
Assert.True(ccv.IsHot);
}
[Fact]
public void Calc_IsNew_AcceptsParameter()
{
var ccv = new Ccv(10);
var result1 = ccv.Update(new TValue(DateTime.UtcNow, 100), isNew: true);
var result2 = ccv.Update(new TValue(DateTime.UtcNow, 101), isNew: true);
var result3 = ccv.Update(new TValue(DateTime.UtcNow, 102), 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 ccv = new Ccv(5);
for (int i = 0; i < 10; i++)
{
ccv.Update(new TValue(DateTime.UtcNow, 100 + i), isNew: true);
}
var baseline = ccv.Update(new TValue(DateTime.UtcNow, 110), isNew: true);
var updated = ccv.Update(new TValue(DateTime.UtcNow, 150), isNew: false);
Assert.NotEqual(baseline.Value, updated.Value);
}
[Fact]
public void IsHot_BecomesTrueAfterWarmup()
{
int period = 10;
var ccv = new Ccv(period);
for (int i = 0; i < period - 1; i++)
{
ccv.Update(new TValue(DateTime.UtcNow, 100 + i));
Assert.False(ccv.IsHot);
}
ccv.Update(new TValue(DateTime.UtcNow, 110));
Assert.True(ccv.IsHot);
}
[Fact]
public void Reset_Works()
{
var ccv = new Ccv(10);
for (int i = 0; i < 15; i++)
{
ccv.Update(new TValue(DateTime.UtcNow, 100 + i));
}
Assert.True(ccv.IsHot);
ccv.Reset();
Assert.False(ccv.IsHot);
}
[Fact]
public void SingleValue_ReturnsZero()
{
var ccv = new Ccv(5);
var result = ccv.Update(new TValue(DateTime.UtcNow, 100));
// First value has no return to calculate, should be 0
Assert.Equal(0.0, result.Value);
}
[Fact]
public void IterativeCorrections_RestoreToOriginalState()
{
var ccv = new Ccv(20);
var bars = GenerateTestData(50);
var times = bars.Times;
var close = bars.CloseValues;
TValue lastValue = default;
for (int i = 0; i < bars.Count; i++)
{
lastValue = ccv.Update(new TValue(times[i], close[i]), isNew: true);
}
double originalValue = lastValue.Value;
var correctedValue = ccv.Update(new TValue(DateTime.UtcNow, 999.99), isNew: false);
Assert.NotEqual(originalValue, correctedValue.Value);
var restoredValue = ccv.Update(new TValue(lastValue.Time, close[bars.Count - 1]), isNew: false);
Assert.Equal(originalValue, restoredValue.Value, 1e-9);
}
[Fact]
public void IsNew_Consistency()
{
var ccv = new Ccv(10);
for (int i = 0; i < 10; i++)
{
ccv.Update(new TValue(DateTime.UtcNow, 100 + i), isNew: true);
}
var result1 = ccv.Update(new TValue(DateTime.UtcNow, 110), isNew: true);
_ = ccv.Update(new TValue(DateTime.UtcNow, 115), isNew: false);
var result3 = ccv.Update(new TValue(DateTime.UtcNow, 110), isNew: false);
Assert.Equal(result1.Value, result3.Value, Tolerance);
}
[Fact]
public void NaN_Input_UsesLastValidValue()
{
var ccv = new Ccv(5);
for (int i = 0; i < 10; i++)
{
ccv.Update(new TValue(DateTime.UtcNow, 100 + i));
}
var resultNan = ccv.Update(new TValue(DateTime.UtcNow, double.NaN));
Assert.True(double.IsFinite(resultNan.Value));
}
[Fact]
public void Infinity_Input_UsesLastValidValue()
{
var ccv = new Ccv(5);
for (int i = 0; i < 10; i++)
{
ccv.Update(new TValue(DateTime.UtcNow, 100 + i));
}
var resultInf = ccv.Update(new TValue(DateTime.UtcNow, double.PositiveInfinity));
Assert.True(double.IsFinite(resultInf.Value));
}
[Fact]
public void LargeDataset_Performance()
{
var ccv = new Ccv(50);
var bars = GenerateTestData(5000);
var times = bars.Times;
var close = bars.CloseValues;
for (int i = 0; i < bars.Count; i++)
{
var result = ccv.Update(new TValue(times[i], close[i]));
Assert.True(double.IsFinite(result.Value));
}
}
[Fact]
public void TSeries_Update_MatchesStreaming()
{
int period = 20;
var ccvStream = new Ccv(period);
var ccvBatch = new Ccv(period);
var bars = GenerateTestData(100);
var times = bars.Times;
var close = bars.CloseValues;
for (int i = 0; i < bars.Count; i++)
{
ccvStream.Update(new TValue(times[i], close[i]));
}
var ts = new TSeries();
for (int i = 0; i < bars.Count; i++)
{
ts.Add(new TValue(times[i], close[i]));
}
var result = ccvBatch.Update(ts);
Assert.Equal(ccvStream.Last.Value, result[result.Count - 1].Value, 1e-9);
}
[Fact]
public void BatchCalc_MatchesIterativeCalc()
{
var ccv = new Ccv(20);
var bars = GenerateTestData(200);
var times = bars.Times;
var close = bars.CloseValues;
for (int i = 0; i < bars.Count; i++)
{
ccv.Update(new TValue(times[i], close[i]));
}
var iterativeResult = ccv.Last.Value;
var ts = new TSeries();
for (int i = 0; i < bars.Count; i++)
{
ts.Add(new TValue(times[i], close[i]));
}
var batchResult = Ccv.Batch(ts, 20);
Assert.Equal(iterativeResult, batchResult[batchResult.Count - 1].Value, 1e-8);
}
[Fact]
public void StaticBatch_Works()
{
var bars = GenerateTestData(100);
var times = bars.Times;
var close = bars.CloseValues;
var ts = new TSeries();
for (int i = 0; i < bars.Count; i++)
{
ts.Add(new TValue(times[i], close[i]));
}
var result = Ccv.Batch(ts, 20);
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>(() => Ccv.Batch(ts, 0));
Assert.Throws<ArgumentException>(() => Ccv.Batch(ts, -1));
Assert.Throws<ArgumentException>(() => Ccv.Batch(ts, 5, 0));
Assert.Throws<ArgumentException>(() => Ccv.Batch(ts, 5, 4));
}
[Fact]
public void Batch_NaN_Safe()
{
var values = new double[] { 100, 101, 102, double.NaN, 104, 105 };
var output = new double[values.Length];
Ccv.Batch(values, output, 3);
Assert.True(output.Length == 6);
}
[Fact]
public void ConstantPrices_ZeroVolatility()
{
var ccv = new Ccv(10);
for (int i = 0; i < 20; i++)
{
ccv.Update(new TValue(DateTime.UtcNow.AddMinutes(i), 100.0));
}
// Constant prices should have near-zero volatility
Assert.True(ccv.Last.Value < 0.01, "Constant prices should have near-zero volatility");
}
[Fact]
public void HighVolatility_ProducesHigherValue()
{
var ccvStable = new Ccv(10);
var ccvVolatile = new Ccv(10);
// Stable prices (small changes)
for (int i = 0; i < 20; i++)
{
ccvStable.Update(new TValue(DateTime.UtcNow.AddMinutes(i), 100 + i * 0.01));
}
// Volatile prices (alternating)
for (int i = 0; i < 20; i++)
{
double volatilePrice = 100 + (i % 2 == 0 ? 5 : -5);
ccvVolatile.Update(new TValue(DateTime.UtcNow.AddMinutes(i), volatilePrice));
}
Assert.True(ccvVolatile.Last.Value > ccvStable.Last.Value,
"Higher volatility should produce higher CCV");
}
[Fact]
public void AllMethods_ProduceValidResults()
{
var bars = GenerateTestData(50);
var times = bars.Times;
var close = bars.CloseValues;
for (int method = 1; method <= 3; method++)
{
var ccv = new Ccv(10, method);
for (int i = 0; i < bars.Count; i++)
{
var result = ccv.Update(new TValue(times[i], close[i]));
Assert.True(double.IsFinite(result.Value));
Assert.True(result.Value >= 0);
}
}
}
[Fact]
public void DifferentMethods_ProduceDistinctValues()
{
var bars = GenerateTestData(50);
var times = bars.Times;
var close = bars.CloseValues;
var ccv1 = new Ccv(20, 1); // SMA
var ccv2 = new Ccv(20, 2); // EMA
var ccv3 = new Ccv(20, 3); // WMA
for (int i = 0; i < bars.Count; i++)
{
ccv1.Update(new TValue(times[i], close[i]));
ccv2.Update(new TValue(times[i], close[i]));
ccv3.Update(new TValue(times[i], close[i]));
}
Assert.True(double.IsFinite(ccv1.Last.Value));
Assert.True(double.IsFinite(ccv2.Last.Value));
Assert.True(double.IsFinite(ccv3.Last.Value));
}
[Fact]
public void AnnualizationFactor_Applied()
{
var ccv = new Ccv(10);
var bars = GenerateTestData(30);
var times = bars.Times;
var close = bars.CloseValues;
for (int i = 0; i < bars.Count; i++)
{
ccv.Update(new TValue(times[i], close[i]));
}
// Annualized volatility should be positive
Assert.True(ccv.Last.Value >= 0);
}
[Fact]
public void Chainability_Works()
{
var ccv = new Ccv(20);
var sma = new Sma(5);
var bars = GenerateTestData(100);
var times = bars.Times;
var close = bars.CloseValues;
for (int i = 0; i < bars.Count; i++)
{
var ccvResult = ccv.Update(new TValue(times[i], close[i]));
sma.Update(ccvResult);
}
Assert.True(sma.IsHot);
Assert.True(double.IsFinite(sma.Last.Value));
}
}
@@ -0,0 +1,304 @@
namespace QuanTAlib.Test;
using Xunit;
/// <summary>
/// Validation tests for CCV (Close-to-Close Volatility).
/// CCV is a standard volatility measure but with specific smoothing options.
/// These tests validate the mathematical correctness of the implementation.
/// </summary>
public class CcvValidationTests
{
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 that CCV calculates annualized log return volatility correctly.
/// Formula: σ_annual = StdDev(ln(C_t/C_{t-1})) × √252
/// </summary>
[Fact]
public void Ccv_MatchesManualLogReturnCalculation()
{
int period = 10;
var ccv = new Ccv(period, 1); // SMA method
// Use fixed prices for deterministic testing
double[] prices = { 100, 102, 101, 103, 105, 104, 106, 108, 107, 109, 110 };
// Feed all prices to the indicator (first price initializes, rest produce returns)
for (int i = 0; i < prices.Length; i++)
{
ccv.Update(new TValue(DateTime.UtcNow.AddMinutes(i), prices[i]));
}
// Calculate log returns (prices[1]/prices[0], prices[2]/prices[1], etc.)
double[] logReturns = new double[prices.Length - 1];
for (int i = 1; i < prices.Length; i++)
{
logReturns[i - 1] = Math.Log(prices[i] / prices[i - 1]);
}
// Calculate expected stddev manually for last 'period' returns
int startIdx = Math.Max(0, logReturns.Length - period);
double sum = 0;
int count = 0;
for (int i = startIdx; i < logReturns.Length; i++)
{
sum += logReturns[i];
count++;
}
double mean = sum / count;
double squaredSum = 0;
for (int i = startIdx; i < logReturns.Length; i++)
{
squaredSum += Math.Pow(logReturns[i] - mean, 2);
}
double stdDev = Math.Sqrt(squaredSum / count);
double expectedAnnualized = stdDev * Math.Sqrt(252);
// Compare (allow for floating-point tolerance - small differences expected due to
// the indicator using a rolling window vs manual batch calculation)
Assert.Equal(expectedAnnualized, ccv.Last.Value, 2);
}
/// <summary>
/// Validates the annualization factor √252 is correctly applied.
/// </summary>
[Fact]
public void Ccv_AnnualizationFactor_IsCorrect()
{
// √252 ≈ 15.8745
double expectedFactor = Math.Sqrt(252);
Assert.Equal(15.874507866387544, expectedFactor, 10);
}
/// <summary>
/// Validates that constant prices produce zero volatility.
/// </summary>
[Fact]
public void Ccv_ConstantPrices_ProducesZeroVolatility()
{
var ccv = new Ccv(10, 1);
for (int i = 0; i < 20; i++)
{
ccv.Update(new TValue(DateTime.UtcNow.AddMinutes(i), 100.0));
}
// Constant prices = zero log returns = zero stddev = zero volatility
Assert.Equal(0.0, ccv.Last.Value, 10);
}
/// <summary>
/// Validates that the EMA method (2) applies warmup compensation correctly.
/// </summary>
[Fact]
public void Ccv_EmaMethod_WarmsUpCorrectly()
{
var ccv = new Ccv(20, 2); // EMA method
var bars = GenerateTestData(50);
var times = bars.Times;
var close = bars.CloseValues;
var results = new List<double>();
for (int i = 0; i < bars.Count; i++)
{
var result = ccv.Update(new TValue(times[i], close[i]));
results.Add(result.Value);
}
// Early values should exist and be finite
Assert.All(results, r => Assert.True(double.IsFinite(r)));
// Values should generally stabilize after warmup
Assert.True(results[^1] >= 0);
}
/// <summary>
/// Validates known volatility scenario with specific returns.
/// </summary>
[Fact]
public void Ccv_KnownReturns_ProducesExpectedVolatility()
{
var ccv = new Ccv(5, 1); // SMA method, 5 periods
// Create prices that produce known log returns
// If we have returns of: 1%, 1%, 1%, 1%, 1% (all same)
// Then stddev = 0, volatility = 0
double price = 100.0;
double returnRate = 0.01; // 1% daily return
ccv.Update(new TValue(DateTime.UtcNow, price)); // First price
for (int i = 0; i < 5; i++)
{
price *= (1 + returnRate);
ccv.Update(new TValue(DateTime.UtcNow.AddMinutes(i + 1), price));
}
// Constant returns should produce near-zero volatility
// (log(1.01) is constant, so stddev ≈ 0)
Assert.True(ccv.Last.Value < 0.01, "Constant returns should have near-zero volatility");
}
/// <summary>
/// Validates that CCV responds to varying volatility correctly.
/// </summary>
[Fact]
public void Ccv_VaryingVolatility_RespondsCorrectly()
{
var ccvLow = new Ccv(10, 1);
var ccvHigh = new Ccv(10, 1);
// Low volatility: small price changes
double priceLow = 100.0;
for (int i = 0; i < 20; i++)
{
priceLow *= (1 + 0.001 * (i % 2 == 0 ? 1 : -1)); // ±0.1%
ccvLow.Update(new TValue(DateTime.UtcNow.AddMinutes(i), priceLow));
}
// High volatility: large price changes
double priceHigh = 100.0;
for (int i = 0; i < 20; i++)
{
priceHigh *= (1 + 0.05 * (i % 2 == 0 ? 1 : -1)); // ±5%
ccvHigh.Update(new TValue(DateTime.UtcNow.AddMinutes(i), priceHigh));
}
Assert.True(ccvHigh.Last.Value > ccvLow.Last.Value,
"Higher price volatility should produce higher CCV");
}
// === Consistency Tests ===
/// <summary>
/// Validates streaming and batch produce identical results.
/// </summary>
[Fact]
public void Ccv_StreamingMatchesBatch()
{
var bars = GenerateTestData(100);
var times = bars.Times;
var close = bars.CloseValues;
// Streaming calculation
var streamingCcv = new Ccv(20, 1);
for (int i = 0; i < bars.Count; i++)
{
streamingCcv.Update(new TValue(times[i], close[i]));
}
// Batch calculation
var source = new double[bars.Count];
var output = new double[bars.Count];
for (int i = 0; i < bars.Count; i++)
{
source[i] = close[i];
}
Ccv.Batch(source, output, 20, 1);
// Compare last values
Assert.Equal(output[^1], streamingCcv.Last.Value, 8);
}
/// <summary>
/// Validates all three smoothing methods produce valid results.
/// </summary>
[Theory]
[InlineData(1)] // SMA
[InlineData(2)] // EMA
[InlineData(3)] // WMA
public void Ccv_AllMethods_ProduceConsistentResults(int method)
{
var bars = GenerateTestData(100);
var times = bars.Times;
var close = bars.CloseValues;
var ccv = new Ccv(20, method);
for (int i = 0; i < bars.Count; i++)
{
var result = ccv.Update(new TValue(times[i], close[i]));
Assert.True(double.IsFinite(result.Value), $"Method {method} should produce finite values");
Assert.True(result.Value >= 0, $"Method {method} should produce non-negative values");
}
}
// === Edge Cases ===
/// <summary>
/// Validates handling of very small price changes.
/// </summary>
[Fact]
public void Ccv_SmallPriceChanges_HandledCorrectly()
{
var ccv = new Ccv(10, 1);
double price = 100.0;
for (int i = 0; i < 20; i++)
{
price += 0.0001; // Very small changes
ccv.Update(new TValue(DateTime.UtcNow.AddMinutes(i), price));
}
Assert.True(double.IsFinite(ccv.Last.Value));
Assert.True(ccv.Last.Value >= 0);
}
/// <summary>
/// Validates handling of large price swings.
/// </summary>
[Fact]
public void Ccv_LargePriceSwings_HandledCorrectly()
{
var ccv = new Ccv(10, 1);
for (int i = 0; i < 20; i++)
{
double price = 100.0 * (i % 2 == 0 ? 2.0 : 0.5); // 100% swings
ccv.Update(new TValue(DateTime.UtcNow.AddMinutes(i), price));
}
Assert.True(double.IsFinite(ccv.Last.Value));
Assert.True(ccv.Last.Value > 0, "Large swings should produce positive volatility");
}
/// <summary>
/// Validates that different periods produce different sensitivities.
/// </summary>
[Fact]
public void Ccv_DifferentPeriods_ProduceDifferentValues()
{
var bars = GenerateTestData(100);
var times = bars.Times;
var close = bars.CloseValues;
var ccv5 = new Ccv(5, 1);
var ccv20 = new Ccv(20, 1);
var ccv50 = new Ccv(50, 1);
for (int i = 0; i < bars.Count; i++)
{
ccv5.Update(new TValue(times[i], close[i]));
ccv20.Update(new TValue(times[i], close[i]));
ccv50.Update(new TValue(times[i], close[i]));
}
// All should be valid
Assert.True(double.IsFinite(ccv5.Last.Value));
Assert.True(double.IsFinite(ccv20.Last.Value));
Assert.True(double.IsFinite(ccv50.Last.Value));
// Shorter periods typically react more to recent volatility
// (but this depends on market data, so just check they're different or similar)
Assert.True(ccv5.Last.Value >= 0);
Assert.True(ccv20.Last.Value >= 0);
Assert.True(ccv50.Last.Value >= 0);
}
}