Files
Miha Kralj 060649192f 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
2026-03-12 12:34:16 -07:00

305 lines
9.6 KiB
C#
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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);
}
}