mirror of
https://github.com/mihakralj/QuanTAlib.git
synced 2026-07-31 10:57:43 +00:00
060649192f
- 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
425 lines
12 KiB
C#
425 lines
12 KiB
C#
// 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
|
|
}
|