mirror of
https://github.com/mihakralj/QuanTAlib.git
synced 2026-08-22 12:38:06 +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,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);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user