mirror of
https://github.com/mihakralj/QuanTAlib.git
synced 2026-08-22 20:48: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,308 @@
|
||||
using TradingPlatform.BusinessLayer;
|
||||
using QuanTAlib;
|
||||
|
||||
namespace QuanTAlib.Tests;
|
||||
|
||||
public class CvIndicatorTests
|
||||
{
|
||||
[Fact]
|
||||
public void CvIndicator_Constructor_SetsDefaults()
|
||||
{
|
||||
var indicator = new CvIndicator();
|
||||
|
||||
Assert.Equal(20, indicator.Period);
|
||||
Assert.Equal(0.2, indicator.Alpha);
|
||||
Assert.Equal(0.7, indicator.Beta);
|
||||
Assert.Equal(SourceType.Close, indicator.Source);
|
||||
Assert.True(indicator.ShowColdValues);
|
||||
Assert.Equal("CV - Conditional Volatility (GARCH(1,1))", indicator.Name);
|
||||
Assert.True(indicator.SeparateWindow);
|
||||
Assert.True(indicator.OnBackGround);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CvIndicator_ShortName_IncludesParameters()
|
||||
{
|
||||
var indicator = new CvIndicator { Period = 14, Alpha = 0.15, Beta = 0.75 };
|
||||
Assert.Contains("CV", indicator.ShortName, StringComparison.Ordinal);
|
||||
Assert.Contains("14", indicator.ShortName, StringComparison.Ordinal);
|
||||
Assert.Contains("0.15", indicator.ShortName, StringComparison.Ordinal);
|
||||
Assert.Contains("0.75", indicator.ShortName, StringComparison.Ordinal);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CvIndicator_MinHistoryDepths_EqualsZero()
|
||||
{
|
||||
var indicator = new CvIndicator();
|
||||
|
||||
Assert.Equal(0, CvIndicator.MinHistoryDepths);
|
||||
Assert.Equal(0, ((IWatchlistIndicator)indicator).MinHistoryDepths);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CvIndicator_Initialize_CreatesInternalCv()
|
||||
{
|
||||
var indicator = new CvIndicator();
|
||||
|
||||
// Initialize should not throw
|
||||
indicator.Initialize();
|
||||
|
||||
// After init, line series should exist
|
||||
Assert.Single(indicator.LinesSeries);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CvIndicator_ProcessUpdate_HistoricalBar_ComputesValue()
|
||||
{
|
||||
var indicator = new CvIndicator { 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); // CV should be non-negative
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CvIndicator_ProcessUpdate_NewBar_ComputesValue()
|
||||
{
|
||||
var indicator = new CvIndicator { 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 CvIndicator_DifferentPeriods_Work()
|
||||
{
|
||||
int[] periods = { 5, 10, 20, 50 };
|
||||
|
||||
foreach (var period in periods)
|
||||
{
|
||||
var indicator = new CvIndicator { 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 CV");
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CvIndicator_DifferentAlphaValues_Work()
|
||||
{
|
||||
double[] alphas = { 0.05, 0.1, 0.2, 0.3 };
|
||||
|
||||
foreach (var alpha in alphas)
|
||||
{
|
||||
var indicator = new CvIndicator { Alpha = alpha, Beta = 0.6 }; // Keep alpha + beta < 1
|
||||
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), $"Alpha {alpha} should produce finite value");
|
||||
Assert.True(val >= 0, $"Alpha {alpha} should produce non-negative CV");
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CvIndicator_DifferentBetaValues_Work()
|
||||
{
|
||||
double[] betas = { 0.5, 0.6, 0.7, 0.8 };
|
||||
|
||||
foreach (var beta in betas)
|
||||
{
|
||||
var indicator = new CvIndicator { Alpha = 0.1, Beta = beta }; // Keep alpha + beta < 1
|
||||
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), $"Beta {beta} should produce finite value");
|
||||
Assert.True(val >= 0, $"Beta {beta} should produce non-negative CV");
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CvIndicator_StationarityConstraint_AdjustsBeta()
|
||||
{
|
||||
// Test that when alpha + beta >= 1, OnInit adjusts beta
|
||||
var indicator = new CvIndicator { Alpha = 0.5, Beta = 0.6 }; // Sum = 1.1, violates constraint
|
||||
indicator.Initialize();
|
||||
|
||||
// Beta should be adjusted to maintain stationarity (0.99 - alpha)
|
||||
Assert.True(indicator.Alpha + indicator.Beta < 1.0,
|
||||
"After initialization, alpha + beta should be less than 1");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CvIndicator_DifferentSourceTypes_Work()
|
||||
{
|
||||
SourceType[] sources = { SourceType.Close, SourceType.High, SourceType.Low, SourceType.HL2, SourceType.HLC3 };
|
||||
|
||||
foreach (var source in sources)
|
||||
{
|
||||
var indicator = new CvIndicator { 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 CvIndicator_Period_CanBeChanged()
|
||||
{
|
||||
var indicator = new CvIndicator();
|
||||
Assert.Equal(20, indicator.Period);
|
||||
|
||||
indicator.Period = 14;
|
||||
Assert.Equal(14, indicator.Period);
|
||||
|
||||
indicator.Period = 50;
|
||||
Assert.Equal(50, indicator.Period);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CvIndicator_Alpha_CanBeChanged()
|
||||
{
|
||||
var indicator = new CvIndicator();
|
||||
Assert.Equal(0.2, indicator.Alpha);
|
||||
|
||||
indicator.Alpha = 0.15;
|
||||
Assert.Equal(0.15, indicator.Alpha);
|
||||
|
||||
indicator.Alpha = 0.25;
|
||||
Assert.Equal(0.25, indicator.Alpha);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CvIndicator_Beta_CanBeChanged()
|
||||
{
|
||||
var indicator = new CvIndicator();
|
||||
Assert.Equal(0.7, indicator.Beta);
|
||||
|
||||
indicator.Beta = 0.6;
|
||||
Assert.Equal(0.6, indicator.Beta);
|
||||
|
||||
indicator.Beta = 0.8;
|
||||
Assert.Equal(0.8, indicator.Beta);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CvIndicator_ShowColdValues_CanBeToggled()
|
||||
{
|
||||
var indicator = new CvIndicator();
|
||||
Assert.True(indicator.ShowColdValues);
|
||||
|
||||
indicator.ShowColdValues = false;
|
||||
Assert.False(indicator.ShowColdValues);
|
||||
|
||||
indicator.ShowColdValues = true;
|
||||
Assert.True(indicator.ShowColdValues);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CvIndicator_SourceCodeLink_IsValid()
|
||||
{
|
||||
var indicator = new CvIndicator();
|
||||
Assert.Contains("github.com", indicator.SourceCodeLink, StringComparison.Ordinal);
|
||||
Assert.Contains("Cv.Quantower.cs", indicator.SourceCodeLink, StringComparison.Ordinal);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CvIndicator_VolatilityClustering_ProducesVaryingOutput()
|
||||
{
|
||||
var indicator = new CvIndicator { Period = 10 };
|
||||
indicator.Initialize();
|
||||
|
||||
var now = DateTime.UtcNow;
|
||||
var values = new List<double>();
|
||||
|
||||
// Add data with varying volatility
|
||||
for (int i = 0; i < 50; i++)
|
||||
{
|
||||
// First 20 bars: low volatility, next 20 bars: high volatility, last 10: low again
|
||||
double volatilityFactor;
|
||||
if (i < 20)
|
||||
{
|
||||
volatilityFactor = 1.0;
|
||||
}
|
||||
else if (i < 40)
|
||||
{
|
||||
volatilityFactor = 5.0;
|
||||
}
|
||||
else
|
||||
{
|
||||
volatilityFactor = 1.0;
|
||||
}
|
||||
double basePrice = 100 + (i % 2 == 0 ? volatilityFactor : -volatilityFactor);
|
||||
indicator.HistoricalData.AddBar(now.AddMinutes(i), basePrice, basePrice + volatilityFactor, basePrice - volatilityFactor, basePrice, 1000);
|
||||
indicator.ProcessUpdate(new UpdateArgs(UpdateReason.HistoricalBar));
|
||||
|
||||
if (i >= 10) // After warmup
|
||||
{
|
||||
values.Add(indicator.LinesSeries[0].GetValue(0));
|
||||
}
|
||||
}
|
||||
|
||||
// Verify we got varying volatility values (GARCH captures clustering)
|
||||
double min = values.Min();
|
||||
double max = values.Max();
|
||||
Assert.True(max > min, "CV should vary with changing volatility patterns");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,459 @@
|
||||
namespace QuanTAlib.Tests;
|
||||
using Xunit;
|
||||
|
||||
public class CvTests
|
||||
{
|
||||
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 Cv(0));
|
||||
Assert.Throws<ArgumentException>(() => new Cv(-1));
|
||||
Assert.Throws<ArgumentException>(() => new Cv(20, 0.0)); // alpha = 0
|
||||
Assert.Throws<ArgumentException>(() => new Cv(20, 1.0)); // alpha = 1
|
||||
Assert.Throws<ArgumentException>(() => new Cv(20, 0.2, 0.0)); // beta = 0
|
||||
Assert.Throws<ArgumentException>(() => new Cv(20, 0.2, 1.0)); // beta = 1
|
||||
Assert.Throws<ArgumentException>(() => new Cv(20, 0.5, 0.6)); // alpha + beta >= 1
|
||||
|
||||
var valid = new Cv(10, 0.2, 0.7);
|
||||
Assert.Equal(10, valid.Period);
|
||||
Assert.Equal(0.2, valid.Alpha);
|
||||
Assert.Equal(0.7, valid.Beta);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void WarmupPeriod_IsCorrect()
|
||||
{
|
||||
var cv = new Cv(20);
|
||||
Assert.Equal(21, cv.WarmupPeriod); // period + 1
|
||||
Assert.True(cv.WarmupPeriod > 0);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Properties_Accessible()
|
||||
{
|
||||
var cv = new Cv(20, 0.15, 0.75);
|
||||
Assert.Equal(20, cv.Period);
|
||||
Assert.Equal(0.15, cv.Alpha);
|
||||
Assert.Equal(0.75, cv.Beta);
|
||||
Assert.Equal("Cv(20,0.15,0.75)", cv.Name);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void BasicCalculation_DoesNotCrash()
|
||||
{
|
||||
var cv = new Cv(5);
|
||||
var bars = GenerateTestData(100);
|
||||
var times = bars.Times;
|
||||
var close = bars.CloseValues;
|
||||
|
||||
for (int i = 0; i < bars.Count; i++)
|
||||
{
|
||||
var result = cv.Update(new TValue(times[i], close[i]));
|
||||
Assert.True(double.IsFinite(result.Value));
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Calc_ReturnsValue()
|
||||
{
|
||||
var cv = new Cv(10);
|
||||
|
||||
for (int i = 0; i < 15; i++)
|
||||
{
|
||||
var result = cv.Update(new TValue(DateTime.UtcNow, 100 + i));
|
||||
Assert.True(double.IsFinite(result.Value));
|
||||
}
|
||||
|
||||
Assert.True(cv.IsHot);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Calc_IsNew_AcceptsParameter()
|
||||
{
|
||||
var cv = new Cv(10);
|
||||
|
||||
var result1 = cv.Update(new TValue(DateTime.UtcNow, 100), isNew: true);
|
||||
var result2 = cv.Update(new TValue(DateTime.UtcNow, 101), isNew: true);
|
||||
var result3 = cv.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 cv = new Cv(5);
|
||||
|
||||
for (int i = 0; i < 10; i++)
|
||||
{
|
||||
cv.Update(new TValue(DateTime.UtcNow, 100 + i), isNew: true);
|
||||
}
|
||||
|
||||
var baseline = cv.Update(new TValue(DateTime.UtcNow, 110), isNew: true);
|
||||
var updated = cv.Update(new TValue(DateTime.UtcNow, 150), isNew: false);
|
||||
|
||||
Assert.NotEqual(baseline.Value, updated.Value);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void IsHot_BecomesTrueAfterWarmup()
|
||||
{
|
||||
int period = 10;
|
||||
var cv = new Cv(period);
|
||||
|
||||
for (int i = 0; i < period - 1; i++)
|
||||
{
|
||||
cv.Update(new TValue(DateTime.UtcNow, 100 + i));
|
||||
Assert.False(cv.IsHot);
|
||||
}
|
||||
|
||||
cv.Update(new TValue(DateTime.UtcNow, 110));
|
||||
Assert.True(cv.IsHot);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Reset_Works()
|
||||
{
|
||||
var cv = new Cv(10);
|
||||
|
||||
for (int i = 0; i < 15; i++)
|
||||
{
|
||||
cv.Update(new TValue(DateTime.UtcNow, 100 + i));
|
||||
}
|
||||
Assert.True(cv.IsHot);
|
||||
|
||||
cv.Reset();
|
||||
Assert.False(cv.IsHot);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SingleValue_ReturnsPositiveVolatility()
|
||||
{
|
||||
var cv = new Cv(5);
|
||||
var result = cv.Update(new TValue(DateTime.UtcNow, 100));
|
||||
|
||||
// First value should still return a value (using default variance)
|
||||
Assert.True(double.IsFinite(result.Value));
|
||||
Assert.True(result.Value >= 0);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void IterativeCorrections_ChangesValue()
|
||||
{
|
||||
var cv = new Cv(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 = cv.Update(new TValue(times[i], close[i]), isNew: true);
|
||||
}
|
||||
double originalValue = lastValue.Value;
|
||||
|
||||
// Verify that isNew=false with different price produces different output
|
||||
var correctedValue = cv.Update(new TValue(DateTime.UtcNow, 999.99), isNew: false);
|
||||
Assert.NotEqual(originalValue, correctedValue.Value);
|
||||
|
||||
// Verify output is still finite and positive
|
||||
Assert.True(double.IsFinite(correctedValue.Value));
|
||||
Assert.True(correctedValue.Value >= 0);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void IsNew_Consistency()
|
||||
{
|
||||
var cv = new Cv(10);
|
||||
|
||||
for (int i = 0; i < 10; i++)
|
||||
{
|
||||
cv.Update(new TValue(DateTime.UtcNow, 100 + i), isNew: true);
|
||||
}
|
||||
|
||||
var result1 = cv.Update(new TValue(DateTime.UtcNow, 110), isNew: true);
|
||||
_ = cv.Update(new TValue(DateTime.UtcNow, 115), isNew: false);
|
||||
var result3 = cv.Update(new TValue(DateTime.UtcNow, 110), isNew: false);
|
||||
|
||||
// GARCH has path-dependent state that may cause slight differences due to omega calculation
|
||||
// on first entry to GARCH phase. Check that values are within 1% of each other.
|
||||
double tolerance = Math.Max(Math.Abs(result1.Value) * 0.01, 0.2);
|
||||
Assert.True(Math.Abs(result1.Value - result3.Value) < tolerance,
|
||||
$"Values should be similar: {result1.Value} vs {result3.Value}");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void NaN_Input_UsesLastValidValue()
|
||||
{
|
||||
var cv = new Cv(5);
|
||||
|
||||
for (int i = 0; i < 10; i++)
|
||||
{
|
||||
cv.Update(new TValue(DateTime.UtcNow, 100 + i));
|
||||
}
|
||||
|
||||
var resultNan = cv.Update(new TValue(DateTime.UtcNow, double.NaN));
|
||||
Assert.True(double.IsFinite(resultNan.Value));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Infinity_Input_UsesLastValidValue()
|
||||
{
|
||||
var cv = new Cv(5);
|
||||
|
||||
for (int i = 0; i < 10; i++)
|
||||
{
|
||||
cv.Update(new TValue(DateTime.UtcNow, 100 + i));
|
||||
}
|
||||
|
||||
var resultInf = cv.Update(new TValue(DateTime.UtcNow, double.PositiveInfinity));
|
||||
Assert.True(double.IsFinite(resultInf.Value));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void LargeDataset_Performance()
|
||||
{
|
||||
var cv = new Cv(50);
|
||||
var bars = GenerateTestData(5000);
|
||||
var times = bars.Times;
|
||||
var close = bars.CloseValues;
|
||||
|
||||
for (int i = 0; i < bars.Count; i++)
|
||||
{
|
||||
var result = cv.Update(new TValue(times[i], close[i]));
|
||||
Assert.True(double.IsFinite(result.Value));
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void TSeries_Update_MatchesStreaming()
|
||||
{
|
||||
int period = 20;
|
||||
var cvStream = new Cv(period);
|
||||
var cvBatch = new Cv(period);
|
||||
var bars = GenerateTestData(100);
|
||||
var times = bars.Times;
|
||||
var close = bars.CloseValues;
|
||||
|
||||
for (int i = 0; i < bars.Count; i++)
|
||||
{
|
||||
cvStream.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 = cvBatch.Update(ts);
|
||||
|
||||
Assert.Equal(cvStream.Last.Value, result[result.Count - 1].Value, 1e-9);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void BatchCalc_MatchesIterativeCalc()
|
||||
{
|
||||
var cv = new Cv(20);
|
||||
var bars = GenerateTestData(200);
|
||||
var times = bars.Times;
|
||||
var close = bars.CloseValues;
|
||||
|
||||
for (int i = 0; i < bars.Count; i++)
|
||||
{
|
||||
cv.Update(new TValue(times[i], close[i]));
|
||||
}
|
||||
var iterativeResult = cv.Last.Value;
|
||||
|
||||
var ts = new TSeries();
|
||||
for (int i = 0; i < bars.Count; i++)
|
||||
{
|
||||
ts.Add(new TValue(times[i], close[i]));
|
||||
}
|
||||
var batchResult = Cv.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 = Cv.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>(() => Cv.Batch(ts, 0));
|
||||
Assert.Throws<ArgumentException>(() => Cv.Batch(ts, -1));
|
||||
Assert.Throws<ArgumentException>(() => Cv.Batch(ts, 5, 0.0)); // alpha = 0
|
||||
Assert.Throws<ArgumentException>(() => Cv.Batch(ts, 5, 0.5, 0.6)); // alpha + beta >= 1
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Batch_NaN_Safe()
|
||||
{
|
||||
var values = new double[] { 100, 101, 102, double.NaN, 104, 105 };
|
||||
var output = new double[values.Length];
|
||||
|
||||
Cv.Batch(values, output, 3);
|
||||
|
||||
Assert.True(output.Length == 6);
|
||||
for (int i = 0; i < output.Length; i++)
|
||||
{
|
||||
Assert.True(double.IsFinite(output[i]));
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ConstantPrices_LowVolatility()
|
||||
{
|
||||
var cv = new Cv(10);
|
||||
|
||||
for (int i = 0; i < 20; i++)
|
||||
{
|
||||
cv.Update(new TValue(DateTime.UtcNow.AddMinutes(i), 100.0));
|
||||
}
|
||||
|
||||
// Constant prices should have very low volatility (approaching zero)
|
||||
Assert.True(cv.Last.Value < 1.0, "Constant prices should have very low volatility");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void HighVolatility_ProducesHigherValue()
|
||||
{
|
||||
var cvStable = new Cv(10);
|
||||
var cvVolatile = new Cv(10);
|
||||
|
||||
// Stable prices (small changes)
|
||||
for (int i = 0; i < 20; i++)
|
||||
{
|
||||
cvStable.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);
|
||||
cvVolatile.Update(new TValue(DateTime.UtcNow.AddMinutes(i), volatilePrice));
|
||||
}
|
||||
|
||||
Assert.True(cvVolatile.Last.Value > cvStable.Last.Value,
|
||||
"Higher volatility should produce higher CV");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void DifferentParameters_ProduceDistinctValues()
|
||||
{
|
||||
var bars = GenerateTestData(50);
|
||||
var times = bars.Times;
|
||||
var close = bars.CloseValues;
|
||||
|
||||
var cv1 = new Cv(20, 0.1, 0.8);
|
||||
var cv2 = new Cv(20, 0.2, 0.7);
|
||||
var cv3 = new Cv(20, 0.3, 0.6);
|
||||
|
||||
for (int i = 0; i < bars.Count; i++)
|
||||
{
|
||||
cv1.Update(new TValue(times[i], close[i]));
|
||||
cv2.Update(new TValue(times[i], close[i]));
|
||||
cv3.Update(new TValue(times[i], close[i]));
|
||||
}
|
||||
|
||||
Assert.True(double.IsFinite(cv1.Last.Value));
|
||||
Assert.True(double.IsFinite(cv2.Last.Value));
|
||||
Assert.True(double.IsFinite(cv3.Last.Value));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void VolatilityClustering_HighVolFollowsHighVol()
|
||||
{
|
||||
var cv = new Cv(10, 0.2, 0.7);
|
||||
|
||||
// Low volatility period
|
||||
for (int i = 0; i < 15; i++)
|
||||
{
|
||||
cv.Update(new TValue(DateTime.UtcNow.AddMinutes(i), 100 + i * 0.1));
|
||||
}
|
||||
double lowVolResult = cv.Last.Value;
|
||||
|
||||
// High volatility shock
|
||||
cv.Update(new TValue(DateTime.UtcNow.AddMinutes(15), 120)); // +20%
|
||||
cv.Update(new TValue(DateTime.UtcNow.AddMinutes(16), 100)); // -16.7%
|
||||
double afterShock = cv.Last.Value;
|
||||
|
||||
// GARCH should show elevated volatility after the shock
|
||||
Assert.True(afterShock > lowVolResult, "GARCH should capture volatility clustering");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void MeanReversion_VolReturnsToLongRun()
|
||||
{
|
||||
var cv = new Cv(10, 0.1, 0.8); // High beta = slower decay
|
||||
|
||||
// Establish long-run variance
|
||||
for (int i = 0; i < 15; i++)
|
||||
{
|
||||
cv.Update(new TValue(DateTime.UtcNow.AddMinutes(i), 100 + i * 0.5));
|
||||
}
|
||||
|
||||
// Introduce shock
|
||||
cv.Update(new TValue(DateTime.UtcNow.AddMinutes(15), 130));
|
||||
double shockVol = cv.Last.Value;
|
||||
|
||||
// Let it decay
|
||||
for (int i = 16; i < 50; i++)
|
||||
{
|
||||
cv.Update(new TValue(DateTime.UtcNow.AddMinutes(i), 100 + (i - 16) * 0.1));
|
||||
}
|
||||
double decayedVol = cv.Last.Value;
|
||||
|
||||
// Volatility should decay (mean revert) after shock
|
||||
Assert.True(decayedVol < shockVol * 0.9, "Volatility should mean-revert after shock");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Chainability_Works()
|
||||
{
|
||||
var cv = new Cv(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 cvResult = cv.Update(new TValue(times[i], close[i]));
|
||||
sma.Update(cvResult);
|
||||
}
|
||||
|
||||
Assert.True(sma.IsHot);
|
||||
Assert.True(double.IsFinite(sma.Last.Value));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,454 @@
|
||||
namespace QuanTAlib.Test;
|
||||
|
||||
using Xunit;
|
||||
|
||||
/// <summary>
|
||||
/// Validation tests for CV (Conditional Volatility - GARCH(1,1)).
|
||||
/// CV implements GARCH(1,1) volatility forecasting.
|
||||
/// These tests validate the mathematical correctness of the implementation.
|
||||
/// Formula: σ²_t = ω + α × r²_{t-1} + β × σ²_{t-1}
|
||||
/// </summary>
|
||||
public class CvValidationTests
|
||||
{
|
||||
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 GARCH stationarity constraint: α + β < 1
|
||||
/// </summary>
|
||||
[Theory]
|
||||
[InlineData(0.1, 0.8)] // Sum = 0.9, valid
|
||||
[InlineData(0.2, 0.7)] // Sum = 0.9, valid (default)
|
||||
[InlineData(0.05, 0.9)] // Sum = 0.95, valid
|
||||
public void Cv_ValidAlphaBetaCombinations_Accepted(double alpha, double beta)
|
||||
{
|
||||
var cv = new Cv(20, alpha, beta);
|
||||
Assert.NotNull(cv);
|
||||
Assert.Equal($"Cv({20},{alpha:F2},{beta:F2})", cv.Name);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Validates the annualization factor √252 is correctly applied.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void Cv_AnnualizationFactor_IsCorrect()
|
||||
{
|
||||
// √252 ≈ 15.8745
|
||||
double expectedFactor = Math.Sqrt(252);
|
||||
Assert.Equal(15.874507866387544, expectedFactor, 10);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Validates that constant prices produce near-zero volatility after warmup.
|
||||
/// Note: Due to MinVariance floor (1e-10) for numerical stability, the result
|
||||
/// is sqrt(252 * 1e-10) * 100 ≈ 0.016%, which is effectively zero for practical purposes.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void Cv_ConstantPrices_ProducesNearZeroVolatility()
|
||||
{
|
||||
var cv = new Cv(10, 0.2, 0.7);
|
||||
|
||||
for (int i = 0; i < 30; i++)
|
||||
{
|
||||
cv.Update(new TValue(DateTime.UtcNow.AddMinutes(i), 100.0));
|
||||
}
|
||||
|
||||
// Constant prices = zero returns = minimal variance (floored at MinVariance)
|
||||
// Result should be very small (< 0.1% annualized volatility)
|
||||
Assert.True(cv.Last.Value < 0.1, $"Expected near-zero volatility, got {cv.Last.Value}");
|
||||
Assert.True(cv.Last.Value >= 0, "Volatility cannot be negative");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Validates GARCH mean reversion property.
|
||||
/// After a shock, volatility should eventually decay toward long-run variance.
|
||||
/// Note: GARCH requires many periods for decay to be observable due to persistence (β).
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void Cv_MeanReversion_VolatilityDecaysAfterShock()
|
||||
{
|
||||
var cv = new Cv(20, 0.2, 0.7);
|
||||
|
||||
// Warmup with stable prices
|
||||
for (int i = 0; i < 25; i++)
|
||||
{
|
||||
double price = 100.0 * (1 + 0.001 * (i % 2 == 0 ? 1 : -1));
|
||||
cv.Update(new TValue(DateTime.UtcNow.AddMinutes(i), price));
|
||||
}
|
||||
|
||||
double preShockVol = cv.Last.Value;
|
||||
|
||||
// Large shock
|
||||
cv.Update(new TValue(DateTime.UtcNow.AddMinutes(30), 120.0)); // 20% jump
|
||||
double shockVol = cv.Last.Value;
|
||||
|
||||
// Shock should increase volatility (this is the key GARCH property)
|
||||
Assert.True(shockVol > preShockVol, "Shock should increase volatility");
|
||||
|
||||
// Continue with stable prices - track decay over many periods
|
||||
// With persistence = 0.9, need many periods for significant decay
|
||||
double lastVol = shockVol;
|
||||
for (int i = 0; i < 50; i++)
|
||||
{
|
||||
double price = 120.0 * (1 + 0.0001 * (i % 2 == 0 ? 1 : -1)); // Very stable prices
|
||||
cv.Update(new TValue(DateTime.UtcNow.AddMinutes(31 + i), price));
|
||||
lastVol = cv.Last.Value;
|
||||
}
|
||||
|
||||
// After many periods of stable prices, volatility should have decayed
|
||||
// (or at least not increased significantly from shock level)
|
||||
Assert.True(lastVol < shockVol * 1.5 || lastVol >= 0,
|
||||
$"Volatility should decay or stabilize after shock: shock={shockVol:F2}, final={lastVol:F2}");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Validates GARCH volatility clustering - high volatility follows high volatility.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void Cv_VolatilityClustering_HighVolFollowsHighVol()
|
||||
{
|
||||
var cv = new Cv(20, 0.2, 0.7);
|
||||
|
||||
// Warmup
|
||||
for (int i = 0; i < 25; i++)
|
||||
{
|
||||
cv.Update(new TValue(DateTime.UtcNow.AddMinutes(i), 100.0 + i * 0.1));
|
||||
}
|
||||
|
||||
// Series of large moves
|
||||
double price = 100.0;
|
||||
var volatilities = new List<double>();
|
||||
for (int i = 0; i < 5; i++)
|
||||
{
|
||||
price *= (i % 2 == 0) ? 1.05 : 0.95; // 5% swings
|
||||
cv.Update(new TValue(DateTime.UtcNow.AddMinutes(30 + i), price));
|
||||
volatilities.Add(cv.Last.Value);
|
||||
}
|
||||
|
||||
// Each subsequent volatility should remain elevated due to clustering
|
||||
for (int i = 1; i < volatilities.Count; i++)
|
||||
{
|
||||
Assert.True(volatilities[i] > 0, "Volatility should remain elevated during turbulent period");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Validates the GARCH formula by manual calculation.
|
||||
/// σ²_t = ω + α × r²_{t-1} + β × σ²_{t-1}
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void Cv_ManualGarchCalculation_MatchesFormula()
|
||||
{
|
||||
double alpha = 0.2;
|
||||
double beta = 0.7;
|
||||
int period = 5;
|
||||
|
||||
// Use fixed prices for deterministic testing
|
||||
double[] prices = { 100, 102, 101, 103, 105, 104, 106, 108, 107, 109, 110, 112, 111, 113, 115 };
|
||||
|
||||
// Calculate log returns
|
||||
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]);
|
||||
}
|
||||
|
||||
// Estimate long-run variance from first 'period' returns
|
||||
double sumSquares = 0;
|
||||
for (int i = 0; i < period; i++)
|
||||
{
|
||||
sumSquares += logReturns[i] * logReturns[i];
|
||||
}
|
||||
double longRunVar = sumSquares / period;
|
||||
double omega = (1 - alpha - beta) * longRunVar;
|
||||
|
||||
// Run GARCH recursion manually
|
||||
double variance = longRunVar;
|
||||
for (int i = period; i < logReturns.Length; i++)
|
||||
{
|
||||
double prevReturn = logReturns[i - 1];
|
||||
variance = omega + alpha * prevReturn * prevReturn + beta * variance;
|
||||
}
|
||||
|
||||
// Expected annualized volatility
|
||||
double expectedVol = Math.Sqrt(variance * 252) * 100;
|
||||
|
||||
// Now calculate using the indicator
|
||||
var cv = new Cv(period, alpha, beta);
|
||||
for (int i = 0; i < prices.Length; i++)
|
||||
{
|
||||
cv.Update(new TValue(DateTime.UtcNow.AddMinutes(i), prices[i]));
|
||||
}
|
||||
|
||||
// Allow some tolerance due to implementation details (initialization, MinVariance floor, etc.)
|
||||
// The test verifies the values are in the same ballpark (within 5% relative or 2 absolute)
|
||||
double relativeError = Math.Abs(expectedVol - cv.Last.Value) / Math.Max(expectedVol, 1e-10);
|
||||
Assert.True(relativeError < 0.05 || Math.Abs(expectedVol - cv.Last.Value) < 2.0,
|
||||
$"Expected ~{expectedVol:F2}, got {cv.Last.Value:F2} (relative error: {relativeError:P1})");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Validates unconditional variance formula: E[σ²] = ω / (1 - α - β)
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void Cv_UnconditionalVariance_MatchesFormula()
|
||||
{
|
||||
double alpha = 0.2;
|
||||
double beta = 0.7;
|
||||
double persistence = alpha + beta; // 0.9
|
||||
|
||||
// Unconditional variance = ω / (1 - α - β) = longRunVar (by construction)
|
||||
// This is because ω = (1 - α - β) × longRunVar
|
||||
// So ω / (1 - α - β) = longRunVar
|
||||
|
||||
// Verify persistence < 1 for stationarity
|
||||
Assert.True(persistence < 1.0, "α + β must be < 1 for stationarity");
|
||||
}
|
||||
|
||||
// === Consistency Tests ===
|
||||
|
||||
/// <summary>
|
||||
/// Validates streaming and batch produce identical results.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void Cv_StreamingMatchesBatch()
|
||||
{
|
||||
var bars = GenerateTestData(100);
|
||||
var times = bars.Times;
|
||||
var close = bars.CloseValues;
|
||||
|
||||
// Streaming calculation
|
||||
var streamingCv = new Cv(20, 0.2, 0.7);
|
||||
for (int i = 0; i < bars.Count; i++)
|
||||
{
|
||||
streamingCv.Update(new TValue(times[i], close[i]));
|
||||
}
|
||||
|
||||
// Batch calculation using Batch(TSeries -> TSeries)
|
||||
var source = new TSeries();
|
||||
for (int i = 0; i < bars.Count; i++)
|
||||
{
|
||||
source.Add(times[i], close[i]);
|
||||
}
|
||||
var batchResult = Cv.Batch(source, 20, 0.2, 0.7);
|
||||
|
||||
// Compare last values
|
||||
Assert.Equal(batchResult.Last.Value, streamingCv.Last.Value, 8);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Validates TSeries input produces same results as TValue streaming.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void Cv_TSeriesInput_MatchesStreaming()
|
||||
{
|
||||
var bars = GenerateTestData(100);
|
||||
var times = bars.Times;
|
||||
var close = bars.CloseValues;
|
||||
|
||||
// Create TSeries
|
||||
var source = new TSeries();
|
||||
for (int i = 0; i < bars.Count; i++)
|
||||
{
|
||||
source.Add(times[i], close[i]);
|
||||
}
|
||||
|
||||
// Streaming
|
||||
var streaming = new Cv(20, 0.2, 0.7);
|
||||
for (int i = 0; i < bars.Count; i++)
|
||||
{
|
||||
streaming.Update(new TValue(times[i], close[i]));
|
||||
}
|
||||
|
||||
// TSeries batch using Calculate
|
||||
var batch = Cv.Batch(source, 20, 0.2, 0.7);
|
||||
|
||||
// Compare
|
||||
Assert.Equal(batch.Last.Value, streaming.Last.Value, 10);
|
||||
}
|
||||
|
||||
// === Parameter Sensitivity ===
|
||||
|
||||
/// <summary>
|
||||
/// Validates higher alpha increases sensitivity to recent shocks.
|
||||
/// Note: GARCH uses lagged squared returns, so the shock's effect appears on the NEXT bar.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void Cv_HigherAlpha_MoreSensitiveToShocks()
|
||||
{
|
||||
var cvLowAlpha = new Cv(20, 0.1, 0.8); // alpha = 0.1, persistence = 0.9
|
||||
var cvHighAlpha = new Cv(20, 0.3, 0.6); // alpha = 0.3, persistence = 0.9
|
||||
|
||||
// Warmup with small variations (not constant, so we get non-zero variance)
|
||||
for (int i = 0; i < 30; i++)
|
||||
{
|
||||
double price = 100.0 + (i % 2 == 0 ? 0.1 : -0.1); // Small oscillation
|
||||
cvLowAlpha.Update(new TValue(DateTime.UtcNow.AddMinutes(i), price));
|
||||
cvHighAlpha.Update(new TValue(DateTime.UtcNow.AddMinutes(i), price));
|
||||
}
|
||||
|
||||
double preLowAlpha = cvLowAlpha.Last.Value;
|
||||
double preHighAlpha = cvHighAlpha.Last.Value;
|
||||
|
||||
// Large shock - same for both
|
||||
cvLowAlpha.Update(new TValue(DateTime.UtcNow.AddMinutes(35), 110.0)); // 10% jump
|
||||
cvHighAlpha.Update(new TValue(DateTime.UtcNow.AddMinutes(35), 110.0));
|
||||
|
||||
// GARCH uses lagged squared returns, so add one more bar to see the shock's effect
|
||||
cvLowAlpha.Update(new TValue(DateTime.UtcNow.AddMinutes(36), 110.5));
|
||||
cvHighAlpha.Update(new TValue(DateTime.UtcNow.AddMinutes(36), 110.5));
|
||||
|
||||
double afterShockLowAlpha = cvLowAlpha.Last.Value;
|
||||
double afterShockHighAlpha = cvHighAlpha.Last.Value;
|
||||
|
||||
// Both should have increased from their baseline after shock effect propagates
|
||||
Assert.True(afterShockLowAlpha > preLowAlpha,
|
||||
$"Low alpha volatility should increase after shock: before={preLowAlpha:F2}, after={afterShockLowAlpha:F2}");
|
||||
Assert.True(afterShockHighAlpha > preHighAlpha,
|
||||
$"High alpha volatility should increase after shock: before={preHighAlpha:F2}, after={afterShockHighAlpha:F2}");
|
||||
|
||||
// Higher alpha should produce larger increase due to higher weight on recent squared return
|
||||
double lowAlphaIncrease = afterShockLowAlpha - preLowAlpha;
|
||||
double highAlphaIncrease = afterShockHighAlpha - preHighAlpha;
|
||||
Assert.True(highAlphaIncrease >= lowAlphaIncrease * 0.9, // Allow 10% tolerance
|
||||
$"Higher alpha should produce larger reaction: low={lowAlphaIncrease:F4}, high={highAlphaIncrease:F4}");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Validates higher beta increases persistence of volatility.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void Cv_HigherBeta_MorePersistentVolatility()
|
||||
{
|
||||
var cvLowBeta = new Cv(20, 0.2, 0.5); // beta = 0.5
|
||||
var cvHighBeta = new Cv(20, 0.2, 0.75); // beta = 0.75
|
||||
|
||||
// Warmup with stable prices then shock
|
||||
for (int i = 0; i < 25; i++)
|
||||
{
|
||||
double price = 100.0 + i * 0.1;
|
||||
cvLowBeta.Update(new TValue(DateTime.UtcNow.AddMinutes(i), price));
|
||||
cvHighBeta.Update(new TValue(DateTime.UtcNow.AddMinutes(i), price));
|
||||
}
|
||||
|
||||
// Large shock
|
||||
cvLowBeta.Update(new TValue(DateTime.UtcNow.AddMinutes(30), 120.0));
|
||||
cvHighBeta.Update(new TValue(DateTime.UtcNow.AddMinutes(30), 120.0));
|
||||
|
||||
double postShockLow = cvLowBeta.Last.Value;
|
||||
double postShockHigh = cvHighBeta.Last.Value;
|
||||
|
||||
// Continue with stable prices - track decay
|
||||
for (int i = 0; i < 20; i++)
|
||||
{
|
||||
double price = 120.0 + i * 0.05;
|
||||
cvLowBeta.Update(new TValue(DateTime.UtcNow.AddMinutes(31 + i), price));
|
||||
cvHighBeta.Update(new TValue(DateTime.UtcNow.AddMinutes(31 + i), price));
|
||||
}
|
||||
|
||||
double decayLow = postShockLow - cvLowBeta.Last.Value;
|
||||
double decayHigh = postShockHigh - cvHighBeta.Last.Value;
|
||||
|
||||
// Higher beta should decay more slowly (less decay)
|
||||
Assert.True(decayHigh < decayLow || Math.Abs(decayHigh - decayLow) < 1,
|
||||
"Higher beta should result in more persistent volatility (slower decay)");
|
||||
}
|
||||
|
||||
// === Edge Cases ===
|
||||
|
||||
/// <summary>
|
||||
/// Validates handling of very small price changes.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void Cv_SmallPriceChanges_HandledCorrectly()
|
||||
{
|
||||
var cv = new Cv(10, 0.2, 0.7);
|
||||
|
||||
double price = 100.0;
|
||||
for (int i = 0; i < 20; i++)
|
||||
{
|
||||
price += 0.0001; // Very small changes
|
||||
cv.Update(new TValue(DateTime.UtcNow.AddMinutes(i), price));
|
||||
}
|
||||
|
||||
Assert.True(double.IsFinite(cv.Last.Value));
|
||||
Assert.True(cv.Last.Value >= 0);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Validates handling of large price swings.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void Cv_LargePriceSwings_HandledCorrectly()
|
||||
{
|
||||
var cv = new Cv(10, 0.2, 0.7);
|
||||
|
||||
for (int i = 0; i < 20; i++)
|
||||
{
|
||||
double price = 100.0 * (i % 2 == 0 ? 2.0 : 0.5); // 100% swings
|
||||
cv.Update(new TValue(DateTime.UtcNow.AddMinutes(i), price));
|
||||
}
|
||||
|
||||
Assert.True(double.IsFinite(cv.Last.Value));
|
||||
Assert.True(cv.Last.Value > 0, "Large swings should produce positive volatility");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Validates that different periods produce different warmup behaviors.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void Cv_DifferentPeriods_DifferentWarmup()
|
||||
{
|
||||
var bars = GenerateTestData(100);
|
||||
var times = bars.Times;
|
||||
var close = bars.CloseValues;
|
||||
|
||||
var cv5 = new Cv(5, 0.2, 0.7);
|
||||
var cv20 = new Cv(20, 0.2, 0.7);
|
||||
var cv50 = new Cv(50, 0.2, 0.7);
|
||||
|
||||
for (int i = 0; i < bars.Count; i++)
|
||||
{
|
||||
cv5.Update(new TValue(times[i], close[i]));
|
||||
cv20.Update(new TValue(times[i], close[i]));
|
||||
cv50.Update(new TValue(times[i], close[i]));
|
||||
}
|
||||
|
||||
// All should be valid
|
||||
Assert.True(double.IsFinite(cv5.Last.Value));
|
||||
Assert.True(double.IsFinite(cv20.Last.Value));
|
||||
Assert.True(double.IsFinite(cv50.Last.Value));
|
||||
|
||||
// All should be non-negative
|
||||
Assert.True(cv5.Last.Value >= 0);
|
||||
Assert.True(cv20.Last.Value >= 0);
|
||||
Assert.True(cv50.Last.Value >= 0);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Validates output is percentage (annualized volatility × 100).
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void Cv_OutputIsPercentage_ReasonableRange()
|
||||
{
|
||||
var bars = GenerateTestData(100);
|
||||
var times = bars.Times;
|
||||
var close = bars.CloseValues;
|
||||
|
||||
var cv = new Cv(20, 0.2, 0.7);
|
||||
for (int i = 0; i < bars.Count; i++)
|
||||
{
|
||||
cv.Update(new TValue(times[i], close[i]));
|
||||
}
|
||||
|
||||
// For typical market data, annualized volatility should be in reasonable range
|
||||
// GBM with default params typically produces 10-50% annualized vol
|
||||
Assert.True(cv.Last.Value >= 0, "Volatility cannot be negative");
|
||||
Assert.True(cv.Last.Value < 500, "Volatility should be reasonable (< 500% annualized)");
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user