mirror of
https://github.com/mihakralj/QuanTAlib.git
synced 2026-08-16 09:38:05 +00:00
Add Close-to-Close Volatility (CCV) implementation and validation tests
- Implemented CCV class for calculating annualized log return volatility using SMA, EMA, and WMA smoothing methods. - Added comprehensive unit tests for CCV to validate mathematical correctness, consistency across methods, and edge cases. - Created documentation for CCV detailing its mathematical foundation, smoothing methods, and performance metrics.
This commit is contained in:
@@ -0,0 +1,225 @@
|
||||
namespace QuanTAlib.Tests;
|
||||
using Xunit;
|
||||
|
||||
/// <summary>
|
||||
/// Validation tests for BBWP (Bollinger Band Width Percentile).
|
||||
/// BBWP is a proprietary indicator, so we validate against internal consistency
|
||||
/// and mathematical properties rather than external libraries.
|
||||
/// </summary>
|
||||
public class BbwpValidationTests
|
||||
{
|
||||
private static TBarSeries GenerateTestData(int count = 500)
|
||||
{
|
||||
var gbm = new GBM(seed: 42);
|
||||
return gbm.Fetch(count, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void BBWP_OutputRange_AlwaysValid()
|
||||
{
|
||||
var bars = GenerateTestData(500);
|
||||
var bbwp = new Bbwp(20, 2.0, 100);
|
||||
|
||||
for (int i = 0; i < bars.Count; i++)
|
||||
{
|
||||
var result = bbwp.Update(new TValue(bars.Times[i], bars.CloseValues[i]));
|
||||
Assert.True(result.Value >= 0.0, $"BBWP at {i} should be >= 0, got {result.Value}");
|
||||
Assert.True(result.Value <= 1.0, $"BBWP at {i} should be <= 1, got {result.Value}");
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void BBWP_StreamingVsBatch_Match()
|
||||
{
|
||||
var bars = GenerateTestData(200);
|
||||
var times = bars.Times;
|
||||
var close = bars.CloseValues;
|
||||
|
||||
// Streaming calculation
|
||||
var bbwpStream = new Bbwp(10, 2.0, 50);
|
||||
var streamResults = new List<double>();
|
||||
for (int i = 0; i < bars.Count; i++)
|
||||
{
|
||||
var result = bbwpStream.Update(new TValue(times[i], close[i]));
|
||||
streamResults.Add(result.Value);
|
||||
}
|
||||
|
||||
// Batch calculation
|
||||
var ts = new TSeries();
|
||||
for (int i = 0; i < bars.Count; i++)
|
||||
{
|
||||
ts.Add(new TValue(times[i], close[i]));
|
||||
}
|
||||
var batchResults = Bbwp.Calculate(ts, 10, 2.0, 50);
|
||||
|
||||
// Compare results (should be identical)
|
||||
for (int i = 0; i < bars.Count; i++)
|
||||
{
|
||||
Assert.Equal(streamResults[i], batchResults.Values[i], 1e-10);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void BBWP_DifferentPeriods_ProduceValidResults()
|
||||
{
|
||||
var bars = GenerateTestData(300);
|
||||
int[] periods = { 5, 10, 20, 50 };
|
||||
|
||||
foreach (int period in periods)
|
||||
{
|
||||
var bbwp = new Bbwp(period, 2.0, 100);
|
||||
|
||||
for (int i = 0; i < bars.Count; i++)
|
||||
{
|
||||
var result = bbwp.Update(new TValue(bars.Times[i], bars.CloseValues[i]));
|
||||
Assert.True(double.IsFinite(result.Value), $"Period {period} at {i} should be finite");
|
||||
Assert.True(result.Value >= 0.0 && result.Value <= 1.0, $"Period {period} at {i} should be in [0,1]");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void BBWP_DifferentLookbacks_ProduceValidResults()
|
||||
{
|
||||
var bars = GenerateTestData(300);
|
||||
int[] lookbacks = { 20, 50, 100, 200 };
|
||||
|
||||
foreach (int lookback in lookbacks)
|
||||
{
|
||||
var bbwp = new Bbwp(20, 2.0, lookback);
|
||||
|
||||
for (int i = 0; i < bars.Count; i++)
|
||||
{
|
||||
var result = bbwp.Update(new TValue(bars.Times[i], bars.CloseValues[i]));
|
||||
Assert.True(double.IsFinite(result.Value), $"Lookback {lookback} at {i} should be finite");
|
||||
Assert.True(result.Value >= 0.0 && result.Value <= 1.0, $"Lookback {lookback} at {i} should be in [0,1]");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void BBWP_DifferentMultipliers_ProduceValidResults()
|
||||
{
|
||||
var bars = GenerateTestData(200);
|
||||
double[] multipliers = { 1.0, 1.5, 2.0, 2.5, 3.0 };
|
||||
|
||||
foreach (double mult in multipliers)
|
||||
{
|
||||
var bbwp = new Bbwp(20, mult, 100);
|
||||
|
||||
for (int i = 0; i < bars.Count; i++)
|
||||
{
|
||||
var result = bbwp.Update(new TValue(bars.Times[i], bars.CloseValues[i]));
|
||||
Assert.True(double.IsFinite(result.Value), $"Multiplier {mult} at {i} should be finite");
|
||||
Assert.True(result.Value >= 0.0 && result.Value <= 1.0, $"Multiplier {mult} at {i} should be in [0,1]");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void BBWP_ConstantInput_ProducesZeroPercentile()
|
||||
{
|
||||
var bbwp = new Bbwp(10, 2.0, 50);
|
||||
|
||||
// Feed constant values - BBW will be 0, and percentile of 0 among 0s is 0
|
||||
for (int i = 0; i < 100; i++)
|
||||
{
|
||||
var result = bbwp.Update(new TValue(DateTime.UtcNow.Ticks + i, 100.0));
|
||||
Assert.True(double.IsFinite(result.Value));
|
||||
Assert.True(result.Value >= 0.0 && result.Value <= 1.0);
|
||||
}
|
||||
|
||||
// With constant input, BBW=0 always, so percentile should be 0 (nothing below 0)
|
||||
Assert.Equal(0.0, bbwp.Last.Value, 1e-10);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void BBWP_HighVolatilitySpike_ProducesHighPercentile()
|
||||
{
|
||||
var bbwp = new Bbwp(5, 2.0, 20);
|
||||
|
||||
// Feed low volatility data first
|
||||
for (int i = 0; i < 25; i++)
|
||||
{
|
||||
bbwp.Update(new TValue(DateTime.UtcNow.Ticks + i, 100.0 + (i % 2) * 0.1));
|
||||
}
|
||||
|
||||
// Then introduce a high volatility spike
|
||||
bbwp.Update(new TValue(DateTime.UtcNow.Ticks + 25, 100.0));
|
||||
bbwp.Update(new TValue(DateTime.UtcNow.Ticks + 26, 110.0)); // Big move
|
||||
bbwp.Update(new TValue(DateTime.UtcNow.Ticks + 27, 105.0));
|
||||
|
||||
// After high volatility, percentile should be elevated
|
||||
Assert.True(bbwp.Last.Value > 0.3, $"High volatility should produce elevated percentile, got {bbwp.Last.Value}");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void BBWP_PercentileDistribution_Reasonable()
|
||||
{
|
||||
var bars = GenerateTestData(500);
|
||||
var bbwp = new Bbwp(20, 2.0, 100);
|
||||
|
||||
var results = new List<double>();
|
||||
for (int i = 0; i < bars.Count; i++)
|
||||
{
|
||||
var result = bbwp.Update(new TValue(bars.Times[i], bars.CloseValues[i]));
|
||||
if (i >= 120) // After warmup
|
||||
{
|
||||
results.Add(result.Value);
|
||||
}
|
||||
}
|
||||
|
||||
// Percentile values should be distributed - check quartiles
|
||||
results.Sort();
|
||||
int q1Idx = results.Count / 4;
|
||||
int q3Idx = 3 * results.Count / 4;
|
||||
|
||||
double q1 = results[q1Idx];
|
||||
double q3 = results[q3Idx];
|
||||
|
||||
// Should have meaningful spread
|
||||
Assert.True(q3 - q1 > 0.1, $"Percentile spread should be meaningful, Q1={q1:F3}, Q3={q3:F3}");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void BBWP_BarCorrection_Works()
|
||||
{
|
||||
var bbwp = new Bbwp(10, 2.0, 30);
|
||||
var bars = GenerateTestData(50);
|
||||
|
||||
// Process all bars
|
||||
for (int i = 0; i < bars.Count; i++)
|
||||
{
|
||||
bbwp.Update(new TValue(bars.Times[i], bars.CloseValues[i]), isNew: true);
|
||||
}
|
||||
double originalValue = bbwp.Last.Value;
|
||||
|
||||
// Correct the last bar with different value
|
||||
bbwp.Update(new TValue(bars.Times[bars.Count - 1], bars.CloseValues[bars.Count - 1] * 2), isNew: false);
|
||||
|
||||
// Restore original value
|
||||
var restored = bbwp.Update(new TValue(bars.Times[bars.Count - 1], bars.CloseValues[bars.Count - 1]), isNew: false);
|
||||
|
||||
Assert.Equal(originalValue, restored.Value, 1e-10);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void BBWP_SpanBatch_MatchesStreaming()
|
||||
{
|
||||
var bars = GenerateTestData(100);
|
||||
var close = bars.CloseValues.ToArray();
|
||||
|
||||
// Streaming
|
||||
var bbwpStream = new Bbwp(10, 2.0, 30);
|
||||
for (int i = 0; i < close.Length; i++)
|
||||
{
|
||||
bbwpStream.Update(new TValue(DateTime.UtcNow.Ticks + i, close[i]));
|
||||
}
|
||||
|
||||
// Batch via span
|
||||
var output = new double[close.Length];
|
||||
Bbwp.Batch(close, output, 10, 2.0, 30);
|
||||
|
||||
Assert.Equal(bbwpStream.Last.Value, output[output.Length - 1], 1e-10);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user