mirror of
https://github.com/mihakralj/QuanTAlib.git
synced 2026-07-31 10:57:43 +00:00
060649192f
- 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
305 lines
9.6 KiB
C#
305 lines
9.6 KiB
C#
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);
|
||
}
|
||
}
|