mirror of
https://github.com/mihakralj/QuanTAlib.git
synced 2026-08-22 04:28: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,345 @@
|
||||
using TradingPlatform.BusinessLayer;
|
||||
using QuanTAlib;
|
||||
|
||||
namespace QuanTAlib.Tests;
|
||||
|
||||
public class RvIndicatorTests
|
||||
{
|
||||
[Fact]
|
||||
public void RvIndicator_Constructor_SetsDefaults()
|
||||
{
|
||||
var indicator = new RvIndicator();
|
||||
|
||||
Assert.Equal(5, indicator.Period);
|
||||
Assert.Equal(20, indicator.SmoothingPeriod);
|
||||
Assert.True(indicator.Annualize);
|
||||
Assert.Equal(252, indicator.AnnualPeriods);
|
||||
Assert.True(indicator.ShowColdValues);
|
||||
Assert.Equal("RV - Realized Volatility", indicator.Name);
|
||||
Assert.True(indicator.SeparateWindow);
|
||||
Assert.True(indicator.OnBackGround);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void RvIndicator_ShortName_IncludesParameters()
|
||||
{
|
||||
var indicator = new RvIndicator { Period = 10, SmoothingPeriod = 15 };
|
||||
Assert.Contains("RV", indicator.ShortName, StringComparison.Ordinal);
|
||||
Assert.Contains("10", indicator.ShortName, StringComparison.Ordinal);
|
||||
Assert.Contains("15", indicator.ShortName, StringComparison.Ordinal);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void RvIndicator_MinHistoryDepths_EqualsZero()
|
||||
{
|
||||
var indicator = new RvIndicator();
|
||||
|
||||
Assert.Equal(0, RvIndicator.MinHistoryDepths);
|
||||
Assert.Equal(0, ((IWatchlistIndicator)indicator).MinHistoryDepths);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void RvIndicator_Initialize_CreatesInternalRv()
|
||||
{
|
||||
var indicator = new RvIndicator();
|
||||
|
||||
indicator.Initialize();
|
||||
|
||||
Assert.Single(indicator.LinesSeries);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void RvIndicator_ProcessUpdate_HistoricalBar_ComputesValue()
|
||||
{
|
||||
var indicator = new RvIndicator { Period = 5, SmoothingPeriod = 10 };
|
||||
indicator.Initialize();
|
||||
|
||||
var now = DateTime.UtcNow;
|
||||
for (int i = 0; i < 30; i++)
|
||||
{
|
||||
double closePrice = 100 + i * 0.5 + Math.Sin(i * 0.3) * 2;
|
||||
indicator.HistoricalData.AddBar(now.AddMinutes(i), closePrice - 1, closePrice + 2, closePrice - 2, closePrice, 1000);
|
||||
|
||||
var args = new UpdateArgs(UpdateReason.HistoricalBar);
|
||||
indicator.ProcessUpdate(args);
|
||||
}
|
||||
|
||||
double val = indicator.LinesSeries[0].GetValue(0);
|
||||
Assert.True(double.IsFinite(val));
|
||||
Assert.True(val >= 0, "Volatility should be non-negative");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void RvIndicator_ProcessUpdate_NewBar_ComputesValue()
|
||||
{
|
||||
var indicator = new RvIndicator { Period = 5, SmoothingPeriod = 10 };
|
||||
indicator.Initialize();
|
||||
|
||||
var now = DateTime.UtcNow;
|
||||
for (int i = 0; i < 30; i++)
|
||||
{
|
||||
double closePrice = 100 + i * 0.3;
|
||||
indicator.HistoricalData.AddBar(now.AddMinutes(i), closePrice - 1, closePrice + 2, closePrice - 2, closePrice, 1000);
|
||||
}
|
||||
|
||||
indicator.ProcessUpdate(new UpdateArgs(UpdateReason.HistoricalBar));
|
||||
|
||||
indicator.HistoricalData.AddBar(now.AddMinutes(30), 115, 120, 110, 118, 1500);
|
||||
indicator.ProcessUpdate(new UpdateArgs(UpdateReason.NewBar));
|
||||
|
||||
Assert.Equal(2, indicator.LinesSeries[0].Count);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void RvIndicator_DifferentPeriods_Work()
|
||||
{
|
||||
int[] periods = { 3, 5, 10 };
|
||||
|
||||
foreach (var period in periods)
|
||||
{
|
||||
var indicator = new RvIndicator { Period = period, SmoothingPeriod = 10 };
|
||||
indicator.Initialize();
|
||||
|
||||
var now = DateTime.UtcNow;
|
||||
for (int i = 0; i < 50; i++)
|
||||
{
|
||||
double closePrice = 100 + i * 0.2 + Math.Sin(i * 0.5) * 3;
|
||||
indicator.HistoricalData.AddBar(now.AddMinutes(i), closePrice - 1, closePrice + 2, closePrice - 2, closePrice, 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 value");
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void RvIndicator_Period_CanBeChanged()
|
||||
{
|
||||
var indicator = new RvIndicator();
|
||||
Assert.Equal(5, indicator.Period);
|
||||
|
||||
indicator.Period = 10;
|
||||
Assert.Equal(10, indicator.Period);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void RvIndicator_SmoothingPeriod_CanBeChanged()
|
||||
{
|
||||
var indicator = new RvIndicator();
|
||||
Assert.Equal(20, indicator.SmoothingPeriod);
|
||||
|
||||
indicator.SmoothingPeriod = 30;
|
||||
Assert.Equal(30, indicator.SmoothingPeriod);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void RvIndicator_Annualize_CanBeToggled()
|
||||
{
|
||||
var indicator = new RvIndicator();
|
||||
Assert.True(indicator.Annualize);
|
||||
|
||||
indicator.Annualize = false;
|
||||
Assert.False(indicator.Annualize);
|
||||
|
||||
indicator.Annualize = true;
|
||||
Assert.True(indicator.Annualize);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void RvIndicator_AnnualPeriods_CanBeChanged()
|
||||
{
|
||||
var indicator = new RvIndicator();
|
||||
Assert.Equal(252, indicator.AnnualPeriods);
|
||||
|
||||
indicator.AnnualPeriods = 365;
|
||||
Assert.Equal(365, indicator.AnnualPeriods);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void RvIndicator_ShowColdValues_CanBeToggled()
|
||||
{
|
||||
var indicator = new RvIndicator();
|
||||
Assert.True(indicator.ShowColdValues);
|
||||
|
||||
indicator.ShowColdValues = false;
|
||||
Assert.False(indicator.ShowColdValues);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void RvIndicator_SourceCodeLink_IsValid()
|
||||
{
|
||||
var indicator = new RvIndicator();
|
||||
Assert.Contains("github.com", indicator.SourceCodeLink, StringComparison.Ordinal);
|
||||
Assert.Contains("Rv.Quantower.cs", indicator.SourceCodeLink, StringComparison.Ordinal);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void RvIndicator_HighVolatility_ProducesHigherValue()
|
||||
{
|
||||
var indicator1 = new RvIndicator { Period = 5, SmoothingPeriod = 10, Annualize = false };
|
||||
var indicator2 = new RvIndicator { Period = 5, SmoothingPeriod = 10, Annualize = false };
|
||||
indicator1.Initialize();
|
||||
indicator2.Initialize();
|
||||
|
||||
var now = DateTime.UtcNow;
|
||||
|
||||
// Low volatility
|
||||
for (int i = 0; i < 30; i++)
|
||||
{
|
||||
double closePrice = 100 + i * 0.01;
|
||||
indicator1.HistoricalData.AddBar(now.AddMinutes(i), closePrice - 0.5, closePrice + 0.5, closePrice - 0.5, closePrice, 1000);
|
||||
indicator1.ProcessUpdate(new UpdateArgs(UpdateReason.HistoricalBar));
|
||||
}
|
||||
|
||||
// High volatility
|
||||
for (int i = 0; i < 30; i++)
|
||||
{
|
||||
double closePrice = 100 + Math.Sin(i * 0.5) * 10;
|
||||
indicator2.HistoricalData.AddBar(now.AddMinutes(i), closePrice - 2, closePrice + 2, closePrice - 2, closePrice, 1000);
|
||||
indicator2.ProcessUpdate(new UpdateArgs(UpdateReason.HistoricalBar));
|
||||
}
|
||||
|
||||
double lowVol = indicator1.LinesSeries[0].GetValue(0);
|
||||
double highVol = indicator2.LinesSeries[0].GetValue(0);
|
||||
|
||||
Assert.True(double.IsFinite(lowVol));
|
||||
Assert.True(double.IsFinite(highVol));
|
||||
Assert.True(highVol > lowVol, "Higher volatility closes should produce higher RV value");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void RvIndicator_AnnualizedValue_IsScaled()
|
||||
{
|
||||
var indicatorRaw = new RvIndicator { Period = 5, SmoothingPeriod = 10, Annualize = false };
|
||||
var indicatorAnn = new RvIndicator { Period = 5, SmoothingPeriod = 10, Annualize = true, AnnualPeriods = 252 };
|
||||
indicatorRaw.Initialize();
|
||||
indicatorAnn.Initialize();
|
||||
|
||||
var now = DateTime.UtcNow;
|
||||
|
||||
for (int i = 0; i < 30; i++)
|
||||
{
|
||||
double closePrice = 100 + i * 0.5 + Math.Sin(i * 0.3) * 2;
|
||||
indicatorRaw.HistoricalData.AddBar(now.AddMinutes(i), closePrice - 1, closePrice + 2, closePrice - 2, closePrice, 1000);
|
||||
indicatorRaw.ProcessUpdate(new UpdateArgs(UpdateReason.HistoricalBar));
|
||||
|
||||
indicatorAnn.HistoricalData.AddBar(now.AddMinutes(i), closePrice - 1, closePrice + 2, closePrice - 2, closePrice, 1000);
|
||||
indicatorAnn.ProcessUpdate(new UpdateArgs(UpdateReason.HistoricalBar));
|
||||
}
|
||||
|
||||
double rawValue = indicatorRaw.LinesSeries[0].GetValue(0);
|
||||
double annValue = indicatorAnn.LinesSeries[0].GetValue(0);
|
||||
|
||||
Assert.True(double.IsFinite(rawValue));
|
||||
Assert.True(double.IsFinite(annValue));
|
||||
|
||||
double expectedRatio = Math.Sqrt(252);
|
||||
double actualRatio = annValue / rawValue;
|
||||
|
||||
Assert.True(Math.Abs(actualRatio - expectedRatio) < 0.01,
|
||||
$"Annualized value should be ~{expectedRatio:F2}× raw, got {actualRatio:F2}×");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void RvIndicator_OnlyUsesClose_IgnoresOpenHighLow()
|
||||
{
|
||||
var indicator1 = new RvIndicator { Period = 5, SmoothingPeriod = 10, Annualize = false };
|
||||
var indicator2 = new RvIndicator { Period = 5, SmoothingPeriod = 10, Annualize = false };
|
||||
indicator1.Initialize();
|
||||
indicator2.Initialize();
|
||||
|
||||
var now = DateTime.UtcNow;
|
||||
|
||||
for (int i = 0; i < 30; i++)
|
||||
{
|
||||
double closePrice = 100 + i * 0.5;
|
||||
// Narrow range
|
||||
indicator1.HistoricalData.AddBar(now.AddMinutes(i), closePrice, closePrice + 1, closePrice - 1, closePrice, 1000);
|
||||
indicator1.ProcessUpdate(new UpdateArgs(UpdateReason.HistoricalBar));
|
||||
|
||||
// Wide range (same close)
|
||||
indicator2.HistoricalData.AddBar(now.AddMinutes(i), closePrice - 5, closePrice + 10, closePrice - 10, closePrice, 1000);
|
||||
indicator2.ProcessUpdate(new UpdateArgs(UpdateReason.HistoricalBar));
|
||||
}
|
||||
|
||||
double val1 = indicator1.LinesSeries[0].GetValue(0);
|
||||
double val2 = indicator2.LinesSeries[0].GetValue(0);
|
||||
|
||||
Assert.True(double.IsFinite(val1));
|
||||
Assert.True(double.IsFinite(val2));
|
||||
Assert.Equal(val1, val2, 10);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void RvIndicator_ConstantPrice_ProducesZeroVolatility()
|
||||
{
|
||||
var indicator = new RvIndicator { Period = 5, SmoothingPeriod = 10, Annualize = false };
|
||||
indicator.Initialize();
|
||||
|
||||
var now = DateTime.UtcNow;
|
||||
|
||||
for (int i = 0; i < 30; i++)
|
||||
{
|
||||
indicator.HistoricalData.AddBar(now.AddMinutes(i), 100, 105, 95, 100, 1000);
|
||||
indicator.ProcessUpdate(new UpdateArgs(UpdateReason.HistoricalBar));
|
||||
}
|
||||
|
||||
double val = indicator.LinesSeries[0].GetValue(0);
|
||||
|
||||
Assert.True(double.IsFinite(val));
|
||||
Assert.True(val < 0.001, "Constant close price should produce near-zero volatility");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void RvIndicator_VaryingReturns_ProducesNonZeroVolatility()
|
||||
{
|
||||
var indicator = new RvIndicator { Period = 5, SmoothingPeriod = 10, Annualize = false };
|
||||
indicator.Initialize();
|
||||
|
||||
var now = DateTime.UtcNow;
|
||||
|
||||
for (int i = 0; i < 30; i++)
|
||||
{
|
||||
double rate = (i % 2 == 0) ? 1.02 : 1.005;
|
||||
double closePrice = 100 * Math.Pow(rate, i / 2 + 1) * (i % 2 == 0 ? 1.0 : rate);
|
||||
indicator.HistoricalData.AddBar(now.AddMinutes(i), closePrice - 1, closePrice + 1, closePrice - 1, closePrice, 1000);
|
||||
indicator.ProcessUpdate(new UpdateArgs(UpdateReason.HistoricalBar));
|
||||
}
|
||||
|
||||
double val = indicator.LinesSeries[0].GetValue(0);
|
||||
|
||||
Assert.True(double.IsFinite(val));
|
||||
Assert.True(val > 0, "Varying returns should produce non-zero volatility");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void RvIndicator_DifferentSmoothingPeriods_ProduceDifferentResults()
|
||||
{
|
||||
var indicator1 = new RvIndicator { Period = 5, SmoothingPeriod = 5, Annualize = false };
|
||||
var indicator2 = new RvIndicator { Period = 5, SmoothingPeriod = 20, Annualize = false };
|
||||
indicator1.Initialize();
|
||||
indicator2.Initialize();
|
||||
|
||||
var now = DateTime.UtcNow;
|
||||
|
||||
for (int i = 0; i < 50; i++)
|
||||
{
|
||||
double closePrice = 100 + Math.Sin(i * 0.3) * 5;
|
||||
indicator1.HistoricalData.AddBar(now.AddMinutes(i), closePrice - 1, closePrice + 1, closePrice - 1, closePrice, 1000);
|
||||
indicator1.ProcessUpdate(new UpdateArgs(UpdateReason.HistoricalBar));
|
||||
|
||||
indicator2.HistoricalData.AddBar(now.AddMinutes(i), closePrice - 1, closePrice + 1, closePrice - 1, closePrice, 1000);
|
||||
indicator2.ProcessUpdate(new UpdateArgs(UpdateReason.HistoricalBar));
|
||||
}
|
||||
|
||||
double val1 = indicator1.LinesSeries[0].GetValue(0);
|
||||
double val2 = indicator2.LinesSeries[0].GetValue(0);
|
||||
|
||||
Assert.True(double.IsFinite(val1));
|
||||
Assert.True(double.IsFinite(val2));
|
||||
// Different smoothing periods should produce different results
|
||||
Assert.NotEqual(val1, val2);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,715 @@
|
||||
namespace QuanTAlib.Tests;
|
||||
using Xunit;
|
||||
|
||||
public class RvTests
|
||||
{
|
||||
private const double Tolerance = 1e-9;
|
||||
|
||||
private static TBarSeries GenerateTestData(int count = 100)
|
||||
{
|
||||
var gbm = new GBM(seed: 42);
|
||||
return gbm.Fetch(count, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1));
|
||||
}
|
||||
|
||||
private static TSeries GeneratePriceSeries(int count = 100)
|
||||
{
|
||||
var gbm = new GBM(seed: 42);
|
||||
var bars = gbm.Fetch(count, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1));
|
||||
var t = new List<long>(count);
|
||||
var v = new List<double>(count);
|
||||
for (int i = 0; i < count; i++)
|
||||
{
|
||||
t.Add(bars[i].Time);
|
||||
v.Add(bars[i].Close);
|
||||
}
|
||||
return new TSeries(t, v);
|
||||
}
|
||||
|
||||
#region Constructor Tests
|
||||
|
||||
[Fact]
|
||||
public void Constructor_DefaultParameters_SetsCorrectValues()
|
||||
{
|
||||
var rv = new Rv();
|
||||
Assert.Equal(5, rv.Period);
|
||||
Assert.Equal(20, rv.SmoothingPeriod);
|
||||
Assert.True(rv.Annualize);
|
||||
Assert.Equal(252, rv.AnnualPeriods);
|
||||
Assert.Equal("Rv(5,20)", rv.Name);
|
||||
Assert.Equal(25, rv.WarmupPeriod); // period + smoothingPeriod
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Constructor_CustomParameters_SetsCorrectValues()
|
||||
{
|
||||
var rv = new Rv(period: 10, smoothingPeriod: 30, annualize: false, annualPeriods: 365);
|
||||
Assert.Equal(10, rv.Period);
|
||||
Assert.Equal(30, rv.SmoothingPeriod);
|
||||
Assert.False(rv.Annualize);
|
||||
Assert.Equal(365, rv.AnnualPeriods);
|
||||
Assert.Equal("Rv(10,30)", rv.Name);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Constructor_ZeroPeriod_ThrowsArgumentException()
|
||||
{
|
||||
var ex = Assert.Throws<ArgumentException>(() => new Rv(period: 0));
|
||||
Assert.Equal("period", ex.ParamName);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Constructor_NegativePeriod_ThrowsArgumentException()
|
||||
{
|
||||
var ex = Assert.Throws<ArgumentException>(() => new Rv(period: -1));
|
||||
Assert.Equal("period", ex.ParamName);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Constructor_ZeroSmoothingPeriod_ThrowsArgumentException()
|
||||
{
|
||||
var ex = Assert.Throws<ArgumentException>(() => new Rv(period: 5, smoothingPeriod: 0));
|
||||
Assert.Equal("smoothingPeriod", ex.ParamName);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Constructor_NegativeSmoothingPeriod_ThrowsArgumentException()
|
||||
{
|
||||
var ex = Assert.Throws<ArgumentException>(() => new Rv(period: 5, smoothingPeriod: -1));
|
||||
Assert.Equal("smoothingPeriod", ex.ParamName);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Constructor_ZeroAnnualPeriodsWhenAnnualizing_ThrowsArgumentException()
|
||||
{
|
||||
var ex = Assert.Throws<ArgumentException>(() => new Rv(period: 5, smoothingPeriod: 20, annualize: true, annualPeriods: 0));
|
||||
Assert.Equal("annualPeriods", ex.ParamName);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Constructor_ZeroAnnualPeriodsWhenNotAnnualizing_DoesNotThrow()
|
||||
{
|
||||
var rv = new Rv(period: 5, smoothingPeriod: 20, annualize: false, annualPeriods: 0);
|
||||
Assert.Equal(0, rv.AnnualPeriods);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Basic Calculation Tests
|
||||
|
||||
[Fact]
|
||||
public void Update_SinglePrice_ReturnsZero()
|
||||
{
|
||||
var rv = new Rv(period: 5, smoothingPeriod: 10);
|
||||
var price = new TValue(DateTime.UtcNow, 100.0);
|
||||
var result = rv.Update(price);
|
||||
|
||||
// First price cannot produce a return, so volatility is 0
|
||||
Assert.Equal(0.0, result.Value);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Update_TwoPrices_ReturnsPositiveVolatility()
|
||||
{
|
||||
var rv = new Rv(period: 5, smoothingPeriod: 10);
|
||||
rv.Update(new TValue(DateTime.UtcNow, 100.0));
|
||||
var result = rv.Update(new TValue(DateTime.UtcNow.AddMinutes(1), 101.0));
|
||||
|
||||
// Second price gives first squared return, so volatility should be positive
|
||||
Assert.True(result.Value >= 0);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Update_MultiplePrices_ReturnsPositiveVolatility()
|
||||
{
|
||||
var rv = new Rv(period: 5, smoothingPeriod: 10);
|
||||
var prices = GeneratePriceSeries(30);
|
||||
|
||||
double lastValue = 0;
|
||||
for (int i = 0; i < prices.Count; i++)
|
||||
{
|
||||
lastValue = rv.Update(prices[i]).Value;
|
||||
}
|
||||
|
||||
Assert.True(lastValue > 0, "RV should return positive volatility after warmup");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Update_ReturnsLastValue()
|
||||
{
|
||||
var rv = new Rv(period: 5, smoothingPeriod: 10);
|
||||
var price = new TValue(DateTime.UtcNow, 100.0);
|
||||
var result = rv.Update(price);
|
||||
|
||||
Assert.Equal(result.Value, rv.Last.Value, Tolerance);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Update_WithoutAnnualization_ReturnsSmallerValues()
|
||||
{
|
||||
var rvAnnual = new Rv(period: 5, smoothingPeriod: 10, annualize: true, annualPeriods: 252);
|
||||
var rvNoAnnual = new Rv(period: 5, smoothingPeriod: 10, annualize: false);
|
||||
var prices = GeneratePriceSeries(30);
|
||||
|
||||
double lastAnnual = 0;
|
||||
double lastNoAnnual = 0;
|
||||
for (int i = 0; i < prices.Count; i++)
|
||||
{
|
||||
lastAnnual = rvAnnual.Update(prices[i]).Value;
|
||||
lastNoAnnual = rvNoAnnual.Update(prices[i]).Value;
|
||||
}
|
||||
|
||||
// Annualized values should be larger by factor of sqrt(252)
|
||||
Assert.True(lastAnnual > lastNoAnnual, "Annualized values should be larger");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Update_AnnualizationFactor_Correct()
|
||||
{
|
||||
var rvAnnual = new Rv(period: 5, smoothingPeriod: 10, annualize: true, annualPeriods: 252);
|
||||
var rvNoAnnual = new Rv(period: 5, smoothingPeriod: 10, annualize: false);
|
||||
var prices = GeneratePriceSeries(50);
|
||||
|
||||
for (int i = 0; i < prices.Count; i++)
|
||||
{
|
||||
rvAnnual.Update(prices[i]);
|
||||
rvNoAnnual.Update(prices[i]);
|
||||
}
|
||||
|
||||
double factor = rvAnnual.Last.Value / rvNoAnnual.Last.Value;
|
||||
double expectedFactor = Math.Sqrt(252);
|
||||
|
||||
Assert.Equal(expectedFactor, factor, 1e-6);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region State Management Tests
|
||||
|
||||
[Fact]
|
||||
public void Update_IsNewTrue_AdvancesState()
|
||||
{
|
||||
var rv = new Rv(period: 3, smoothingPeriod: 5);
|
||||
var prices = GeneratePriceSeries(10);
|
||||
|
||||
for (int i = 0; i < 5; i++)
|
||||
{
|
||||
rv.Update(prices[i], isNew: true);
|
||||
}
|
||||
var result1 = rv.Last.Value;
|
||||
|
||||
rv.Update(prices[5], isNew: true);
|
||||
var result2 = rv.Last.Value;
|
||||
|
||||
Assert.True(result1 >= 0, "First result should be non-negative");
|
||||
Assert.True(result2 >= 0, "Second result should be non-negative");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Update_IsNewFalse_UpdatesCurrentBar()
|
||||
{
|
||||
var rv = new Rv(period: 3, smoothingPeriod: 5);
|
||||
var prices = GeneratePriceSeries(10);
|
||||
|
||||
for (int i = 0; i < 5; i++)
|
||||
{
|
||||
rv.Update(prices[i], isNew: true);
|
||||
}
|
||||
|
||||
rv.Update(prices[5], isNew: true);
|
||||
var firstValue = rv.Last.Value;
|
||||
|
||||
var updatedPrice = new TValue(prices[5].Time, prices[5].Value * 1.05);
|
||||
rv.Update(updatedPrice, isNew: false);
|
||||
var updatedValue = rv.Last.Value;
|
||||
|
||||
Assert.NotEqual(firstValue, updatedValue);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Update_IterativeCorrections_RestoresState()
|
||||
{
|
||||
var rv = new Rv(period: 3, smoothingPeriod: 5);
|
||||
var prices = GeneratePriceSeries(15);
|
||||
|
||||
for (int i = 0; i < 5; i++)
|
||||
{
|
||||
rv.Update(prices[i], isNew: true);
|
||||
}
|
||||
|
||||
rv.Update(prices[5], isNew: true);
|
||||
rv.Update(prices[5], isNew: false);
|
||||
rv.Update(prices[5], isNew: false);
|
||||
rv.Update(prices[5], isNew: false);
|
||||
|
||||
rv.Update(prices[6], isNew: true);
|
||||
|
||||
var rv2 = new Rv(period: 3, smoothingPeriod: 5);
|
||||
for (int i = 0; i < 7; i++)
|
||||
{
|
||||
rv2.Update(prices[i], isNew: true);
|
||||
}
|
||||
|
||||
Assert.Equal(rv.Last.Value, rv2.Last.Value, Tolerance);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region IsHot and Warmup Tests
|
||||
|
||||
[Fact]
|
||||
public void IsHot_BeforeWarmup_ReturnsFalse()
|
||||
{
|
||||
var rv = new Rv(period: 5, smoothingPeriod: 10);
|
||||
var prices = GeneratePriceSeries(10);
|
||||
|
||||
for (int i = 0; i < prices.Count; i++)
|
||||
{
|
||||
rv.Update(prices[i]);
|
||||
}
|
||||
|
||||
Assert.False(rv.IsHot);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void IsHot_AfterWarmup_ReturnsTrue()
|
||||
{
|
||||
var rv = new Rv(period: 5, smoothingPeriod: 10);
|
||||
var prices = GeneratePriceSeries(20);
|
||||
|
||||
for (int i = 0; i < prices.Count; i++)
|
||||
{
|
||||
rv.Update(prices[i]);
|
||||
}
|
||||
|
||||
Assert.True(rv.IsHot);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Reset Tests
|
||||
|
||||
[Fact]
|
||||
public void Reset_ClearsState()
|
||||
{
|
||||
var rv = new Rv(period: 5, smoothingPeriod: 10);
|
||||
var prices = GeneratePriceSeries(20);
|
||||
|
||||
for (int i = 0; i < prices.Count; i++)
|
||||
{
|
||||
rv.Update(prices[i]);
|
||||
}
|
||||
|
||||
rv.Reset();
|
||||
|
||||
Assert.False(rv.IsHot);
|
||||
Assert.Equal(0, rv.Last.Value);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Reset_AllowsReprocessing()
|
||||
{
|
||||
var rv = new Rv(period: 5, smoothingPeriod: 10);
|
||||
var prices = GeneratePriceSeries(20);
|
||||
|
||||
for (int i = 0; i < prices.Count; i++)
|
||||
{
|
||||
rv.Update(prices[i]);
|
||||
}
|
||||
var firstResult = rv.Last.Value;
|
||||
|
||||
rv.Reset();
|
||||
for (int i = 0; i < prices.Count; i++)
|
||||
{
|
||||
rv.Update(prices[i]);
|
||||
}
|
||||
var secondResult = rv.Last.Value;
|
||||
|
||||
Assert.Equal(firstResult, secondResult, Tolerance);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Robustness Tests
|
||||
|
||||
[Fact]
|
||||
public void Update_WithNaNValues_UsesLastValidValue()
|
||||
{
|
||||
var rv = new Rv(period: 5, smoothingPeriod: 10);
|
||||
var prices = GeneratePriceSeries(20);
|
||||
|
||||
for (int i = 0; i < prices.Count; i++)
|
||||
{
|
||||
rv.Update(prices[i]);
|
||||
}
|
||||
var valueBeforeInvalid = rv.Last.Value;
|
||||
|
||||
var nanPrice = new TValue(DateTime.UtcNow, double.NaN);
|
||||
var result = rv.Update(nanPrice);
|
||||
|
||||
Assert.True(double.IsFinite(result.Value), "Result should be finite when using last valid value");
|
||||
Assert.Equal(valueBeforeInvalid, result.Value, Tolerance);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Update_WithInfinityValues_UsesLastValidValue()
|
||||
{
|
||||
var rv = new Rv(period: 5, smoothingPeriod: 10);
|
||||
var prices = GeneratePriceSeries(20);
|
||||
|
||||
for (int i = 0; i < prices.Count; i++)
|
||||
{
|
||||
rv.Update(prices[i]);
|
||||
}
|
||||
var valueBeforeInvalid = rv.Last.Value;
|
||||
|
||||
var infPrice = new TValue(DateTime.UtcNow, double.PositiveInfinity);
|
||||
var result = rv.Update(infPrice);
|
||||
|
||||
Assert.True(double.IsFinite(result.Value), "Result should be finite when using last valid value");
|
||||
Assert.Equal(valueBeforeInvalid, result.Value, Tolerance);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Update_WithZeroPrice_UsesLastValidValue()
|
||||
{
|
||||
var rv = new Rv(period: 5, smoothingPeriod: 10);
|
||||
var prices = GeneratePriceSeries(20);
|
||||
|
||||
for (int i = 0; i < prices.Count; i++)
|
||||
{
|
||||
rv.Update(prices[i]);
|
||||
}
|
||||
var valueBeforeInvalid = rv.Last.Value;
|
||||
|
||||
var zeroPrice = new TValue(DateTime.UtcNow, 0.0);
|
||||
var result = rv.Update(zeroPrice);
|
||||
|
||||
Assert.True(double.IsFinite(result.Value), "Result should be finite when using last valid value");
|
||||
Assert.Equal(valueBeforeInvalid, result.Value, Tolerance);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Update_WithNegativePrice_UsesLastValidValue()
|
||||
{
|
||||
var rv = new Rv(period: 5, smoothingPeriod: 10);
|
||||
var prices = GeneratePriceSeries(20);
|
||||
|
||||
for (int i = 0; i < prices.Count; i++)
|
||||
{
|
||||
rv.Update(prices[i]);
|
||||
}
|
||||
var valueBeforeInvalid = rv.Last.Value;
|
||||
|
||||
var negPrice = new TValue(DateTime.UtcNow, -100.0);
|
||||
var result = rv.Update(negPrice);
|
||||
|
||||
Assert.True(double.IsFinite(result.Value), "Result should be finite when using last valid value");
|
||||
Assert.Equal(valueBeforeInvalid, result.Value, Tolerance);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Batch and Series Tests
|
||||
|
||||
[Fact]
|
||||
public void Batch_MatchesStreamingResults()
|
||||
{
|
||||
const int dataCount = 100;
|
||||
var prices = GeneratePriceSeries(dataCount);
|
||||
|
||||
var rvStreaming = new Rv(period: 5, smoothingPeriod: 10);
|
||||
var streamingResults = new double[dataCount];
|
||||
for (int i = 0; i < dataCount; i++)
|
||||
{
|
||||
streamingResults[i] = rvStreaming.Update(prices[i]).Value;
|
||||
}
|
||||
|
||||
var batchResults = new double[dataCount];
|
||||
Rv.Batch(prices.Values, batchResults, period: 5, smoothingPeriod: 10);
|
||||
|
||||
for (int i = 50; i < dataCount; i++)
|
||||
{
|
||||
Assert.Equal(streamingResults[i], batchResults[i], Tolerance);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Calculate_TSeries_ReturnsCorrectLength()
|
||||
{
|
||||
const int dataCount = 50;
|
||||
var priceSeries = GeneratePriceSeries(dataCount);
|
||||
|
||||
var result = Rv.Batch(priceSeries, period: 5, smoothingPeriod: 10);
|
||||
|
||||
Assert.Equal(dataCount, result.Count);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Update_TSeries_MatchesStreamingResults()
|
||||
{
|
||||
const int dataCount = 50;
|
||||
var priceSeries = GeneratePriceSeries(dataCount);
|
||||
|
||||
var rvSeries = new Rv(period: 5, smoothingPeriod: 10);
|
||||
var seriesResult = rvSeries.Update(priceSeries);
|
||||
|
||||
var rvStreaming = new Rv(period: 5, smoothingPeriod: 10);
|
||||
var streamingResults = new double[dataCount];
|
||||
for (int i = 0; i < dataCount; i++)
|
||||
{
|
||||
streamingResults[i] = rvStreaming.Update(priceSeries[i]).Value;
|
||||
}
|
||||
|
||||
for (int i = 20; i < dataCount; i++)
|
||||
{
|
||||
Assert.Equal(streamingResults[i], seriesResult.Values[i], Tolerance);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Batch_EmptyInput_DoesNotThrow()
|
||||
{
|
||||
var prices = Array.Empty<double>();
|
||||
var output = Array.Empty<double>();
|
||||
|
||||
Rv.Batch(prices, output, period: 5, smoothingPeriod: 10);
|
||||
Assert.Empty(output);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Batch_OutputTooShort_ThrowsArgumentException()
|
||||
{
|
||||
var prices = new double[10];
|
||||
var output = new double[5];
|
||||
|
||||
var ex = Assert.Throws<ArgumentException>(() =>
|
||||
Rv.Batch(prices, output, period: 5, smoothingPeriod: 10));
|
||||
Assert.Equal("output", ex.ParamName);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Batch_InvalidPeriod_ThrowsArgumentException()
|
||||
{
|
||||
var prices = new double[10];
|
||||
var output = new double[10];
|
||||
|
||||
var ex = Assert.Throws<ArgumentException>(() =>
|
||||
Rv.Batch(prices, output, period: 0, smoothingPeriod: 10));
|
||||
Assert.Equal("period", ex.ParamName);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Batch_InvalidSmoothingPeriod_ThrowsArgumentException()
|
||||
{
|
||||
var prices = new double[10];
|
||||
var output = new double[10];
|
||||
|
||||
var ex = Assert.Throws<ArgumentException>(() =>
|
||||
Rv.Batch(prices, output, period: 5, smoothingPeriod: 0));
|
||||
Assert.Equal("smoothingPeriod", ex.ParamName);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Event Publishing Tests
|
||||
|
||||
[Fact]
|
||||
public void Update_PublishesEvent()
|
||||
{
|
||||
var rv = new Rv(period: 5, smoothingPeriod: 10);
|
||||
bool eventFired = false;
|
||||
rv.Pub += (object? sender, in TValueEventArgs args) => eventFired = true;
|
||||
|
||||
var price = new TValue(DateTime.UtcNow, 100.0);
|
||||
rv.Update(price);
|
||||
|
||||
Assert.True(eventFired);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ChainedIndicator_ReceivesValues()
|
||||
{
|
||||
var source = new Rv(period: 5, smoothingPeriod: 10);
|
||||
var downstream = new Sma(source, period: 3);
|
||||
|
||||
var prices = GeneratePriceSeries(30);
|
||||
for (int i = 0; i < prices.Count; i++)
|
||||
{
|
||||
source.Update(prices[i]);
|
||||
}
|
||||
|
||||
Assert.True(downstream.Last.Value > 0, "Downstream indicator should receive values");
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region TBar Update Tests
|
||||
|
||||
[Fact]
|
||||
public void Update_TBar_UsesClosePrice()
|
||||
{
|
||||
var rv1 = new Rv(period: 5, smoothingPeriod: 10);
|
||||
var rv2 = new Rv(period: 5, smoothingPeriod: 10);
|
||||
|
||||
var bar = new TBar(DateTime.UtcNow, 100.0, 105.0, 98.0, 102.0, 1000);
|
||||
rv1.Update(bar);
|
||||
|
||||
var tvalue = new TValue(bar.Time, bar.Close);
|
||||
rv2.Update(tvalue);
|
||||
|
||||
Assert.Equal(rv1.Last.Value, rv2.Last.Value, Tolerance);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Update_TBarSeries_ReturnsCorrectLength()
|
||||
{
|
||||
const int dataCount = 50;
|
||||
var barSeries = GenerateTestData(dataCount);
|
||||
|
||||
var rv = new Rv(period: 5, smoothingPeriod: 10);
|
||||
var result = rv.Update(barSeries);
|
||||
|
||||
Assert.Equal(dataCount, result.Count);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Additional Tests
|
||||
|
||||
[Fact]
|
||||
public void LargeDataset_Performance()
|
||||
{
|
||||
var rv = new Rv(period: 5, smoothingPeriod: 20);
|
||||
var prices = GeneratePriceSeries(5000);
|
||||
|
||||
for (int i = 0; i < prices.Count; i++)
|
||||
{
|
||||
var result = rv.Update(prices[i]);
|
||||
Assert.True(double.IsFinite(result.Value));
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void DifferentParameters_ProduceDistinctValues()
|
||||
{
|
||||
var prices = GeneratePriceSeries(50);
|
||||
|
||||
var rv1 = new Rv(period: 5, smoothingPeriod: 10);
|
||||
var rv2 = new Rv(period: 10, smoothingPeriod: 20);
|
||||
var rv3 = new Rv(period: 5, smoothingPeriod: 10, annualize: false);
|
||||
|
||||
for (int i = 0; i < prices.Count; i++)
|
||||
{
|
||||
rv1.Update(prices[i]);
|
||||
rv2.Update(prices[i]);
|
||||
rv3.Update(prices[i]);
|
||||
}
|
||||
|
||||
Assert.True(double.IsFinite(rv1.Last.Value));
|
||||
Assert.True(double.IsFinite(rv2.Last.Value));
|
||||
Assert.True(double.IsFinite(rv3.Last.Value));
|
||||
Assert.NotEqual(rv1.Last.Value, rv2.Last.Value);
|
||||
Assert.NotEqual(rv1.Last.Value, rv3.Last.Value);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void StaticCalculate_TSeries_Works()
|
||||
{
|
||||
var prices = GeneratePriceSeries(100);
|
||||
|
||||
var result = Rv.Batch(prices, period: 5, smoothingPeriod: 14);
|
||||
|
||||
Assert.Equal(100, result.Count);
|
||||
Assert.True(double.IsFinite(result[result.Count - 1].Value));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void StaticCalculate_TBarSeries_Works()
|
||||
{
|
||||
var bars = GenerateTestData(100);
|
||||
|
||||
var result = Rv.Batch(bars, period: 5, smoothingPeriod: 14);
|
||||
|
||||
Assert.Equal(100, result.Count);
|
||||
Assert.True(double.IsFinite(result[result.Count - 1].Value));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void StaticCalculate_ValidatesInput()
|
||||
{
|
||||
var prices = GeneratePriceSeries(10);
|
||||
|
||||
Assert.Throws<ArgumentException>(() => Rv.Batch(prices, period: 0));
|
||||
Assert.Throws<ArgumentException>(() => Rv.Batch(prices, period: -1));
|
||||
Assert.Throws<ArgumentException>(() => Rv.Batch(prices, period: 5, smoothingPeriod: 0));
|
||||
Assert.Throws<ArgumentException>(() => Rv.Batch(prices, period: 5, smoothingPeriod: 10, annualize: true, annualPeriods: 0));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Prime_Works()
|
||||
{
|
||||
var rv = new Rv(period: 3, smoothingPeriod: 5);
|
||||
var values = new double[] { 100.0, 101.0, 99.5, 102.0, 100.5, 103.0, 101.0, 104.0, 102.0, 105.0 };
|
||||
|
||||
rv.Prime(values);
|
||||
|
||||
Assert.True(rv.IsHot);
|
||||
Assert.True(double.IsFinite(rv.Last.Value));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void KnownValue_ManualCalculation()
|
||||
{
|
||||
// Test with known values: prices 100, 101, 102, 103 (3 returns)
|
||||
// Log returns: ln(101/100), ln(102/101), ln(103/102)
|
||||
// ≈ 0.00995, 0.00985, 0.00975
|
||||
// Squared returns sum, then sqrt, then SMA
|
||||
|
||||
var rv = new Rv(period: 3, smoothingPeriod: 2, annualize: false);
|
||||
var prices = new double[] { 100.0, 101.0, 102.0, 103.0, 104.0 };
|
||||
|
||||
for (int i = 0; i < prices.Length; i++)
|
||||
{
|
||||
rv.Update(new TValue(DateTime.UtcNow.AddMinutes(i), prices[i]));
|
||||
}
|
||||
|
||||
// The result should be positive and finite
|
||||
Assert.True(rv.Last.Value > 0);
|
||||
Assert.True(double.IsFinite(rv.Last.Value));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SmoothingEffect_ReducesNoise()
|
||||
{
|
||||
// Compare RV with different smoothing periods
|
||||
var prices = GeneratePriceSeries(100);
|
||||
|
||||
var rvShortSmooth = new Rv(period: 5, smoothingPeriod: 3, annualize: false);
|
||||
var rvLongSmooth = new Rv(period: 5, smoothingPeriod: 20, annualize: false);
|
||||
|
||||
var shortSmoothValues = new List<double>();
|
||||
var longSmoothValues = new List<double>();
|
||||
|
||||
for (int i = 0; i < prices.Count; i++)
|
||||
{
|
||||
shortSmoothValues.Add(rvShortSmooth.Update(prices[i]).Value);
|
||||
longSmoothValues.Add(rvLongSmooth.Update(prices[i]).Value);
|
||||
}
|
||||
|
||||
// Calculate variance of last 50 values
|
||||
double VarianceOfLast50(List<double> vals)
|
||||
{
|
||||
var last50 = vals.Skip(vals.Count - 50).ToList();
|
||||
double mean = last50.Average();
|
||||
return last50.Sum(v => (v - mean) * (v - mean)) / last50.Count;
|
||||
}
|
||||
|
||||
double shortVariance = VarianceOfLast50(shortSmoothValues);
|
||||
double longVariance = VarianceOfLast50(longSmoothValues);
|
||||
|
||||
// Longer smoothing should have lower variance (smoother)
|
||||
Assert.True(longVariance < shortVariance, "Longer smoothing should produce smoother output");
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
@@ -0,0 +1,561 @@
|
||||
namespace QuanTAlib.Test;
|
||||
|
||||
using Xunit;
|
||||
|
||||
/// <summary>
|
||||
/// Validation tests for RV (Realized Volatility).
|
||||
/// RV calculates volatility from squared log returns, smoothed with SMA.
|
||||
/// Formula: RV = SMA(√(Σr²)) × annualizationFactor
|
||||
/// </summary>
|
||||
public class RvValidationTests
|
||||
{
|
||||
private static TBarSeries GenerateTestData(int count = 100)
|
||||
{
|
||||
var gbm = new GBM(seed: 42);
|
||||
return gbm.Fetch(count, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1));
|
||||
}
|
||||
|
||||
private static TSeries GeneratePriceSeries(int count = 100)
|
||||
{
|
||||
var gbm = new GBM(seed: 42);
|
||||
var bars = gbm.Fetch(count, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1));
|
||||
var t = new List<long>(count);
|
||||
var v = new List<double>(count);
|
||||
for (int i = 0; i < count; i++)
|
||||
{
|
||||
t.Add(bars[i].Time);
|
||||
v.Add(bars[i].Close);
|
||||
}
|
||||
return new TSeries(t, v);
|
||||
}
|
||||
|
||||
// === Mathematical Validation ===
|
||||
|
||||
/// <summary>
|
||||
/// Validates squared log return calculation.
|
||||
/// </summary>
|
||||
[Theory]
|
||||
[InlineData(100.0, 101.0)]
|
||||
[InlineData(100.0, 110.0)]
|
||||
[InlineData(100.0, 90.0)]
|
||||
public void Rv_SquaredLogReturn_IsCorrect(double prevPrice, double curPrice)
|
||||
{
|
||||
double logReturn = Math.Log(curPrice / prevPrice);
|
||||
double squaredReturn = logReturn * logReturn;
|
||||
|
||||
Assert.True(squaredReturn >= 0, "Squared return must be non-negative");
|
||||
Assert.Equal(Math.Pow(logReturn, 2), squaredReturn, 15);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Validates realized variance formula: sum of squared returns.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void Rv_RealizedVarianceFormula_IsCorrect()
|
||||
{
|
||||
double[] squaredReturns = { 0.0001, 0.0004, 0.0009, 0.0016, 0.0025 };
|
||||
double sumSquared = 0;
|
||||
for (int i = 0; i < squaredReturns.Length; i++)
|
||||
{
|
||||
sumSquared += squaredReturns[i];
|
||||
}
|
||||
|
||||
// Expected sum = 0.0055
|
||||
Assert.Equal(0.0055, sumSquared, 10);
|
||||
|
||||
// Realized volatility = sqrt(sum)
|
||||
double rv = Math.Sqrt(sumSquared);
|
||||
Assert.Equal(Math.Sqrt(0.0055), rv, 10);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Validates annualization factor: √(252) for daily data.
|
||||
/// </summary>
|
||||
[Theory]
|
||||
[InlineData(252, 15.8745078663875)]
|
||||
[InlineData(365, 19.1049731745428)]
|
||||
[InlineData(52, 7.21110255092798)]
|
||||
public void Rv_AnnualizationFactor_IsCorrect(int annualPeriods, double expectedFactor)
|
||||
{
|
||||
double factor = Math.Sqrt(annualPeriods);
|
||||
Assert.Equal(expectedFactor, factor, 10);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Validates known calculation with manual verification.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void Rv_KnownCalculation_IsCorrect()
|
||||
{
|
||||
// Prices: 100, 102, 101, 103, 102, 104 (6 prices = 5 returns)
|
||||
double[] prices = { 100.0, 102.0, 101.0, 103.0, 102.0, 104.0 };
|
||||
|
||||
// Manual calculation with period=5 (all 5 returns), smoothingPeriod=1 (no smoothing)
|
||||
double sumSquared = 0;
|
||||
for (int i = 1; i < prices.Length; i++)
|
||||
{
|
||||
double r = Math.Log(prices[i] / prices[i - 1]);
|
||||
sumSquared += r * r;
|
||||
}
|
||||
double expected = Math.Sqrt(sumSquared);
|
||||
|
||||
// Verify with indicator (no annualization, smoothing=1)
|
||||
var rv = new Rv(period: 5, smoothingPeriod: 1, annualize: false);
|
||||
for (int i = 0; i < prices.Length; i++)
|
||||
{
|
||||
rv.Update(new TValue(DateTime.UtcNow.AddMinutes(i), prices[i]));
|
||||
}
|
||||
|
||||
Assert.Equal(expected, rv.Last.Value, 10);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Validates constant prices produce zero volatility.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void Rv_ConstantPrices_ProducesZeroVolatility()
|
||||
{
|
||||
var rv = new Rv(period: 5, smoothingPeriod: 3, annualize: false);
|
||||
|
||||
for (int i = 0; i < 20; i++)
|
||||
{
|
||||
rv.Update(new TValue(DateTime.UtcNow.AddMinutes(i), 100.0));
|
||||
}
|
||||
|
||||
Assert.Equal(0.0, rv.Last.Value, 10);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Validates SMA smoothing of raw volatilities.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void Rv_SmaSmoothing_WorksCorrectly()
|
||||
{
|
||||
var prices = GeneratePriceSeries(50);
|
||||
|
||||
// Short smoothing vs long smoothing
|
||||
var rvShort = new Rv(period: 5, smoothingPeriod: 3, annualize: false);
|
||||
var rvLong = new Rv(period: 5, smoothingPeriod: 10, annualize: false);
|
||||
|
||||
var shortResults = new List<double>();
|
||||
var longResults = new List<double>();
|
||||
|
||||
for (int i = 0; i < prices.Count; i++)
|
||||
{
|
||||
rvShort.Update(prices[i]);
|
||||
rvLong.Update(prices[i]);
|
||||
|
||||
if (rvShort.IsHot && rvLong.IsHot)
|
||||
{
|
||||
shortResults.Add(rvShort.Last.Value);
|
||||
longResults.Add(rvLong.Last.Value);
|
||||
}
|
||||
}
|
||||
|
||||
// Longer smoothing should produce smoother (less variable) results
|
||||
double shortVar = Variance(shortResults);
|
||||
double longVar = Variance(longResults);
|
||||
|
||||
Assert.True(shortResults.Count > 0, "Should have results");
|
||||
Assert.True(longVar < shortVar, "Longer smoothing should be smoother");
|
||||
}
|
||||
|
||||
// === Consistency Tests ===
|
||||
|
||||
/// <summary>
|
||||
/// Validates streaming and batch produce identical results.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void Rv_StreamingMatchesBatch()
|
||||
{
|
||||
var prices = GeneratePriceSeries(100);
|
||||
|
||||
// Streaming calculation
|
||||
var streamingRv = new Rv(5, 10);
|
||||
for (int i = 0; i < prices.Count; i++)
|
||||
{
|
||||
streamingRv.Update(prices[i]);
|
||||
}
|
||||
|
||||
// Batch calculation
|
||||
var batchResult = Rv.Batch(prices, 5, 10);
|
||||
|
||||
Assert.Equal(batchResult.Last.Value, streamingRv.Last.Value, 8);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Validates TSeries input matches TValue streaming.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void Rv_TSeriesInput_MatchesStreaming()
|
||||
{
|
||||
var prices = GeneratePriceSeries(100);
|
||||
|
||||
// Streaming
|
||||
var streamingRv = new Rv(5, 10);
|
||||
for (int i = 0; i < prices.Count; i++)
|
||||
{
|
||||
streamingRv.Update(prices[i]);
|
||||
}
|
||||
|
||||
// TSeries batch
|
||||
var batchRv = new Rv(5, 10);
|
||||
var batchResult = batchRv.Update(prices);
|
||||
|
||||
Assert.Equal(batchResult.Last.Value, streamingRv.Last.Value, 10);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Validates annualized output is scaled correctly.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void Rv_Annualized_ScaledCorrectly()
|
||||
{
|
||||
var prices = GeneratePriceSeries(50);
|
||||
|
||||
var rvRaw = new Rv(5, 10, annualize: false);
|
||||
var rvAnn = new Rv(5, 10, annualize: true, annualPeriods: 252);
|
||||
|
||||
for (int i = 0; i < prices.Count; i++)
|
||||
{
|
||||
rvRaw.Update(prices[i]);
|
||||
rvAnn.Update(prices[i]);
|
||||
}
|
||||
|
||||
double expectedRatio = Math.Sqrt(252);
|
||||
double actualRatio = rvAnn.Last.Value / rvRaw.Last.Value;
|
||||
|
||||
Assert.Equal(expectedRatio, actualRatio, 6);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Validates TBar update uses only Close price.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void Rv_TBar_UsesOnlyClose()
|
||||
{
|
||||
var bars = GenerateTestData(50);
|
||||
|
||||
var rvBar = new Rv(5, 10);
|
||||
for (int i = 0; i < bars.Count; i++)
|
||||
{
|
||||
rvBar.Update(bars[i]);
|
||||
}
|
||||
|
||||
var rvClose = new Rv(5, 10);
|
||||
for (int i = 0; i < bars.Count; i++)
|
||||
{
|
||||
rvClose.Update(new TValue(bars[i].Time, bars[i].Close));
|
||||
}
|
||||
|
||||
Assert.Equal(rvClose.Last.Value, rvBar.Last.Value, 10);
|
||||
}
|
||||
|
||||
// === Parameter Sensitivity ===
|
||||
|
||||
/// <summary>
|
||||
/// Validates shorter period is more responsive.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void Rv_ShorterPeriod_MoreResponsive()
|
||||
{
|
||||
var prices = GeneratePriceSeries(50);
|
||||
|
||||
var rvShort = new Rv(3, 5);
|
||||
var rvLong = new Rv(10, 5);
|
||||
|
||||
var shortResults = new List<double>();
|
||||
var longResults = new List<double>();
|
||||
|
||||
for (int i = 0; i < prices.Count; i++)
|
||||
{
|
||||
rvShort.Update(prices[i]);
|
||||
rvLong.Update(prices[i]);
|
||||
|
||||
if (rvShort.IsHot && rvLong.IsHot)
|
||||
{
|
||||
shortResults.Add(rvShort.Last.Value);
|
||||
longResults.Add(rvLong.Last.Value);
|
||||
}
|
||||
}
|
||||
|
||||
double shortVar = Variance(shortResults);
|
||||
double longVar = Variance(longResults);
|
||||
|
||||
Assert.True(shortResults.Count > 0, "Should have results");
|
||||
Assert.True(shortVar > longVar * 0.5, "Shorter period should be more variable");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Validates different parameters produce different results.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void Rv_DifferentParameters_ProduceDifferentResults()
|
||||
{
|
||||
var prices = GeneratePriceSeries(50);
|
||||
|
||||
var rv1 = new Rv(5, 10);
|
||||
var rv2 = new Rv(5, 20);
|
||||
var rv3 = new Rv(10, 10);
|
||||
|
||||
for (int i = 0; i < prices.Count; i++)
|
||||
{
|
||||
rv1.Update(prices[i]);
|
||||
rv2.Update(prices[i]);
|
||||
rv3.Update(prices[i]);
|
||||
}
|
||||
|
||||
Assert.NotEqual(rv1.Last.Value, rv2.Last.Value);
|
||||
Assert.NotEqual(rv1.Last.Value, rv3.Last.Value);
|
||||
}
|
||||
|
||||
// === Edge Cases ===
|
||||
|
||||
/// <summary>
|
||||
/// Validates handling of very small price changes.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void Rv_VerySmallChanges_HandledCorrectly()
|
||||
{
|
||||
var rv = new Rv(5, 10, annualize: false);
|
||||
|
||||
double price = 100.0;
|
||||
for (int i = 0; i < 30; i++)
|
||||
{
|
||||
price += 0.001 * (i % 2 == 0 ? 1 : -1);
|
||||
rv.Update(new TValue(DateTime.UtcNow.AddMinutes(i), price));
|
||||
}
|
||||
|
||||
Assert.True(double.IsFinite(rv.Last.Value));
|
||||
Assert.True(rv.Last.Value >= 0);
|
||||
Assert.True(rv.Last.Value < 0.01, "Small changes should produce small RV");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Validates handling of large price swings.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void Rv_LargePriceSwings_HandledCorrectly()
|
||||
{
|
||||
var rv = new Rv(5, 10, annualize: false);
|
||||
|
||||
double price = 100.0;
|
||||
for (int i = 0; i < 30; i++)
|
||||
{
|
||||
price *= (i % 2 == 0 ? 1.1 : 0.9);
|
||||
rv.Update(new TValue(DateTime.UtcNow.AddMinutes(i), price));
|
||||
}
|
||||
|
||||
Assert.True(double.IsFinite(rv.Last.Value));
|
||||
Assert.True(rv.Last.Value > 0, "Large swings should produce positive RV");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Validates warmup period calculation.
|
||||
/// </summary>
|
||||
[Theory]
|
||||
[InlineData(5, 10, 15)]
|
||||
[InlineData(5, 20, 25)]
|
||||
[InlineData(10, 10, 20)]
|
||||
public void Rv_WarmupPeriod_IsCorrect(int period, int smoothing, int expectedWarmup)
|
||||
{
|
||||
var rv = new Rv(period, smoothing);
|
||||
Assert.Equal(expectedWarmup, rv.WarmupPeriod);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Validates output is always non-negative.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void Rv_Output_IsNonNegative()
|
||||
{
|
||||
var prices = GeneratePriceSeries(100);
|
||||
var rv = new Rv(5, 10);
|
||||
|
||||
for (int i = 0; i < prices.Count; i++)
|
||||
{
|
||||
rv.Update(prices[i]);
|
||||
if (rv.IsHot)
|
||||
{
|
||||
Assert.True(rv.Last.Value >= 0, $"RV should be non-negative at bar {i}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Validates bar correction works correctly.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void Rv_BarCorrection_WorksCorrectly()
|
||||
{
|
||||
var rv = new Rv(5, 10);
|
||||
var prices = GeneratePriceSeries(30);
|
||||
|
||||
for (int i = 0; i < 20; i++)
|
||||
{
|
||||
rv.Update(prices[i], isNew: true);
|
||||
}
|
||||
|
||||
rv.Update(prices[20], isNew: true);
|
||||
double afterNew = rv.Last.Value;
|
||||
|
||||
var correctedPrice = new TValue(prices[20].Time, prices[20].Value * 2.0);
|
||||
rv.Update(correctedPrice, isNew: false);
|
||||
double afterCorrection = rv.Last.Value;
|
||||
|
||||
rv.Update(prices[20], isNew: false);
|
||||
double afterRestore = rv.Last.Value;
|
||||
|
||||
Assert.NotEqual(afterNew, afterCorrection);
|
||||
Assert.Equal(afterNew, afterRestore, 10);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Validates iterative corrections converge.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void Rv_IterativeCorrections_Converge()
|
||||
{
|
||||
var rv = new Rv(5, 10);
|
||||
var prices = GeneratePriceSeries(30);
|
||||
|
||||
for (int i = 0; i < 20; i++)
|
||||
{
|
||||
rv.Update(prices[i], isNew: true);
|
||||
}
|
||||
|
||||
for (int j = 0; j < 5; j++)
|
||||
{
|
||||
var tempPrice = new TValue(prices[19].Time, prices[19].Value * (1.0 + j * 0.01));
|
||||
rv.Update(tempPrice, isNew: false);
|
||||
}
|
||||
|
||||
rv.Update(prices[19], isNew: false);
|
||||
double afterCorrections = rv.Last.Value;
|
||||
|
||||
var rvFresh = new Rv(5, 10);
|
||||
for (int i = 0; i < 20; i++)
|
||||
{
|
||||
rvFresh.Update(prices[i], isNew: true);
|
||||
}
|
||||
double freshValue = rvFresh.Last.Value;
|
||||
|
||||
Assert.Equal(freshValue, afterCorrections, 10);
|
||||
}
|
||||
|
||||
// === Comparison Tests ===
|
||||
|
||||
/// <summary>
|
||||
/// Validates RV vs HV produce correlated but different results.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void Rv_VsHv_RelatedButDifferent()
|
||||
{
|
||||
var bars = GenerateTestData(50);
|
||||
|
||||
// RV with period=14, smoothing=1 (similar to HV behavior)
|
||||
var rv = new Rv(14, 1, annualize: false);
|
||||
var hv = new Hv(14, annualize: false);
|
||||
|
||||
for (int i = 0; i < bars.Count; i++)
|
||||
{
|
||||
rv.Update(bars[i]);
|
||||
hv.Update(bars[i]);
|
||||
}
|
||||
|
||||
// Both should produce positive values
|
||||
Assert.True(rv.Last.Value > 0);
|
||||
Assert.True(hv.Last.Value > 0);
|
||||
|
||||
// They measure similar concepts but with different formulas
|
||||
// RV uses sum of squared returns, HV uses standard deviation
|
||||
// Both should be in similar magnitude range
|
||||
double ratio = rv.Last.Value / hv.Last.Value;
|
||||
Assert.True(ratio > 0.1 && ratio < 10, "RV and HV should be in similar range");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Validates stability over repeated runs with same seed.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void Rv_Stability_ConsistentOverRepeatedRuns()
|
||||
{
|
||||
var results = new List<double>();
|
||||
|
||||
for (int run = 0; run < 3; run++)
|
||||
{
|
||||
var gbm = new GBM(seed: 42);
|
||||
var bars = gbm.Fetch(100, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1));
|
||||
var rv = new Rv(5, 10);
|
||||
|
||||
for (int i = 0; i < bars.Count; i++)
|
||||
{
|
||||
rv.Update(bars[i]);
|
||||
}
|
||||
results.Add(rv.Last.Value);
|
||||
}
|
||||
|
||||
Assert.Equal(results[0], results[1], 15);
|
||||
Assert.Equal(results[1], results[2], 15);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Validates RV responds to volatility regime changes.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void Rv_RespondsToVolatilityRegimeChange()
|
||||
{
|
||||
var rv = new Rv(5, 5, annualize: false);
|
||||
|
||||
// Low volatility regime
|
||||
double price = 100.0;
|
||||
for (int i = 0; i < 20; i++)
|
||||
{
|
||||
price *= (i % 2 == 0 ? 1.001 : 0.999);
|
||||
rv.Update(new TValue(DateTime.UtcNow.AddMinutes(i), price));
|
||||
}
|
||||
double lowVolValue = rv.Last.Value;
|
||||
|
||||
// High volatility regime
|
||||
for (int i = 20; i < 40; i++)
|
||||
{
|
||||
price *= (i % 2 == 0 ? 1.05 : 0.95);
|
||||
rv.Update(new TValue(DateTime.UtcNow.AddMinutes(i), price));
|
||||
}
|
||||
double highVolValue = rv.Last.Value;
|
||||
|
||||
Assert.True(highVolValue > lowVolValue * 5,
|
||||
"RV should significantly increase with higher volatility regime");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Validates RV produces reasonable volatility estimate.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void Rv_ProducesReasonableVolatilityEstimate()
|
||||
{
|
||||
var prices = GeneratePriceSeries(100);
|
||||
var rv = new Rv(5, 10, annualize: false);
|
||||
|
||||
for (int i = 0; i < prices.Count; i++)
|
||||
{
|
||||
rv.Update(prices[i]);
|
||||
}
|
||||
|
||||
Assert.True(double.IsFinite(rv.Last.Value));
|
||||
Assert.True(rv.Last.Value > 0);
|
||||
Assert.True(rv.Last.Value < 1, "Raw RV should be < 100%");
|
||||
}
|
||||
|
||||
// === Helper Methods ===
|
||||
|
||||
private static double Variance(List<double> values)
|
||||
{
|
||||
if (values.Count == 0)
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
double mean = values.Average();
|
||||
return values.Average(v => Math.Pow(v - mean, 2));
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user