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,384 @@
|
||||
using TradingPlatform.BusinessLayer;
|
||||
|
||||
namespace QuanTAlib.Tests;
|
||||
|
||||
public class VrIndicatorTests
|
||||
{
|
||||
[Fact]
|
||||
public void VrIndicator_Constructor_SetsDefaults()
|
||||
{
|
||||
var indicator = new VrIndicator();
|
||||
|
||||
Assert.Equal(14, indicator.Period);
|
||||
Assert.True(indicator.ShowColdValues);
|
||||
Assert.Equal("VR - Volatility Ratio", indicator.Name);
|
||||
Assert.True(indicator.SeparateWindow);
|
||||
Assert.True(indicator.OnBackGround);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void VrIndicator_ShortName_IncludesParameters()
|
||||
{
|
||||
var indicator = new VrIndicator { Period = 20 };
|
||||
Assert.Contains("VR", indicator.ShortName, StringComparison.Ordinal);
|
||||
Assert.Contains("20", indicator.ShortName, StringComparison.Ordinal);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void VrIndicator_MinHistoryDepths_EqualsZero()
|
||||
{
|
||||
var indicator = new VrIndicator();
|
||||
|
||||
Assert.Equal(0, VrIndicator.MinHistoryDepths);
|
||||
Assert.Equal(0, ((IWatchlistIndicator)indicator).MinHistoryDepths);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void VrIndicator_Initialize_CreatesInternalVr()
|
||||
{
|
||||
var indicator = new VrIndicator();
|
||||
|
||||
// Initialize should not throw
|
||||
indicator.Initialize();
|
||||
|
||||
// After init, line series should exist
|
||||
Assert.Single(indicator.LinesSeries);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void VrIndicator_ProcessUpdate_HistoricalBar_ComputesValue()
|
||||
{
|
||||
var indicator = new VrIndicator { Period = 10 };
|
||||
indicator.Initialize();
|
||||
|
||||
// Add historical data with varying volatility
|
||||
var now = DateTime.UtcNow;
|
||||
for (int i = 0; i < 50; i++)
|
||||
{
|
||||
// Create price movement that generates volatility
|
||||
double basePrice = 100 + Math.Sin(i * 0.3) * (5 + i * 0.1);
|
||||
indicator.HistoricalData.AddBar(now.AddMinutes(i), basePrice, basePrice + 2, basePrice - 2, basePrice, 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, "VR should be non-negative");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void VrIndicator_ProcessUpdate_NewBar_ComputesValue()
|
||||
{
|
||||
var indicator = new VrIndicator { Period = 10 };
|
||||
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 + 2, basePrice - 2, basePrice + 1, 1000);
|
||||
}
|
||||
|
||||
indicator.ProcessUpdate(new UpdateArgs(UpdateReason.HistoricalBar));
|
||||
|
||||
// Add new bar
|
||||
indicator.HistoricalData.AddBar(now.AddMinutes(30), 130, 135, 125, 132, 1500);
|
||||
indicator.ProcessUpdate(new UpdateArgs(UpdateReason.NewBar));
|
||||
|
||||
Assert.Equal(2, indicator.LinesSeries[0].Count);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void VrIndicator_DifferentPeriods_Work()
|
||||
{
|
||||
var periods = new[] { 5, 10, 14, 20 };
|
||||
|
||||
foreach (int period in periods)
|
||||
{
|
||||
var indicator = new VrIndicator { Period = period };
|
||||
indicator.Initialize();
|
||||
|
||||
var now = DateTime.UtcNow;
|
||||
for (int i = 0; i < 60; i++)
|
||||
{
|
||||
// Create price movement with varying amplitude
|
||||
double basePrice = 100 + Math.Sin(i * 0.2) * 5;
|
||||
indicator.HistoricalData.AddBar(now.AddMinutes(i), basePrice, basePrice + 2, basePrice - 2, basePrice, 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 VrIndicator_Period_CanBeChanged()
|
||||
{
|
||||
var indicator = new VrIndicator();
|
||||
Assert.Equal(14, indicator.Period);
|
||||
|
||||
indicator.Period = 20;
|
||||
Assert.Equal(20, indicator.Period);
|
||||
|
||||
indicator.Period = 10;
|
||||
Assert.Equal(10, indicator.Period);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void VrIndicator_ShowColdValues_CanBeToggled()
|
||||
{
|
||||
var indicator = new VrIndicator();
|
||||
Assert.True(indicator.ShowColdValues);
|
||||
|
||||
indicator.ShowColdValues = false;
|
||||
Assert.False(indicator.ShowColdValues);
|
||||
|
||||
indicator.ShowColdValues = true;
|
||||
Assert.True(indicator.ShowColdValues);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void VrIndicator_SourceCodeLink_IsValid()
|
||||
{
|
||||
var indicator = new VrIndicator();
|
||||
Assert.Contains("github.com", indicator.SourceCodeLink, StringComparison.Ordinal);
|
||||
Assert.Contains("Vr.Quantower.cs", indicator.SourceCodeLink, StringComparison.Ordinal);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void VrIndicator_ConstantPrice_ProducesNearOne()
|
||||
{
|
||||
var indicator = new VrIndicator { Period = 10 };
|
||||
indicator.Initialize();
|
||||
|
||||
var now = DateTime.UtcNow;
|
||||
|
||||
// Constant price with small range - TR ≈ ATR so VR ≈ 1
|
||||
for (int i = 0; i < 30; i++)
|
||||
{
|
||||
indicator.HistoricalData.AddBar(now.AddMinutes(i), 100, 101, 99, 100, 1000);
|
||||
indicator.ProcessUpdate(new UpdateArgs(UpdateReason.HistoricalBar));
|
||||
}
|
||||
|
||||
double val = indicator.LinesSeries[0].GetValue(0);
|
||||
Assert.True(double.IsFinite(val));
|
||||
// VR should be around 1 when volatility is constant
|
||||
Assert.True(val >= 0.5 && val <= 2.0, $"Constant volatility should produce VR near 1, got {val}");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void VrIndicator_HighVolatility_ProducesPositiveValue()
|
||||
{
|
||||
var indicator = new VrIndicator { Period = 5 };
|
||||
indicator.Initialize();
|
||||
|
||||
var now = DateTime.UtcNow;
|
||||
|
||||
// High volatility with large price swings
|
||||
for (int i = 0; i < 30; i++)
|
||||
{
|
||||
double price = 100 + (i % 2 == 0 ? 10 : -10); // Large oscillations
|
||||
indicator.HistoricalData.AddBar(now.AddMinutes(i), price, price + 5, price - 5, price, 1000);
|
||||
indicator.ProcessUpdate(new UpdateArgs(UpdateReason.HistoricalBar));
|
||||
}
|
||||
|
||||
double val = indicator.LinesSeries[0].GetValue(0);
|
||||
Assert.True(double.IsFinite(val));
|
||||
Assert.True(val > 0, "High volatility should produce positive VR value");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void VrIndicator_UsesHLC_ForCalculation()
|
||||
{
|
||||
// VR uses HLC (True Range / ATR)
|
||||
var indicator = new VrIndicator { Period = 5 };
|
||||
indicator.Initialize();
|
||||
|
||||
var now = DateTime.UtcNow;
|
||||
|
||||
// Price with varying HLC
|
||||
for (int i = 0; i < 20; i++)
|
||||
{
|
||||
double close = 100 + Math.Sin(i * 0.3) * 3;
|
||||
double high = close + 2 + Math.Abs(Math.Sin(i * 0.5));
|
||||
double low = close - 2 - Math.Abs(Math.Cos(i * 0.5));
|
||||
indicator.HistoricalData.AddBar(now.AddMinutes(i), close, high, low, close, 1000);
|
||||
indicator.ProcessUpdate(new UpdateArgs(UpdateReason.HistoricalBar));
|
||||
}
|
||||
|
||||
double val = indicator.LinesSeries[0].GetValue(0);
|
||||
|
||||
Assert.True(double.IsFinite(val));
|
||||
Assert.True(val >= 0, "VR should be non-negative");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void VrIndicator_BreakoutDetection_HighRatio()
|
||||
{
|
||||
var indicator = new VrIndicator { Period = 10 };
|
||||
indicator.Initialize();
|
||||
|
||||
var now = DateTime.UtcNow;
|
||||
|
||||
// Calm period - small ranges
|
||||
for (int i = 0; i < 20; i++)
|
||||
{
|
||||
indicator.HistoricalData.AddBar(now.AddMinutes(i), 100, 101, 99, 100, 1000);
|
||||
indicator.ProcessUpdate(new UpdateArgs(UpdateReason.HistoricalBar));
|
||||
}
|
||||
|
||||
double calmVr = indicator.LinesSeries[0].GetValue(0);
|
||||
|
||||
// Breakout - large range
|
||||
indicator.HistoricalData.AddBar(now.AddMinutes(20), 100, 115, 85, 110, 5000);
|
||||
indicator.ProcessUpdate(new UpdateArgs(UpdateReason.NewBar));
|
||||
|
||||
double breakoutVr = indicator.LinesSeries[0].GetValue(0);
|
||||
|
||||
Assert.True(double.IsFinite(calmVr));
|
||||
Assert.True(double.IsFinite(breakoutVr));
|
||||
Assert.True(breakoutVr > calmVr, "Breakout should produce higher VR than calm period");
|
||||
Assert.True(breakoutVr > 1.5, "Breakout VR should be significantly above 1");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void VrIndicator_LargerPeriod_SmootherATR()
|
||||
{
|
||||
var indicator1 = new VrIndicator { Period = 5 };
|
||||
var indicator2 = new VrIndicator { Period = 20 };
|
||||
indicator1.Initialize();
|
||||
indicator2.Initialize();
|
||||
|
||||
var now = DateTime.UtcNow;
|
||||
var results1 = new List<double>();
|
||||
var results2 = new List<double>();
|
||||
|
||||
for (int i = 0; i < 60; i++)
|
||||
{
|
||||
double price = 100 + Math.Sin(i * 0.3) * 5;
|
||||
indicator1.HistoricalData.AddBar(now.AddMinutes(i), price, price + 2, price - 2, price, 1000);
|
||||
indicator2.HistoricalData.AddBar(now.AddMinutes(i), price, price + 2, price - 2, price, 1000);
|
||||
indicator1.ProcessUpdate(new UpdateArgs(UpdateReason.HistoricalBar));
|
||||
indicator2.ProcessUpdate(new UpdateArgs(UpdateReason.HistoricalBar));
|
||||
|
||||
if (i >= 25) // After both are fully warmed up
|
||||
{
|
||||
results1.Add(indicator1.LinesSeries[0].GetValue(0));
|
||||
results2.Add(indicator2.LinesSeries[0].GetValue(0));
|
||||
}
|
||||
}
|
||||
|
||||
// Both should produce valid values
|
||||
Assert.True(results1.All(double.IsFinite));
|
||||
Assert.True(results2.All(double.IsFinite));
|
||||
}
|
||||
|
||||
private static double CalculateChangeVariance(List<double> values)
|
||||
{
|
||||
if (values.Count < 2)
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
|
||||
var changes = new List<double>();
|
||||
for (int i = 1; i < values.Count; i++)
|
||||
{
|
||||
changes.Add(values[i] - values[i - 1]);
|
||||
}
|
||||
|
||||
double mean = changes.Average();
|
||||
double variance = changes.Select(c => (c - mean) * (c - mean)).Average();
|
||||
return variance;
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void VrIndicator_GapUp_IncreasesRatio()
|
||||
{
|
||||
var indicator = new VrIndicator { Period = 10 };
|
||||
indicator.Initialize();
|
||||
|
||||
var now = DateTime.UtcNow;
|
||||
|
||||
// Normal trading
|
||||
for (int i = 0; i < 15; i++)
|
||||
{
|
||||
indicator.HistoricalData.AddBar(now.AddMinutes(i), 100, 101, 99, 100, 1000);
|
||||
indicator.ProcessUpdate(new UpdateArgs(UpdateReason.HistoricalBar));
|
||||
}
|
||||
|
||||
double beforeGap = indicator.LinesSeries[0].GetValue(0);
|
||||
|
||||
// Large gap up - TR will be large due to gap from previous close
|
||||
indicator.HistoricalData.AddBar(now.AddMinutes(15), 110, 115, 108, 112, 2000);
|
||||
indicator.ProcessUpdate(new UpdateArgs(UpdateReason.NewBar));
|
||||
|
||||
double afterGap = indicator.LinesSeries[0].GetValue(0);
|
||||
|
||||
Assert.True(double.IsFinite(beforeGap));
|
||||
Assert.True(double.IsFinite(afterGap));
|
||||
Assert.True(afterGap > beforeGap, "Gap should increase VR");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void VrIndicator_VolatilityExpansion_RespondsQuickly()
|
||||
{
|
||||
var indicator = new VrIndicator { Period = 5 };
|
||||
indicator.Initialize();
|
||||
|
||||
var now = DateTime.UtcNow;
|
||||
|
||||
// Low volatility period
|
||||
for (int i = 0; i < 15; i++)
|
||||
{
|
||||
indicator.HistoricalData.AddBar(now.AddMinutes(i), 100, 100.5, 99.5, 100, 1000);
|
||||
indicator.ProcessUpdate(new UpdateArgs(UpdateReason.HistoricalBar));
|
||||
}
|
||||
|
||||
double lowVolVr = indicator.LinesSeries[0].GetValue(0);
|
||||
|
||||
// Sudden volatility expansion
|
||||
indicator.HistoricalData.AddBar(now.AddMinutes(15), 100, 110, 90, 105, 5000);
|
||||
indicator.ProcessUpdate(new UpdateArgs(UpdateReason.NewBar));
|
||||
|
||||
double expansionVr = indicator.LinesSeries[0].GetValue(0);
|
||||
|
||||
Assert.True(double.IsFinite(lowVolVr));
|
||||
Assert.True(double.IsFinite(expansionVr));
|
||||
Assert.True(expansionVr > lowVolVr * 2, "VR should respond quickly to volatility expansion");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void VrIndicator_TypicalValues_AroundOne()
|
||||
{
|
||||
var indicator = new VrIndicator { Period = 14 };
|
||||
indicator.Initialize();
|
||||
|
||||
var now = DateTime.UtcNow;
|
||||
var values = new List<double>();
|
||||
|
||||
// Normal market with consistent volatility
|
||||
for (int i = 0; i < 100; i++)
|
||||
{
|
||||
double price = 100 + Math.Sin(i * 0.1) * 2;
|
||||
double range = 2 + Math.Sin(i * 0.2) * 0.5; // Consistent range
|
||||
indicator.HistoricalData.AddBar(now.AddMinutes(i), price, price + range, price - range, price, 1000);
|
||||
indicator.ProcessUpdate(new UpdateArgs(UpdateReason.HistoricalBar));
|
||||
|
||||
if (i >= 20)
|
||||
{
|
||||
values.Add(indicator.LinesSeries[0].GetValue(0));
|
||||
}
|
||||
}
|
||||
|
||||
double avgVr = values.Average();
|
||||
|
||||
// In steady state with consistent volatility, VR should hover around 1
|
||||
Assert.True(avgVr >= 0.5 && avgVr <= 2.0, $"Average VR should be around 1, got {avgVr}");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,628 @@
|
||||
// Volatility Ratio (VR) Unit Tests
|
||||
|
||||
using Xunit;
|
||||
|
||||
namespace QuanTAlib.Tests;
|
||||
|
||||
public class VrTests
|
||||
{
|
||||
private readonly GBM _gbm;
|
||||
private const double Tolerance = 1e-10;
|
||||
private const int DefaultPeriod = 14;
|
||||
|
||||
public VrTests()
|
||||
{
|
||||
_gbm = new GBM(startPrice: 100.0, mu: 0.05, sigma: 0.2, seed: 42);
|
||||
}
|
||||
|
||||
private TBarSeries GenerateBarData(int count)
|
||||
{
|
||||
_gbm.Reset(DateTime.UtcNow.Ticks);
|
||||
return _gbm.Fetch(count, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1));
|
||||
}
|
||||
|
||||
#region Constructor Tests
|
||||
|
||||
[Fact]
|
||||
public void Constructor_DefaultParameters_SetsCorrectValues()
|
||||
{
|
||||
var vr = new Vr();
|
||||
Assert.Equal(DefaultPeriod, vr.Period);
|
||||
Assert.Equal($"Vr({DefaultPeriod})", vr.Name);
|
||||
Assert.Equal(DefaultPeriod, vr.WarmupPeriod);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Constructor_CustomPeriod_SetsCorrectValues()
|
||||
{
|
||||
var vr = new Vr(period: 20);
|
||||
Assert.Equal(20, vr.Period);
|
||||
Assert.Equal("Vr(20)", vr.Name);
|
||||
Assert.Equal(20, vr.WarmupPeriod);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Constructor_ZeroPeriod_ThrowsArgumentException()
|
||||
{
|
||||
var ex = Assert.Throws<ArgumentException>(() => new Vr(period: 0));
|
||||
Assert.Equal("period", ex.ParamName);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Constructor_NegativePeriod_ThrowsArgumentException()
|
||||
{
|
||||
var ex = Assert.Throws<ArgumentException>(() => new Vr(period: -5));
|
||||
Assert.Equal("period", ex.ParamName);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Constructor_WithTBarSeriesSource_PrimesIndicator()
|
||||
{
|
||||
var bars = GenerateBarData(50);
|
||||
var vr = new Vr(bars, period: 10);
|
||||
Assert.True(vr.IsHot);
|
||||
Assert.True(double.IsFinite(vr.Last.Value));
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Basic Calculation Tests
|
||||
|
||||
[Fact]
|
||||
public void Update_SingleBar_ReturnsNonNegativeValue()
|
||||
{
|
||||
var vr = new Vr();
|
||||
var bar = new TBar(DateTime.UtcNow, 100.0, 102.0, 98.0, 101.0, 1000);
|
||||
var result = vr.Update(bar);
|
||||
Assert.True(result.Value >= 0);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Update_ConstantTR_ProducesVRNearOne()
|
||||
{
|
||||
var vr = new Vr(period: 5);
|
||||
for (int i = 0; i < 30; i++)
|
||||
{
|
||||
// Consistent range: VR should converge to 1.0
|
||||
vr.Update(new TBar(DateTime.UtcNow, 100.0, 102.0, 98.0, 101.0, 1000));
|
||||
}
|
||||
// With constant TR, VR should be near 1.0
|
||||
Assert.True(vr.Last.Value > 0.9 && vr.Last.Value < 1.1, $"Expected near 1.0, got {vr.Last.Value}");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Update_ReturnsNonNegativeValue()
|
||||
{
|
||||
var vr = new Vr();
|
||||
var bars = GenerateBarData(100);
|
||||
|
||||
for (int i = 0; i < bars.Count; i++)
|
||||
{
|
||||
var result = vr.Update(bars[i]);
|
||||
Assert.True(result.Value >= 0, $"VR should be non-negative, got {result.Value}");
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Update_HighVolatilityBar_ProducesVRAboveOne()
|
||||
{
|
||||
var vr = new Vr(period: 10);
|
||||
|
||||
// Build up ATR with normal bars
|
||||
for (int i = 0; i < 20; i++)
|
||||
{
|
||||
vr.Update(new TBar(DateTime.UtcNow, 100.0, 101.0, 99.0, 100.5, 1000));
|
||||
}
|
||||
|
||||
// High volatility bar: TR much larger than ATR
|
||||
var highVolBar = new TBar(DateTime.UtcNow, 100.0, 110.0, 90.0, 105.0, 1000);
|
||||
var result = vr.Update(highVolBar);
|
||||
|
||||
Assert.True(result.Value > 1.0, $"VR should be > 1.0 for high vol bar, got {result.Value}");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Update_LowVolatilityBar_ProducesVRBelowOne()
|
||||
{
|
||||
var vr = new Vr(period: 10);
|
||||
|
||||
// Build up ATR with normal bars
|
||||
for (int i = 0; i < 20; i++)
|
||||
{
|
||||
vr.Update(new TBar(DateTime.UtcNow, 100.0, 105.0, 95.0, 102.0, 1000));
|
||||
}
|
||||
|
||||
// Low volatility bar: TR much smaller than ATR
|
||||
var lowVolBar = new TBar(DateTime.UtcNow, 100.0, 100.5, 99.5, 100.2, 1000);
|
||||
var result = vr.Update(lowVolBar);
|
||||
|
||||
Assert.True(result.Value < 1.0, $"VR should be < 1.0 for low vol bar, got {result.Value}");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Update_GapIncludedInTR_ProducesCorrectVR()
|
||||
{
|
||||
var vr = new Vr(period: 10);
|
||||
|
||||
// Build up some history
|
||||
for (int i = 0; i < 15; i++)
|
||||
{
|
||||
vr.Update(new TBar(DateTime.UtcNow, 100.0, 101.0, 99.0, 100.0, 1000));
|
||||
}
|
||||
|
||||
// Gap up: High-PrevClose should be largest component
|
||||
var gapBar = new TBar(DateTime.UtcNow, 105.0, 106.0, 104.0, 105.5, 1000);
|
||||
var result = vr.Update(gapBar);
|
||||
|
||||
// TR = max(2, 6, 4) = 6 (High - PrevClose = 106 - 100 = 6)
|
||||
Assert.True(result.Value > 1.0, $"Gap bar should produce VR > 1.0, got {result.Value}");
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region IsHot and Warmup Tests
|
||||
|
||||
[Fact]
|
||||
public void IsHot_BeforeWarmup_ReturnsFalse()
|
||||
{
|
||||
var vr = new Vr(period: 10);
|
||||
for (int i = 0; i < 5; i++)
|
||||
{
|
||||
vr.Update(new TBar(DateTime.UtcNow, 100.0 + i, 102.0 + i, 98.0 + i, 101.0 + i, 1000));
|
||||
}
|
||||
Assert.False(vr.IsHot);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void IsHot_AfterWarmup_ReturnsTrue()
|
||||
{
|
||||
var vr = new Vr(period: 10);
|
||||
for (int i = 0; i < 15; i++)
|
||||
{
|
||||
vr.Update(new TBar(DateTime.UtcNow, 100.0 + i, 102.0 + i, 98.0 + i, 101.0 + i, 1000));
|
||||
}
|
||||
Assert.True(vr.IsHot);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void WarmupPeriod_EqualsToPeriod()
|
||||
{
|
||||
var vr = new Vr(period: 15);
|
||||
Assert.Equal(15, vr.WarmupPeriod);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Bar Correction (isNew) Tests
|
||||
|
||||
[Fact]
|
||||
public void Update_IsNewTrue_AdvancesState()
|
||||
{
|
||||
var vr = new Vr(period: 5);
|
||||
var time = DateTime.UtcNow;
|
||||
|
||||
for (int i = 0; i < 10; i++)
|
||||
{
|
||||
vr.Update(new TBar(time.AddSeconds(i), 100 + i, 102 + i, 98 + i, 101 + i, 1000), isNew: true);
|
||||
}
|
||||
|
||||
double valueBeforeNew = vr.Last.Value;
|
||||
vr.Update(new TBar(time.AddSeconds(10), 150, 155, 145, 152, 1000), isNew: true);
|
||||
|
||||
Assert.NotEqual(valueBeforeNew, vr.Last.Value);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Update_IsNewFalse_UpdatesCurrentBar()
|
||||
{
|
||||
var vr = new Vr(period: 5);
|
||||
var time = DateTime.UtcNow;
|
||||
|
||||
for (int i = 0; i < 15; i++)
|
||||
{
|
||||
vr.Update(new TBar(time.AddSeconds(i), 100 + i, 102 + i, 98 + i, 101 + i, 1000), isNew: true);
|
||||
}
|
||||
|
||||
double valueBeforeCorrection = vr.Last.Value;
|
||||
|
||||
// First correction
|
||||
vr.Update(new TBar(time.AddSeconds(15), 200, 210, 190, 205, 1000), isNew: false);
|
||||
double valueAfterCorrection1 = vr.Last.Value;
|
||||
|
||||
// Second correction to different value
|
||||
vr.Update(new TBar(time.AddSeconds(15), 50, 55, 45, 52, 1000), isNew: false);
|
||||
double valueAfterCorrection2 = vr.Last.Value;
|
||||
|
||||
Assert.NotEqual(valueBeforeCorrection, valueAfterCorrection1);
|
||||
Assert.NotEqual(valueAfterCorrection1, valueAfterCorrection2);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Update_MultipleCorrections_RestoresPreviousState()
|
||||
{
|
||||
var vr = new Vr(period: 5);
|
||||
var time = DateTime.UtcNow;
|
||||
|
||||
for (int i = 0; i < 15; i++)
|
||||
{
|
||||
vr.Update(new TBar(time.AddSeconds(i), 100 + i, 102 + i, 98 + i, 101 + i, 1000), isNew: true);
|
||||
}
|
||||
|
||||
// Add a new bar
|
||||
var newBar = new TBar(time.AddSeconds(15), 115, 117, 113, 116, 1000);
|
||||
vr.Update(newBar, isNew: true);
|
||||
double baseValue = vr.Last.Value;
|
||||
|
||||
// Multiple corrections should all restore to same base state
|
||||
vr.Update(new TBar(time.AddSeconds(15), 200, 210, 190, 205, 1000), isNew: false);
|
||||
vr.Update(newBar, isNew: false);
|
||||
double restoredValue = vr.Last.Value;
|
||||
|
||||
Assert.Equal(baseValue, restoredValue, 10);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Reset Tests
|
||||
|
||||
[Fact]
|
||||
public void Reset_ClearsAllState()
|
||||
{
|
||||
var vr = new Vr(period: 5);
|
||||
var bars = GenerateBarData(20);
|
||||
|
||||
for (int i = 0; i < bars.Count; i++)
|
||||
{
|
||||
vr.Update(bars[i]);
|
||||
}
|
||||
|
||||
Assert.True(vr.IsHot);
|
||||
|
||||
vr.Reset();
|
||||
|
||||
Assert.False(vr.IsHot);
|
||||
Assert.Equal(default, vr.Last);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Reset_AllowsReuse()
|
||||
{
|
||||
var vr = new Vr(period: 5);
|
||||
var bars = GenerateBarData(20);
|
||||
|
||||
for (int i = 0; i < bars.Count; i++)
|
||||
{
|
||||
vr.Update(bars[i]);
|
||||
}
|
||||
double firstRunValue = vr.Last.Value;
|
||||
|
||||
vr.Reset();
|
||||
|
||||
for (int i = 0; i < bars.Count; i++)
|
||||
{
|
||||
vr.Update(bars[i]);
|
||||
}
|
||||
double secondRunValue = vr.Last.Value;
|
||||
|
||||
Assert.Equal(firstRunValue, secondRunValue, 10);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region NaN and Infinity Handling Tests
|
||||
|
||||
[Fact]
|
||||
public void Update_NaNInput_UsesLastValidValue()
|
||||
{
|
||||
var vr = new Vr(period: 5);
|
||||
|
||||
for (int i = 0; i < 15; i++)
|
||||
{
|
||||
vr.Update(new TBar(DateTime.UtcNow, 100 + i, 102 + i, 98 + i, 101 + i, 1000));
|
||||
}
|
||||
|
||||
// Update with NaN
|
||||
vr.Update(new TBar(DateTime.UtcNow, double.NaN, double.NaN, double.NaN, double.NaN, 1000));
|
||||
Assert.True(double.IsFinite(vr.Last.Value));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Update_InfinityInput_UsesLastValidValue()
|
||||
{
|
||||
var vr = new Vr(period: 5);
|
||||
|
||||
for (int i = 0; i < 15; i++)
|
||||
{
|
||||
vr.Update(new TBar(DateTime.UtcNow, 100 + i, 102 + i, 98 + i, 101 + i, 1000));
|
||||
}
|
||||
|
||||
vr.Update(new TBar(DateTime.UtcNow, double.PositiveInfinity, double.PositiveInfinity, 98, 101, 1000));
|
||||
Assert.True(double.IsFinite(vr.Last.Value));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Update_MultipleNaNs_StaysFinite()
|
||||
{
|
||||
var vr = new Vr(period: 5);
|
||||
|
||||
for (int i = 0; i < 15; i++)
|
||||
{
|
||||
vr.Update(new TBar(DateTime.UtcNow, 100 + i, 102 + i, 98 + i, 101 + i, 1000));
|
||||
}
|
||||
|
||||
for (int i = 0; i < 5; i++)
|
||||
{
|
||||
vr.Update(new TBar(DateTime.UtcNow, double.NaN, double.NaN, double.NaN, double.NaN, 1000));
|
||||
}
|
||||
|
||||
Assert.True(double.IsFinite(vr.Last.Value));
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region TBarSeries and Batch Tests
|
||||
|
||||
[Fact]
|
||||
public void Update_TBarSeries_ReturnsCorrectLength()
|
||||
{
|
||||
var vr = new Vr();
|
||||
var bars = GenerateBarData(100);
|
||||
|
||||
var result = vr.Update(bars);
|
||||
Assert.Equal(bars.Count, result.Count);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Calculate_Static_ProducesValidResults()
|
||||
{
|
||||
var bars = GenerateBarData(100);
|
||||
|
||||
var result = Vr.Batch(bars, period: 10);
|
||||
|
||||
Assert.Equal(bars.Count, result.Count);
|
||||
for (int i = 0; i < result.Count; i++)
|
||||
{
|
||||
Assert.True(double.IsFinite(result.Values[i]));
|
||||
Assert.True(result.Values[i] >= 0);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Batch_ProducesConsistentResults()
|
||||
{
|
||||
var bars = GenerateBarData(100);
|
||||
|
||||
double[] output = new double[100];
|
||||
Vr.Batch(bars, output, period: 10);
|
||||
|
||||
for (int i = 0; i < output.Length; i++)
|
||||
{
|
||||
Assert.True(double.IsFinite(output[i]));
|
||||
Assert.True(output[i] >= 0);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Batch_ZeroPeriod_ThrowsArgumentException()
|
||||
{
|
||||
var bars = GenerateBarData(10);
|
||||
double[] output = new double[10];
|
||||
var ex = Assert.Throws<ArgumentException>(() => Vr.Batch(bars, output, period: 0));
|
||||
Assert.Equal("period", ex.ParamName);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Batch_OutputTooSmall_ThrowsArgumentException()
|
||||
{
|
||||
var bars = GenerateBarData(10);
|
||||
double[] output = new double[5];
|
||||
var ex = Assert.Throws<ArgumentException>(() => Vr.Batch(bars, output));
|
||||
Assert.Equal("output", ex.ParamName);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Batch_EmptySource_DoesNotThrow()
|
||||
{
|
||||
var bars = new TBarSeries();
|
||||
double[] output = [];
|
||||
Vr.Batch(bars, output);
|
||||
Assert.Empty(output);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Batch_HlcArrays_ProducesValidResults()
|
||||
{
|
||||
int len = 50;
|
||||
double[] high = new double[len];
|
||||
double[] low = new double[len];
|
||||
double[] close = new double[len];
|
||||
double[] output = new double[len];
|
||||
|
||||
for (int i = 0; i < len; i++)
|
||||
{
|
||||
high[i] = 102 + i;
|
||||
low[i] = 98 + i;
|
||||
close[i] = 101 + i;
|
||||
}
|
||||
|
||||
Vr.Batch(high, low, close, output, period: 10);
|
||||
|
||||
for (int i = 0; i < output.Length; i++)
|
||||
{
|
||||
Assert.True(double.IsFinite(output[i]));
|
||||
Assert.True(output[i] >= 0);
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Mode Consistency Tests
|
||||
|
||||
[Fact]
|
||||
public void AllModes_ProduceSameResults()
|
||||
{
|
||||
var bars = GenerateBarData(100);
|
||||
int period = 10;
|
||||
|
||||
// Mode 1: Streaming
|
||||
var streamingVr = new Vr(period);
|
||||
for (int i = 0; i < bars.Count; i++)
|
||||
{
|
||||
streamingVr.Update(bars[i], isNew: true);
|
||||
}
|
||||
|
||||
// Mode 2: TBarSeries batch
|
||||
var batchResult = Vr.Batch(bars, period);
|
||||
|
||||
// Mode 3: Span batch
|
||||
double[] spanOutput = new double[bars.Count];
|
||||
Vr.Batch(bars, spanOutput, period);
|
||||
|
||||
// Compare last 50 values (after warmup)
|
||||
int compareStart = bars.Count - 50;
|
||||
for (int i = compareStart; i < bars.Count; i++)
|
||||
{
|
||||
double batch = batchResult[i].Value;
|
||||
double span = spanOutput[i];
|
||||
|
||||
Assert.Equal(batch, span, Tolerance);
|
||||
}
|
||||
|
||||
// Final values should match
|
||||
Assert.Equal(streamingVr.Last.Value, batchResult[bars.Count - 1].Value, 1e-8);
|
||||
Assert.Equal(streamingVr.Last.Value, spanOutput[bars.Count - 1], 1e-8);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Event Tests
|
||||
|
||||
[Fact]
|
||||
public void Pub_FiresOnUpdate()
|
||||
{
|
||||
var vr = new Vr(period: 5);
|
||||
int eventCount = 0;
|
||||
|
||||
vr.Pub += (object? sender, in TValueEventArgs args) => eventCount++;
|
||||
|
||||
var time = DateTime.UtcNow;
|
||||
for (int i = 0; i < 5; i++)
|
||||
{
|
||||
vr.Update(new TBar(time.AddSeconds(i), 100 + i, 102 + i, 98 + i, 101 + i, 1000));
|
||||
}
|
||||
|
||||
Assert.Equal(5, eventCount);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region TValue Input Tests
|
||||
|
||||
[Fact]
|
||||
public void Update_TValue_CreatesSyntheticBar()
|
||||
{
|
||||
var vr1 = new Vr(period: 5);
|
||||
var vr2 = new Vr(period: 5);
|
||||
var time = DateTime.UtcNow;
|
||||
|
||||
for (int i = 0; i < 15; i++)
|
||||
{
|
||||
// TValue input creates bar with O=H=L=C
|
||||
vr1.Update(new TValue(time.AddSeconds(i), 100.0 + i));
|
||||
vr2.Update(new TBar(time.AddSeconds(i), 100.0 + i, 100.0 + i, 100.0 + i, 100.0 + i, 0));
|
||||
}
|
||||
|
||||
Assert.Equal(vr1.Last.Value, vr2.Last.Value, Tolerance);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Large Period Tests
|
||||
|
||||
[Fact]
|
||||
public void LargeDataset_NoStackOverflow()
|
||||
{
|
||||
var bars = GenerateBarData(10000);
|
||||
|
||||
double[] output = new double[10000];
|
||||
Vr.Batch(bars, output, period: 14);
|
||||
|
||||
for (int i = 0; i < output.Length; i++)
|
||||
{
|
||||
Assert.True(double.IsFinite(output[i]));
|
||||
Assert.True(output[i] >= 0);
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Prime Tests
|
||||
|
||||
[Fact]
|
||||
public void Prime_SetsInitialState()
|
||||
{
|
||||
var vr = new Vr(period: 5);
|
||||
double[] warmupData = [100, 101, 102, 103, 104, 105, 106, 107, 108, 109];
|
||||
|
||||
vr.Prime(warmupData);
|
||||
|
||||
Assert.True(vr.IsHot);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region VR Specific Tests
|
||||
|
||||
[Fact]
|
||||
public void Update_TrueRangeCalculation_IncludesGaps()
|
||||
{
|
||||
var vr = new Vr(period: 5);
|
||||
|
||||
// First bar establishes previous close
|
||||
vr.Update(new TBar(DateTime.UtcNow, 100, 101, 99, 100, 1000));
|
||||
|
||||
// Gap up bar: High-PrevClose > H-L
|
||||
// PrevClose = 100, Current bar: O=105, H=107, L=104, C=106
|
||||
// TR = max(3, 7, 4) = 7 (High - PrevClose)
|
||||
var gapUpBar = new TBar(DateTime.UtcNow, 105, 107, 104, 106, 1000);
|
||||
vr.Update(gapUpBar);
|
||||
|
||||
// The TR should incorporate the gap
|
||||
Assert.True(vr.Last.Value > 0, "VR should be positive with gap");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Update_BiasCorrection_WorksDuringWarmup()
|
||||
{
|
||||
var vr = new Vr(period: 20);
|
||||
var bars = GenerateBarData(5);
|
||||
|
||||
// During warmup, bias correction should prevent extreme values
|
||||
for (int i = 0; i < bars.Count; i++)
|
||||
{
|
||||
var result = vr.Update(bars[i]);
|
||||
Assert.True(double.IsFinite(result.Value), $"Value at index {i} should be finite");
|
||||
Assert.True(result.Value >= 0, $"Value at index {i} should be non-negative");
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Update_VRMeanReverts_TowardsOne()
|
||||
{
|
||||
var vr = new Vr(period: 10);
|
||||
|
||||
// Build up history with varying volatility
|
||||
for (int i = 0; i < 50; i++)
|
||||
{
|
||||
double range = 2.0 + (i % 5) * 0.5; // Varying range
|
||||
vr.Update(new TBar(DateTime.UtcNow, 100, 100 + range, 100 - range, 100 + range / 2, 1000));
|
||||
}
|
||||
|
||||
// VR should oscillate around 1.0 over time
|
||||
// After many bars, the average should be close to 1.0
|
||||
Assert.True(vr.Last.Value > 0, "VR should be positive");
|
||||
Assert.True(double.IsFinite(vr.Last.Value), "VR should be finite");
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
@@ -0,0 +1,424 @@
|
||||
// Volatility Ratio (VR) Validation Tests
|
||||
// Validates against the PineScript reference implementation
|
||||
|
||||
using Xunit;
|
||||
|
||||
namespace QuanTAlib.Tests;
|
||||
|
||||
public class VrValidationTests
|
||||
{
|
||||
private readonly GBM _gbm;
|
||||
private const double PineScriptTolerance = 1e-6;
|
||||
|
||||
public VrValidationTests()
|
||||
{
|
||||
_gbm = new GBM(startPrice: 100.0, mu: 0.05, sigma: 0.2, seed: 42);
|
||||
}
|
||||
|
||||
private TBarSeries GenerateBarData(int count)
|
||||
{
|
||||
_gbm.Reset(DateTime.UtcNow.Ticks);
|
||||
return _gbm.Fetch(count, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1));
|
||||
}
|
||||
|
||||
#region PineScript Algorithm Validation
|
||||
|
||||
[Fact]
|
||||
public void Vr_TrueRangeCalculation_MatchesPineScript()
|
||||
{
|
||||
// TR = max(high - low, abs(high - prevClose), abs(low - prevClose))
|
||||
double prevClose = 100.0;
|
||||
double high = 105.0;
|
||||
double low = 98.0;
|
||||
|
||||
double hl = high - low; // 7
|
||||
double hPc = Math.Abs(high - prevClose); // 5
|
||||
double lPc = Math.Abs(low - prevClose); // 2
|
||||
|
||||
double expectedTR = Math.Max(hl, Math.Max(hPc, lPc)); // 7
|
||||
|
||||
Assert.Equal(7.0, expectedTR);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Vr_TrueRangeWithGapUp_MatchesPineScript()
|
||||
{
|
||||
// Gap up scenario: High-PrevClose is largest
|
||||
double prevClose = 100.0;
|
||||
double high = 110.0;
|
||||
double low = 108.0;
|
||||
|
||||
double hl = high - low; // 2
|
||||
double hPc = Math.Abs(high - prevClose); // 10
|
||||
double lPc = Math.Abs(low - prevClose); // 8
|
||||
|
||||
double expectedTR = Math.Max(hl, Math.Max(hPc, lPc)); // 10
|
||||
|
||||
Assert.Equal(10.0, expectedTR);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Vr_TrueRangeWithGapDown_MatchesPineScript()
|
||||
{
|
||||
// Gap down scenario: Low-PrevClose (abs) is largest
|
||||
double prevClose = 100.0;
|
||||
double high = 92.0;
|
||||
double low = 90.0;
|
||||
|
||||
double hl = high - low; // 2
|
||||
double hPc = Math.Abs(high - prevClose); // 8
|
||||
double lPc = Math.Abs(low - prevClose); // 10
|
||||
|
||||
double expectedTR = Math.Max(hl, Math.Max(hPc, lPc)); // 10
|
||||
|
||||
Assert.Equal(10.0, expectedTR);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Vr_BiasCorrection_MatchesPineScript()
|
||||
{
|
||||
// Verify bias correction formula: atr = rawAtr / (1 - eComp)
|
||||
// where eComp = (1 - alpha)^n for n bars
|
||||
|
||||
int period = 10;
|
||||
double alpha = 1.0 / period;
|
||||
|
||||
// After 1 bar: eComp = 0.9
|
||||
double eComp1 = 1.0 - alpha;
|
||||
Assert.Equal(0.9, eComp1, 10);
|
||||
|
||||
// After 2 bars: eComp = 0.81
|
||||
double eComp2 = (1.0 - alpha) * eComp1;
|
||||
Assert.Equal(0.81, eComp2, 10);
|
||||
|
||||
// After 3 bars: eComp = 0.729
|
||||
double eComp3 = (1.0 - alpha) * eComp2;
|
||||
Assert.Equal(0.729, eComp3, 10);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Vr_ConstantTR_ConvergesToOne()
|
||||
{
|
||||
// When TR is constant, VR = TR / ATR should approach 1.0
|
||||
// because ATR converges to TR
|
||||
|
||||
var vr = new Vr(period: 10);
|
||||
|
||||
// Feed bars with constant TR (H-L = 4)
|
||||
for (int i = 0; i < 100; i++)
|
||||
{
|
||||
vr.Update(new TBar(DateTime.UtcNow, 100, 102, 98, 100, 1000));
|
||||
}
|
||||
|
||||
// VR should be very close to 1.0
|
||||
Assert.True(Math.Abs(vr.Last.Value - 1.0) < 0.01,
|
||||
$"Constant TR should yield VR near 1.0, got {vr.Last.Value}");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Vr_Formula_MatchesPineScript()
|
||||
{
|
||||
// VR = TR / ATR
|
||||
// With bias-corrected ATR (period = 14 in typical usage)
|
||||
|
||||
double tr = 5.0;
|
||||
double rawAtr = 4.0;
|
||||
double eComp = 0.5; // Example compensator
|
||||
|
||||
double atr = rawAtr / (1.0 - eComp); // = 4.0 / 0.5 = 8.0
|
||||
double expectedVr = tr / atr; // = 5.0 / 8.0 = 0.625
|
||||
|
||||
Assert.Equal(0.625, expectedVr, 10);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Streaming vs Batch Consistency
|
||||
|
||||
[Fact]
|
||||
public void Vr_StreamingMatchesBatch_AllPeriods()
|
||||
{
|
||||
int[] periods = [5, 10, 14, 20, 50];
|
||||
|
||||
foreach (int period in periods)
|
||||
{
|
||||
var bars = GenerateBarData(100);
|
||||
|
||||
// Streaming
|
||||
var streamingVr = new Vr(period);
|
||||
for (int i = 0; i < bars.Count; i++)
|
||||
{
|
||||
streamingVr.Update(bars[i], isNew: true);
|
||||
}
|
||||
|
||||
// Batch
|
||||
double[] batchOutput = new double[bars.Count];
|
||||
Vr.Batch(bars, batchOutput, period);
|
||||
|
||||
// Compare final value
|
||||
Assert.Equal(streamingVr.Last.Value, batchOutput[bars.Count - 1], PineScriptTolerance);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Vr_BatchMatchesCalculate_AllValues()
|
||||
{
|
||||
var bars = GenerateBarData(100);
|
||||
int period = 14;
|
||||
|
||||
// Using static Calculate
|
||||
var calculateResult = Vr.Batch(bars, period);
|
||||
|
||||
// Using Batch
|
||||
double[] batchOutput = new double[bars.Count];
|
||||
Vr.Batch(bars, batchOutput, period);
|
||||
|
||||
for (int i = 0; i < bars.Count; i++)
|
||||
{
|
||||
Assert.Equal(calculateResult[i].Value, batchOutput[i], PineScriptTolerance);
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Mathematical Properties
|
||||
|
||||
[Fact]
|
||||
public void Vr_AlwaysNonNegative()
|
||||
{
|
||||
var bars = GenerateBarData(500);
|
||||
var vr = new Vr(14);
|
||||
|
||||
for (int i = 0; i < bars.Count; i++)
|
||||
{
|
||||
var result = vr.Update(bars[i]);
|
||||
Assert.True(result.Value >= 0, $"VR at index {i} should be non-negative: {result.Value}");
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Vr_FirstBar_HasValidValue()
|
||||
{
|
||||
var vr = new Vr(14);
|
||||
var bar = new TBar(DateTime.UtcNow, 100, 105, 95, 102, 1000);
|
||||
|
||||
var result = vr.Update(bar);
|
||||
|
||||
// First bar: TR = H-L = 10, ATR = TR = 10, VR = 1.0
|
||||
Assert.True(double.IsFinite(result.Value));
|
||||
Assert.True(result.Value >= 0);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Vr_HighVolatilityBar_ExceedsOne()
|
||||
{
|
||||
var vr = new Vr(period: 10);
|
||||
|
||||
// Build up ATR with low volatility
|
||||
for (int i = 0; i < 30; i++)
|
||||
{
|
||||
vr.Update(new TBar(DateTime.UtcNow, 100, 101, 99, 100, 1000));
|
||||
}
|
||||
|
||||
// Now add a high volatility bar
|
||||
var highVolBar = new TBar(DateTime.UtcNow, 100, 110, 90, 100, 1000);
|
||||
var result = vr.Update(highVolBar);
|
||||
|
||||
Assert.True(result.Value > 1.0,
|
||||
$"High volatility bar should produce VR > 1.0, got {result.Value}");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Vr_LowVolatilityBar_BelowOne()
|
||||
{
|
||||
var vr = new Vr(period: 10);
|
||||
|
||||
// Build up ATR with moderate volatility
|
||||
for (int i = 0; i < 30; i++)
|
||||
{
|
||||
vr.Update(new TBar(DateTime.UtcNow, 100, 105, 95, 100, 1000));
|
||||
}
|
||||
|
||||
// Now add a low volatility bar
|
||||
var lowVolBar = new TBar(DateTime.UtcNow, 100, 100.5, 99.5, 100, 1000);
|
||||
var result = vr.Update(lowVolBar);
|
||||
|
||||
Assert.True(result.Value < 1.0,
|
||||
$"Low volatility bar should produce VR < 1.0, got {result.Value}");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Vr_MeanRevertsToOne()
|
||||
{
|
||||
var vr = new Vr(period: 10);
|
||||
double sumVr = 0;
|
||||
int count = 0;
|
||||
|
||||
// Generate many bars
|
||||
var bars = GenerateBarData(500);
|
||||
for (int i = 0; i < bars.Count; i++)
|
||||
{
|
||||
var result = vr.Update(bars[i]);
|
||||
if (vr.IsHot)
|
||||
{
|
||||
sumVr += result.Value;
|
||||
count++;
|
||||
}
|
||||
}
|
||||
|
||||
double avgVr = sumVr / count;
|
||||
|
||||
// Average VR should be near 1.0 over time
|
||||
Assert.True(avgVr > 0.5 && avgVr < 2.0,
|
||||
$"Average VR should be near 1.0, got {avgVr}");
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Edge Cases
|
||||
|
||||
[Fact]
|
||||
public void Vr_Period1_HandlesCorrectly()
|
||||
{
|
||||
var vr = new Vr(1);
|
||||
var bar = new TBar(DateTime.UtcNow, 100, 105, 95, 102, 1000);
|
||||
|
||||
var result = vr.Update(bar);
|
||||
Assert.True(double.IsFinite(result.Value));
|
||||
Assert.True(result.Value >= 0);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Vr_LargePeriod_HandlesCorrectly()
|
||||
{
|
||||
var vr = new Vr(200);
|
||||
var bars = GenerateBarData(300);
|
||||
|
||||
for (int i = 0; i < bars.Count; i++)
|
||||
{
|
||||
var result = vr.Update(bars[i]);
|
||||
Assert.True(double.IsFinite(result.Value));
|
||||
Assert.True(result.Value >= 0);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Vr_ZeroRange_HandlesCorrectly()
|
||||
{
|
||||
var vr = new Vr(10);
|
||||
|
||||
// Build up some ATR
|
||||
for (int i = 0; i < 20; i++)
|
||||
{
|
||||
vr.Update(new TBar(DateTime.UtcNow, 100, 105, 95, 100, 1000));
|
||||
}
|
||||
|
||||
// Zero range bar
|
||||
var zeroRangeBar = new TBar(DateTime.UtcNow, 100, 100, 100, 100, 1000);
|
||||
var result = vr.Update(zeroRangeBar);
|
||||
|
||||
// VR should be 0 when TR is 0
|
||||
Assert.True(double.IsFinite(result.Value));
|
||||
Assert.True(result.Value < 0.01, $"Zero TR should produce VR near 0, got {result.Value}");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Vr_GapUp_IncorporatedInTR()
|
||||
{
|
||||
var vr = new Vr(period: 10);
|
||||
|
||||
// Establish baseline
|
||||
for (int i = 0; i < 15; i++)
|
||||
{
|
||||
vr.Update(new TBar(DateTime.UtcNow, 100, 101, 99, 100, 1000));
|
||||
}
|
||||
|
||||
// Gap up bar: previous close = 100, open = 110
|
||||
var gapBar = new TBar(DateTime.UtcNow, 110, 112, 109, 111, 1000);
|
||||
var result = vr.Update(gapBar);
|
||||
|
||||
// TR should include gap (High - PrevClose = 12)
|
||||
Assert.True(result.Value > 1.0,
|
||||
$"Gap up should produce VR > 1.0, got {result.Value}");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Vr_GapDown_IncorporatedInTR()
|
||||
{
|
||||
var vr = new Vr(period: 10);
|
||||
|
||||
// Establish baseline
|
||||
for (int i = 0; i < 15; i++)
|
||||
{
|
||||
vr.Update(new TBar(DateTime.UtcNow, 100, 101, 99, 100, 1000));
|
||||
}
|
||||
|
||||
// Gap down bar: previous close = 100, open = 90
|
||||
var gapBar = new TBar(DateTime.UtcNow, 90, 91, 88, 89, 1000);
|
||||
var result = vr.Update(gapBar);
|
||||
|
||||
// TR should include gap (abs(Low - PrevClose) = 12)
|
||||
Assert.True(result.Value > 1.0,
|
||||
$"Gap down should produce VR > 1.0, got {result.Value}");
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Breakout Detection Tests
|
||||
|
||||
[Fact]
|
||||
public void Vr_BreakoutDetection_HighVRIndicatesBreakout()
|
||||
{
|
||||
var vr = new Vr(period: 14);
|
||||
|
||||
// Low volatility consolidation
|
||||
for (int i = 0; i < 50; i++)
|
||||
{
|
||||
vr.Update(new TBar(DateTime.UtcNow, 100, 101, 99, 100 + (i % 2) * 0.5, 1000));
|
||||
}
|
||||
|
||||
double consolidationVr = vr.Last.Value;
|
||||
|
||||
// Breakout bar
|
||||
var breakoutBar = new TBar(DateTime.UtcNow, 100, 115, 100, 114, 1000);
|
||||
var breakoutResult = vr.Update(breakoutBar);
|
||||
|
||||
Assert.True(breakoutResult.Value > 2.0,
|
||||
$"Breakout bar should produce VR > 2.0, got {breakoutResult.Value}");
|
||||
Assert.True(breakoutResult.Value > consolidationVr * 2,
|
||||
$"Breakout VR ({breakoutResult.Value}) should be much higher than consolidation VR ({consolidationVr})");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Vr_VolatilityExpansion_Detected()
|
||||
{
|
||||
var vr = new Vr(period: 14);
|
||||
|
||||
// Track VR during expansion
|
||||
var vrValues = new List<double>();
|
||||
|
||||
// Start with low volatility
|
||||
for (int i = 0; i < 20; i++)
|
||||
{
|
||||
var result = vr.Update(new TBar(DateTime.UtcNow, 100, 101, 99, 100, 1000));
|
||||
vrValues.Add(result.Value);
|
||||
}
|
||||
|
||||
// Gradually increase volatility
|
||||
for (int i = 0; i < 20; i++)
|
||||
{
|
||||
double range = 1 + i * 0.5;
|
||||
var result = vr.Update(new TBar(DateTime.UtcNow, 100, 100 + range, 100 - range, 100, 1000));
|
||||
vrValues.Add(result.Value);
|
||||
}
|
||||
|
||||
// Later VR values should be higher during expansion
|
||||
double earlyAvg = vrValues.Skip(15).Take(5).Average();
|
||||
double lateAvg = vrValues.Skip(35).Take(5).Average();
|
||||
|
||||
Assert.True(lateAvg > earlyAvg,
|
||||
$"Expanding volatility should show increasing VR: early={earlyAvg}, late={lateAvg}");
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
Reference in New Issue
Block a user