volatility indicators

This commit is contained in:
Miha Kralj
2026-02-01 17:48:16 -08:00
parent bcb52ef5ec
commit dde19f2226
40 changed files with 13350 additions and 62 deletions
+304
View File
@@ -0,0 +1,304 @@
using TradingPlatform.BusinessLayer;
using QuanTAlib;
namespace QuanTAlib.Tests;
public class HlvIndicatorTests
{
[Fact]
public void HlvIndicator_Constructor_SetsDefaults()
{
var indicator = new HlvIndicator();
Assert.Equal(20, indicator.Period);
Assert.True(indicator.Annualize);
Assert.Equal(252, indicator.AnnualPeriods);
Assert.True(indicator.ShowColdValues);
Assert.Equal("HLV - High-Low Volatility (Parkinson)", indicator.Name);
Assert.True(indicator.SeparateWindow);
Assert.True(indicator.OnBackGround);
}
[Fact]
public void HlvIndicator_ShortName_IncludesParameters()
{
var indicator = new HlvIndicator { Period = 14 };
Assert.Contains("HLV", indicator.ShortName, StringComparison.Ordinal);
Assert.Contains("14", indicator.ShortName, StringComparison.Ordinal);
}
[Fact]
public void HlvIndicator_MinHistoryDepths_EqualsZero()
{
var indicator = new HlvIndicator();
Assert.Equal(0, HlvIndicator.MinHistoryDepths);
Assert.Equal(0, ((IWatchlistIndicator)indicator).MinHistoryDepths);
}
[Fact]
public void HlvIndicator_Initialize_CreatesInternalHlv()
{
var indicator = new HlvIndicator();
// Initialize should not throw
indicator.Initialize();
// After init, line series should exist
Assert.Single(indicator.LinesSeries);
}
[Fact]
public void HlvIndicator_ProcessUpdate_HistoricalBar_ComputesValue()
{
var indicator = new HlvIndicator { Period = 10 };
indicator.Initialize();
// Add historical data with varying volatility
var now = DateTime.UtcNow;
for (int i = 0; i < 30; i++)
{
double basePrice = 100 + i;
double range = 2 + (i % 5); // Varying ranges
indicator.HistoricalData.AddBar(now.AddMinutes(i), basePrice, basePrice + range, basePrice - range, basePrice + 1, 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, "Volatility should be non-negative");
}
[Fact]
public void HlvIndicator_ProcessUpdate_NewBar_ComputesValue()
{
var indicator = new HlvIndicator { 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 + 5, basePrice - 5, basePrice + 2, 1000);
}
indicator.ProcessUpdate(new UpdateArgs(UpdateReason.HistoricalBar));
// Add new bar with larger range
indicator.HistoricalData.AddBar(now.AddMinutes(30), 120, 135, 105, 125, 1500);
indicator.ProcessUpdate(new UpdateArgs(UpdateReason.NewBar));
Assert.Equal(2, indicator.LinesSeries[0].Count);
}
[Fact]
public void HlvIndicator_DifferentPeriods_Work()
{
int[] periods = { 5, 10, 14, 20 };
foreach (var period in periods)
{
var indicator = new HlvIndicator { Period = period };
indicator.Initialize();
var now = DateTime.UtcNow;
for (int i = 0; i < 50; i++)
{
double basePrice = 100 + i;
double range = 3 + (i % 4);
indicator.HistoricalData.AddBar(now.AddMinutes(i), basePrice, basePrice + range, basePrice - range, basePrice + 1, 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 HlvIndicator_Period_CanBeChanged()
{
var indicator = new HlvIndicator();
Assert.Equal(20, indicator.Period);
indicator.Period = 14;
Assert.Equal(14, indicator.Period);
indicator.Period = 10;
Assert.Equal(10, indicator.Period);
}
[Fact]
public void HlvIndicator_Annualize_CanBeToggled()
{
var indicator = new HlvIndicator();
Assert.True(indicator.Annualize);
indicator.Annualize = false;
Assert.False(indicator.Annualize);
indicator.Annualize = true;
Assert.True(indicator.Annualize);
}
[Fact]
public void HlvIndicator_AnnualPeriods_CanBeChanged()
{
var indicator = new HlvIndicator();
Assert.Equal(252, indicator.AnnualPeriods);
indicator.AnnualPeriods = 365;
Assert.Equal(365, indicator.AnnualPeriods);
indicator.AnnualPeriods = 52;
Assert.Equal(52, indicator.AnnualPeriods);
}
[Fact]
public void HlvIndicator_ShowColdValues_CanBeToggled()
{
var indicator = new HlvIndicator();
Assert.True(indicator.ShowColdValues);
indicator.ShowColdValues = false;
Assert.False(indicator.ShowColdValues);
indicator.ShowColdValues = true;
Assert.True(indicator.ShowColdValues);
}
[Fact]
public void HlvIndicator_SourceCodeLink_IsValid()
{
var indicator = new HlvIndicator();
Assert.Contains("github.com", indicator.SourceCodeLink, StringComparison.Ordinal);
Assert.Contains("Hlv.Quantower.cs", indicator.SourceCodeLink, StringComparison.Ordinal);
}
[Fact]
public void HlvIndicator_HighVolatility_ProducesHigherValue()
{
var indicator1 = new HlvIndicator { Period = 10, Annualize = false };
var indicator2 = new HlvIndicator { Period = 10, Annualize = false };
indicator1.Initialize();
indicator2.Initialize();
var now = DateTime.UtcNow;
// Indicator 1: low volatility (narrow range)
for (int i = 0; i < 30; i++)
{
double basePrice = 100;
indicator1.HistoricalData.AddBar(now.AddMinutes(i), basePrice, basePrice + 1, basePrice - 1, basePrice + 0.5, 1000);
indicator1.ProcessUpdate(new UpdateArgs(UpdateReason.HistoricalBar));
}
// Indicator 2: high volatility (wide range)
for (int i = 0; i < 30; i++)
{
double basePrice = 100;
indicator2.HistoricalData.AddBar(now.AddMinutes(i), basePrice, basePrice + 10, basePrice - 10, basePrice + 2, 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 bars should produce higher HLV value");
}
[Fact]
public void HlvIndicator_AnnualizedValue_IsScaled()
{
var indicatorRaw = new HlvIndicator { Period = 10, Annualize = false };
var indicatorAnn = new HlvIndicator { Period = 10, Annualize = true, AnnualPeriods = 252 };
indicatorRaw.Initialize();
indicatorAnn.Initialize();
var now = DateTime.UtcNow;
// Same data for both
for (int i = 0; i < 30; i++)
{
double basePrice = 100 + i * 0.5;
indicatorRaw.HistoricalData.AddBar(now.AddMinutes(i), basePrice, basePrice + 3, basePrice - 3, basePrice + 1, 1000);
indicatorRaw.ProcessUpdate(new UpdateArgs(UpdateReason.HistoricalBar));
indicatorAnn.HistoricalData.AddBar(now.AddMinutes(i), basePrice, basePrice + 3, basePrice - 3, basePrice + 1, 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));
// Annualized should be approximately sqrt(252) times larger
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 HlvIndicator_OnlyUsesHighLow_IgnoresOpenClose()
{
// Test that HLV only uses High-Low (not Open-Close)
var indicator1 = new HlvIndicator { Period = 10, Annualize = false };
var indicator2 = new HlvIndicator { Period = 10, Annualize = false };
indicator1.Initialize();
indicator2.Initialize();
var now = DateTime.UtcNow;
// Same high/low range but different open/close
for (int i = 0; i < 30; i++)
{
// Indicator 1: open = close (doji pattern)
indicator1.HistoricalData.AddBar(now.AddMinutes(i), 100, 105, 95, 100, 1000);
indicator1.ProcessUpdate(new UpdateArgs(UpdateReason.HistoricalBar));
// Indicator 2: open != close (directional move)
indicator2.HistoricalData.AddBar(now.AddMinutes(i), 98, 105, 95, 104, 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));
// HLV should be identical since H-L range is the same
Assert.Equal(val1, val2, 10);
}
[Fact]
public void HlvIndicator_ConstantPrice_ProducesZeroVolatility()
{
var indicator = new HlvIndicator { Period = 10, Annualize = false };
indicator.Initialize();
var now = DateTime.UtcNow;
// Constant price (no volatility)
for (int i = 0; i < 30; i++)
{
indicator.HistoricalData.AddBar(now.AddMinutes(i), 100, 100, 100, 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 price should produce near-zero volatility");
}
}
+55
View File
@@ -0,0 +1,55 @@
using System.Drawing;
using System.Runtime.CompilerServices;
using TradingPlatform.BusinessLayer;
namespace QuanTAlib;
[SkipLocalsInit]
public sealed class HlvIndicator : Indicator, IWatchlistIndicator
{
[InputParameter("Period", sortIndex: 1, 1, 1000, 1, 0)]
public int Period { get; set; } = 20;
[InputParameter("Annualize", sortIndex: 2)]
public bool Annualize { get; set; } = true;
[InputParameter("Annual Periods", sortIndex: 3, 1, 365, 1, 0)]
public int AnnualPeriods { get; set; } = 252;
[InputParameter("Show cold values", sortIndex: 21)]
public bool ShowColdValues { get; set; } = true;
private Hlv _hlv = null!;
private readonly LineSeries _series;
public static int MinHistoryDepths => 0;
int IWatchlistIndicator.MinHistoryDepths => MinHistoryDepths;
public override string ShortName => $"HLV {Period}";
public override string SourceCodeLink => "https://github.com/mihakralj/QuanTAlib/blob/main/lib/volatility/hlv/Hlv.Quantower.cs";
public HlvIndicator()
{
OnBackGround = true;
SeparateWindow = true;
Name = "HLV - High-Low Volatility (Parkinson)";
Description = "High-Low Volatility is a range-based volatility estimator using only High-Low prices (Parkinson method), providing efficient estimates without requiring Open-Close data";
_series = new LineSeries(name: "HLV", color: IndicatorExtensions.Volatility, width: 2, style: LineStyle.Solid);
AddLineSeries(_series);
}
protected override void OnInit()
{
_hlv = new Hlv(Period, Annualize, AnnualPeriods);
base.OnInit();
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
protected override void OnUpdate(UpdateArgs args)
{
TBar bar = this.GetInputBar(args);
TValue result = _hlv.Update(bar, isNew: args.IsNewBar());
_series.SetValue(result.Value, _hlv.IsHot, ShowColdValues);
}
}
+649
View File
@@ -0,0 +1,649 @@
namespace QuanTAlib.Tests;
using Xunit;
public class HlvTests
{
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));
}
#region Constructor Tests
[Fact]
public void Constructor_DefaultParameters_SetsCorrectValues()
{
var hlv = new Hlv();
Assert.Equal(20, hlv.Period);
Assert.True(hlv.Annualize);
Assert.Equal(252, hlv.AnnualPeriods);
Assert.Equal("Hlv(20)", hlv.Name);
Assert.Equal(20, hlv.WarmupPeriod);
}
[Fact]
public void Constructor_CustomParameters_SetsCorrectValues()
{
var hlv = new Hlv(period: 10, annualize: false, annualPeriods: 365);
Assert.Equal(10, hlv.Period);
Assert.False(hlv.Annualize);
Assert.Equal(365, hlv.AnnualPeriods);
Assert.Equal("Hlv(10)", hlv.Name);
}
[Fact]
public void Constructor_ZeroPeriod_ThrowsArgumentException()
{
var ex = Assert.Throws<ArgumentException>(() => new Hlv(period: 0));
Assert.Equal("period", ex.ParamName);
}
[Fact]
public void Constructor_NegativePeriod_ThrowsArgumentException()
{
var ex = Assert.Throws<ArgumentException>(() => new Hlv(period: -1));
Assert.Equal("period", ex.ParamName);
}
[Fact]
public void Constructor_ZeroAnnualPeriodsWhenAnnualizing_ThrowsArgumentException()
{
var ex = Assert.Throws<ArgumentException>(() => new Hlv(period: 10, annualize: true, annualPeriods: 0));
Assert.Equal("annualPeriods", ex.ParamName);
}
[Fact]
public void Constructor_ZeroAnnualPeriodsWhenNotAnnualizing_DoesNotThrow()
{
var hlv = new Hlv(period: 10, annualize: false, annualPeriods: 0);
Assert.Equal(0, hlv.AnnualPeriods);
}
#endregion
#region Basic Calculation Tests
[Fact]
public void Update_SingleBar_ReturnsNonNegativeValue()
{
var hlv = new Hlv(period: 5);
var bar = new TBar(DateTime.UtcNow, 100.0, 105.0, 98.0, 102.0, 1000);
var result = hlv.Update(bar);
Assert.True(result.Value >= 0, "HLV should return non-negative values");
}
[Fact]
public void Update_MultipleBars_ReturnsCorrectCount()
{
var hlv = new Hlv(period: 5);
var bars = GenerateTestData(10);
for (int i = 0; i < bars.Count; i++)
{
hlv.Update(bars[i]);
}
Assert.True(hlv.IsHot, "Indicator should be hot after warmup period");
}
[Fact]
public void Update_ReturnsLastValue()
{
var hlv = new Hlv(period: 5);
var bar = new TBar(DateTime.UtcNow, 100.0, 105.0, 98.0, 102.0, 1000);
var result = hlv.Update(bar);
Assert.Equal(result.Value, hlv.Last.Value, Tolerance);
}
[Fact]
public void Update_WithoutAnnualization_ReturnsSmallerValues()
{
var hlvAnnual = new Hlv(period: 10, annualize: true, annualPeriods: 252);
var hlvNoAnnual = new Hlv(period: 10, annualize: false);
var bars = GenerateTestData(20);
double lastAnnual = 0;
double lastNoAnnual = 0;
for (int i = 0; i < bars.Count; i++)
{
lastAnnual = hlvAnnual.Update(bars[i]).Value;
lastNoAnnual = hlvNoAnnual.Update(bars[i]).Value;
}
// Annualized values should be larger by factor of sqrt(252)
Assert.True(lastAnnual > lastNoAnnual, "Annualized values should be larger");
}
#endregion
#region State Management Tests
[Fact]
public void Update_IsNewTrue_AdvancesState()
{
var hlv = new Hlv(period: 5);
var bar1 = new TBar(DateTime.UtcNow, 100.0, 105.0, 98.0, 102.0, 1000);
var bar2 = new TBar(DateTime.UtcNow.AddMinutes(1), 102.0, 107.0, 100.0, 105.0, 1000);
hlv.Update(bar1, isNew: true);
var result1 = hlv.Last.Value;
hlv.Update(bar2, isNew: true);
var result2 = hlv.Last.Value;
Assert.NotEqual(result1, result2);
}
[Fact]
public void Update_IsNewFalse_UpdatesCurrentBar()
{
var hlv = new Hlv(period: 5);
var bar1 = new TBar(DateTime.UtcNow, 100.0, 105.0, 98.0, 102.0, 1000);
hlv.Update(bar1, isNew: true);
var firstValue = hlv.Last.Value;
// Update the same bar with different high-low values
var bar1Updated = new TBar(DateTime.UtcNow, 100.0, 110.0, 95.0, 108.0, 1000);
hlv.Update(bar1Updated, isNew: false);
var updatedValue = hlv.Last.Value;
Assert.NotEqual(firstValue, updatedValue);
}
[Fact]
public void Update_IterativeCorrections_RestoresState()
{
var hlv = new Hlv(period: 5);
var bars = GenerateTestData(10);
// Process first 5 bars
for (int i = 0; i < 5; i++)
{
hlv.Update(bars[i], isNew: true);
}
// Add bar 6 and correct multiple times
hlv.Update(bars[5], isNew: true);
hlv.Update(bars[5], isNew: false);
hlv.Update(bars[5], isNew: false);
hlv.Update(bars[5], isNew: false);
// Now continue with bar 7
hlv.Update(bars[6], isNew: true);
// Create new instance and process same data
var hlv2 = new Hlv(period: 5);
for (int i = 0; i < 7; i++)
{
hlv2.Update(bars[i], isNew: true);
}
Assert.Equal(hlv.Last.Value, hlv2.Last.Value, Tolerance);
}
#endregion
#region IsHot and Warmup Tests
[Fact]
public void IsHot_BeforeWarmup_ReturnsFalse()
{
var hlv = new Hlv(period: 10);
var bars = GenerateTestData(5);
for (int i = 0; i < bars.Count; i++)
{
hlv.Update(bars[i]);
}
Assert.False(hlv.IsHot);
}
[Fact]
public void IsHot_AfterWarmup_ReturnsTrue()
{
var hlv = new Hlv(period: 10);
var bars = GenerateTestData(15);
for (int i = 0; i < bars.Count; i++)
{
hlv.Update(bars[i]);
}
Assert.True(hlv.IsHot);
}
[Fact]
public void IsHot_ExactlyAtWarmup_ReturnsTrue()
{
var hlv = new Hlv(period: 10);
var bars = GenerateTestData(10);
for (int i = 0; i < bars.Count; i++)
{
hlv.Update(bars[i]);
}
Assert.True(hlv.IsHot);
}
#endregion
#region Reset Tests
[Fact]
public void Reset_ClearsState()
{
var hlv = new Hlv(period: 5);
var bars = GenerateTestData(10);
for (int i = 0; i < bars.Count; i++)
{
hlv.Update(bars[i]);
}
hlv.Reset();
Assert.False(hlv.IsHot);
Assert.Equal(0, hlv.Last.Value);
}
[Fact]
public void Reset_AllowsReprocessing()
{
var hlv = new Hlv(period: 5);
var bars = GenerateTestData(10);
// First pass
for (int i = 0; i < bars.Count; i++)
{
hlv.Update(bars[i]);
}
var firstResult = hlv.Last.Value;
// Reset and second pass
hlv.Reset();
for (int i = 0; i < bars.Count; i++)
{
hlv.Update(bars[i]);
}
var secondResult = hlv.Last.Value;
Assert.Equal(firstResult, secondResult, Tolerance);
}
#endregion
#region Robustness Tests
[Fact]
public void Update_WithNaNValues_UsesLastValidEstimator()
{
var hlv = new Hlv(period: 5);
var bars = GenerateTestData(10);
for (int i = 0; i < bars.Count; i++)
{
hlv.Update(bars[i]);
}
var valueBeforeInvalid = hlv.Last.Value;
// Bar with NaN high - should use last valid Parkinson estimator
var nanBar = new TBar(DateTime.UtcNow, 100.0, double.NaN, 98.0, 102.0, 1000);
var result = hlv.Update(nanBar);
// Result should be finite and close to previous (RMA smoothed)
Assert.True(double.IsFinite(result.Value), "Result should be finite when using last valid estimator");
Assert.True(result.Value >= 0, "Volatility should be non-negative");
double relativeDiff = Math.Abs(result.Value - valueBeforeInvalid) / valueBeforeInvalid;
Assert.True(relativeDiff < 0.2, $"Value should be similar to previous: {valueBeforeInvalid} vs {result.Value}");
}
[Fact]
public void Update_WithInfinityValues_UsesLastValidEstimator()
{
var hlv = new Hlv(period: 5);
var bars = GenerateTestData(10);
for (int i = 0; i < bars.Count; i++)
{
hlv.Update(bars[i]);
}
var valueBeforeInvalid = hlv.Last.Value;
// Bar with infinity - should use last valid Parkinson estimator
var infBar = new TBar(DateTime.UtcNow, 100.0, double.PositiveInfinity, 98.0, 102.0, 1000);
var result = hlv.Update(infBar);
Assert.True(double.IsFinite(result.Value), "Result should be finite when using last valid estimator");
Assert.True(result.Value >= 0, "Volatility should be non-negative");
double relativeDiff = Math.Abs(result.Value - valueBeforeInvalid) / valueBeforeInvalid;
Assert.True(relativeDiff < 0.2, $"Value should be similar to previous: {valueBeforeInvalid} vs {result.Value}");
}
[Fact]
public void Update_WithZeroPrices_UsesLastValidEstimator()
{
var hlv = new Hlv(period: 5);
var bars = GenerateTestData(10);
for (int i = 0; i < bars.Count; i++)
{
hlv.Update(bars[i]);
}
var valueBeforeInvalid = hlv.Last.Value;
// Bar with zero low (invalid for log) - should use last valid Parkinson estimator
var zeroBar = new TBar(DateTime.UtcNow, 100.0, 105.0, 0.0, 102.0, 1000);
var result = hlv.Update(zeroBar);
Assert.True(double.IsFinite(result.Value), "Result should be finite when using last valid estimator");
Assert.True(result.Value >= 0, "Volatility should be non-negative");
double relativeDiff = Math.Abs(result.Value - valueBeforeInvalid) / valueBeforeInvalid;
Assert.True(relativeDiff < 0.2, $"Value should be similar to previous: {valueBeforeInvalid} vs {result.Value}");
}
[Fact]
public void Update_WithNegativePrices_UsesLastValidEstimator()
{
var hlv = new Hlv(period: 5);
var bars = GenerateTestData(10);
for (int i = 0; i < bars.Count; i++)
{
hlv.Update(bars[i]);
}
var valueBeforeInvalid = hlv.Last.Value;
// Bar with negative price - should use last valid Parkinson estimator
var negBar = new TBar(DateTime.UtcNow, 100.0, 105.0, -98.0, 102.0, 1000);
var result = hlv.Update(negBar);
Assert.True(double.IsFinite(result.Value), "Result should be finite when using last valid estimator");
Assert.True(result.Value >= 0, "Volatility should be non-negative");
double relativeDiff = Math.Abs(result.Value - valueBeforeInvalid) / valueBeforeInvalid;
Assert.True(relativeDiff < 0.2, $"Value should be similar to previous: {valueBeforeInvalid} vs {result.Value}");
}
#endregion
#region Batch and Series Tests
[Fact]
public void Batch_MatchesStreamingResults()
{
const int dataCount = 100;
var bars = GenerateTestData(dataCount);
// Streaming
var hlvStreaming = new Hlv(period: 10);
var streamingResults = new double[dataCount];
for (int i = 0; i < dataCount; i++)
{
streamingResults[i] = hlvStreaming.Update(bars[i]).Value;
}
// Batch (HLV only uses high-low)
var highs = new double[dataCount];
var lows = new double[dataCount];
var batchResults = new double[dataCount];
for (int i = 0; i < dataCount; i++)
{
highs[i] = bars[i].High;
lows[i] = bars[i].Low;
}
Hlv.Batch(highs, lows, batchResults, period: 10);
// Compare last 50 values (after warmup)
for (int i = 50; i < dataCount; i++)
{
Assert.Equal(streamingResults[i], batchResults[i], Tolerance);
}
}
[Fact]
public void Calculate_TBarSeries_ReturnsCorrectLength()
{
const int dataCount = 50;
var barSeries = GenerateTestData(dataCount);
var result = Hlv.Calculate(barSeries, period: 10);
Assert.Equal(dataCount, result.Count);
}
[Fact]
public void Update_TBarSeries_MatchesStreamingResults()
{
const int dataCount = 50;
var barSeries = GenerateTestData(dataCount);
// Series update
var hlvSeries = new Hlv(period: 10);
var seriesResult = hlvSeries.Update(barSeries);
// Streaming
var hlvStreaming = new Hlv(period: 10);
var streamingResults = new double[dataCount];
for (int i = 0; i < dataCount; i++)
{
streamingResults[i] = hlvStreaming.Update(barSeries[i]).Value;
}
// Compare last 30 values
for (int i = 20; i < dataCount; i++)
{
Assert.Equal(streamingResults[i], seriesResult.Values[i], Tolerance);
}
}
[Fact]
public void Batch_EmptyInput_DoesNotThrow()
{
var highs = Array.Empty<double>();
var lows = Array.Empty<double>();
var output = Array.Empty<double>();
// Should not throw
Hlv.Batch(highs, lows, output, period: 10);
Assert.Empty(output);
}
[Fact]
public void Batch_MismatchedLengths_ThrowsArgumentException()
{
var highs = new double[10];
var lows = new double[5]; // Mismatched
var output = new double[10];
var ex = Assert.Throws<ArgumentException>(() =>
Hlv.Batch(highs, lows, output, period: 10));
Assert.Equal("low", ex.ParamName);
}
[Fact]
public void Batch_OutputTooShort_ThrowsArgumentException()
{
var highs = new double[10];
var lows = new double[10];
var output = new double[5]; // Too short
var ex = Assert.Throws<ArgumentException>(() =>
Hlv.Batch(highs, lows, output, period: 10));
Assert.Equal("output", ex.ParamName);
}
[Fact]
public void Batch_InvalidPeriod_ThrowsArgumentException()
{
var highs = new double[10];
var lows = new double[10];
var output = new double[10];
var ex = Assert.Throws<ArgumentException>(() =>
Hlv.Batch(highs, lows, output, period: 0));
Assert.Equal("period", ex.ParamName);
}
#endregion
#region Event Publishing Tests
[Fact]
public void Update_PublishesEvent()
{
var hlv = new Hlv(period: 5);
bool eventFired = false;
hlv.Pub += (object? sender, in TValueEventArgs args) => eventFired = true;
var bar = new TBar(DateTime.UtcNow, 100.0, 105.0, 98.0, 102.0, 1000);
hlv.Update(bar);
Assert.True(eventFired);
}
[Fact]
public void ChainedIndicator_ReceivesValues()
{
var source = new Hlv(period: 5);
var downstream = new Sma(source, period: 3);
var bars = GenerateTestData(10);
for (int i = 0; i < bars.Count; i++)
{
source.Update(bars[i]);
}
Assert.True(downstream.Last.Value > 0, "Downstream indicator should receive values");
}
#endregion
#region TValue Update Tests
[Fact]
public void Update_TValue_TreatsAsPrecomputedEstimator()
{
var hlv1 = new Hlv(period: 5);
var hlv2 = new Hlv(period: 5);
// For hlv1, use bar data
var bar = new TBar(DateTime.UtcNow, 100.0, 105.0, 98.0, 102.0, 1000);
hlv1.Update(bar);
// For hlv2, use pre-computed Parkinson estimator value
// Compute manually: (1/(4*ln(2))) * (ln(105)-ln(98))^2
double lnH = Math.Log(105.0);
double lnL = Math.Log(98.0);
double hlRange = lnH - lnL;
double C_4LN2_INV = 0.36067376022224085; // 1 / (4 * ln(2))
double pkEstimator = C_4LN2_INV * hlRange * hlRange;
var tvalue = new TValue(bar.Time, pkEstimator);
hlv2.Update(tvalue);
Assert.Equal(hlv1.Last.Value, hlv2.Last.Value, Tolerance);
}
#endregion
#region Additional Tests
[Fact]
public void LargeDataset_Performance()
{
var hlv = new Hlv(period: 20);
var bars = GenerateTestData(5000);
for (int i = 0; i < bars.Count; i++)
{
var result = hlv.Update(bars[i]);
Assert.True(double.IsFinite(result.Value));
}
}
[Fact]
public void DifferentParameters_ProduceDistinctValues()
{
var bars = GenerateTestData(50);
var hlv1 = new Hlv(period: 10);
var hlv2 = new Hlv(period: 20);
var hlv3 = new Hlv(period: 10, annualize: false);
for (int i = 0; i < bars.Count; i++)
{
hlv1.Update(bars[i]);
hlv2.Update(bars[i]);
hlv3.Update(bars[i]);
}
Assert.True(double.IsFinite(hlv1.Last.Value));
Assert.True(double.IsFinite(hlv2.Last.Value));
Assert.True(double.IsFinite(hlv3.Last.Value));
// Different parameters should produce different values
Assert.NotEqual(hlv1.Last.Value, hlv2.Last.Value);
Assert.NotEqual(hlv1.Last.Value, hlv3.Last.Value);
}
[Fact]
public void StaticCalculate_Works()
{
var bars = GenerateTestData(100);
var result = Hlv.Calculate(bars, period: 14);
Assert.Equal(100, result.Count);
Assert.True(double.IsFinite(result[result.Count - 1].Value));
}
[Fact]
public void StaticCalculate_ValidatesInput()
{
var bars = GenerateTestData(10);
Assert.Throws<ArgumentException>(() => Hlv.Calculate(bars, period: 0));
Assert.Throws<ArgumentException>(() => Hlv.Calculate(bars, period: -1));
Assert.Throws<ArgumentException>(() => Hlv.Calculate(bars, period: 10, annualize: true, annualPeriods: 0));
}
[Fact]
public void Prime_Works()
{
var hlv = new Hlv(period: 5);
var values = new double[] { 0.001, 0.002, 0.0015, 0.0018, 0.0012, 0.0022 };
hlv.Prime(values);
Assert.True(hlv.IsHot);
Assert.True(double.IsFinite(hlv.Last.Value));
}
[Fact]
public void Hlv_OnlyUsesHighLow_NotOpenClose()
{
// HLV (Parkinson) only uses High-Low, so changing Open/Close shouldn't affect result
var hlv1 = new Hlv(period: 5);
var hlv2 = new Hlv(period: 5);
// Bar with same High-Low but different Open-Close
var bar1 = new TBar(DateTime.UtcNow, 100.0, 105.0, 98.0, 102.0, 1000);
var bar2 = new TBar(DateTime.UtcNow, 99.0, 105.0, 98.0, 104.0, 1000); // Different O/C
var result1 = hlv1.Update(bar1).Value;
var result2 = hlv2.Update(bar2).Value;
// Results should be identical since only H-L matters
Assert.Equal(result1, result2, Tolerance);
}
#endregion
}
+647
View File
@@ -0,0 +1,647 @@
namespace QuanTAlib.Test;
using Xunit;
/// <summary>
/// Validation tests for HLV (High-Low Volatility / Parkinson Volatility).
/// HLV is a range-based volatility estimator using only High-Low prices.
/// Formula: parkinsonEstimator = (1/(4*ln(2))) * (lnH - lnL)²
/// RMA smoothing with bias correction applied.
/// </summary>
public class HlvValidationTests
{
private static TBarSeries GenerateTestData(int count = 100)
{
var gbm = new GBM(seed: 42);
return gbm.Fetch(count, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1));
}
// === Mathematical Validation ===
/// <summary>
/// Validates the Parkinson coefficient: 1/(4*ln(2)) ≈ 0.36067376
/// </summary>
[Fact]
public void Hlv_ParkinsonCoefficient_IsCorrect()
{
double expectedCoeff = 1.0 / (4.0 * Math.Log(2));
Assert.Equal(0.36067376022224085, expectedCoeff, 10);
}
/// <summary>
/// Validates RMA decay formula: decay = 1 - (1/period)
/// </summary>
[Theory]
[InlineData(14, 0.928571428571429)] // 1 - 1/14 = 13/14
[InlineData(20, 0.95)] // 1 - 1/20 = 19/20
[InlineData(10, 0.9)] // 1 - 1/10 = 9/10
public void Hlv_RmaDecay_IsCorrect(int period, double expectedDecay)
{
double decay = 1.0 - 1.0 / period;
Assert.Equal(expectedDecay, decay, 10);
}
/// <summary>
/// Validates Parkinson estimator formula: (1/(4*ln(2))) * (lnH - lnL)²
/// </summary>
[Fact]
public void Hlv_ParkinsonEstimatorFormula_IsCorrect()
{
double high = 105.0;
double low = 95.0;
double lnH = Math.Log(high);
double lnL = Math.Log(low);
double coeff = 1.0 / (4.0 * Math.Log(2));
double expectedPk = coeff * Math.Pow(lnH - lnL, 2);
// Manual calculation
// lnH - lnL = ln(105/95) ≈ 0.1001
// (lnH - lnL)² ≈ 0.01002
// coeff ≈ 0.36067
// Pk ≈ 0.36067 * 0.01002 ≈ 0.00361
Assert.True(expectedPk > 0, "Parkinson estimator should be positive for bars with range");
Assert.True(expectedPk < 0.1, "Parkinson estimator should be small for 10% range");
}
/// <summary>
/// Validates that flat bar (H=L) produces zero Parkinson estimator.
/// </summary>
[Fact]
public void Hlv_FlatBar_ProducesZeroPk()
{
double price = 100.0;
double lnH = Math.Log(price);
double lnL = Math.Log(price);
double coeff = 1.0 / (4.0 * Math.Log(2));
double pk = coeff * Math.Pow(lnH - lnL, 2); // 0
Assert.Equal(0.0, pk, 15);
}
/// <summary>
/// Validates bias correction formula: corrected = raw / (1 - decay^n)
/// </summary>
[Theory]
[InlineData(14, 5)] // Early in warmup
[InlineData(14, 14)] // At warmup
[InlineData(14, 50)] // Well past warmup
[InlineData(14, 100)] // Very late - correction should be minimal
public void Hlv_BiasCorrection_WorksCorrectly(int period, int count)
{
double decay = 1.0 - 1.0 / period;
double e = Math.Pow(decay, count);
double correctionFactor = 1.0 / (1.0 - e);
// Early: large correction needed
// Later: correction approaches 1.0
if (count < period)
{
Assert.True(correctionFactor > 1.05, "Early values should need significant correction");
}
else if (count > period * 5)
{
Assert.True(correctionFactor < 1.01, "Very late values should need minimal correction");
}
else if (count > period * 2)
{
Assert.True(correctionFactor < 1.1, "Late values should need small correction");
}
}
/// <summary>
/// Validates annualization factor: √(annualPeriods)
/// </summary>
[Theory]
[InlineData(252, 15.8745078663875)] // Daily trading days
[InlineData(365, 19.1049731745428)] // Calendar days
[InlineData(52, 7.21110255092798)] // Weekly
[InlineData(12, 3.46410161513775)] // Monthly
public void Hlv_AnnualizationFactor_IsCorrect(int annualPeriods, double expectedFactor)
{
double factor = Math.Sqrt(annualPeriods);
Assert.Equal(expectedFactor, factor, 10);
}
/// <summary>
/// Validates that wider range produces higher Parkinson estimator.
/// </summary>
[Fact]
public void Hlv_WiderRange_ProducesHigherPk()
{
// Narrow range bar
double narrowPk = ComputeParkinsonEstimator(101, 99);
// Wide range bar
double widePk = ComputeParkinsonEstimator(110, 90);
Assert.True(widePk > narrowPk,
"Wider range should produce higher Parkinson estimator");
}
/// <summary>
/// Validates that HLV only uses High-Low (ignores Open-Close).
/// Same H-L range with different O-C should produce identical results.
/// </summary>
[Fact]
public void Hlv_OnlyUsesHighLow_IgnoresOpenClose()
{
var hlv1 = new Hlv(14, annualize: false);
var hlv2 = new Hlv(14, annualize: false);
for (int i = 0; i < 30; i++)
{
// Same high/low range but different open/close
// Indicator 1: doji pattern (open = close)
var bar1 = new TBar(
DateTime.UtcNow.AddMinutes(i).Ticks,
100.0, 105.0, 95.0, 100.0, 1000.0
);
hlv1.Update(bar1);
// Indicator 2: directional move (open != close)
var bar2 = new TBar(
DateTime.UtcNow.AddMinutes(i).Ticks,
98.0, 105.0, 95.0, 104.0, 1000.0
);
hlv2.Update(bar2);
}
// HLV should be identical since H-L range is the same
Assert.Equal(hlv1.Last.Value, hlv2.Last.Value, 10);
}
// === Consistency Tests ===
/// <summary>
/// Validates streaming and batch produce identical results.
/// </summary>
[Fact]
public void Hlv_StreamingMatchesBatch()
{
var bars = GenerateTestData(100);
// Streaming calculation
var streamingHlv = new Hlv(14);
for (int i = 0; i < bars.Count; i++)
{
streamingHlv.Update(bars[i]);
}
// Batch calculation
var batchResult = Hlv.Calculate(bars, 14);
// Compare last values
Assert.Equal(batchResult.Last.Value, streamingHlv.Last.Value, 8);
}
/// <summary>
/// Validates TBarSeries input matches TBar streaming.
/// </summary>
[Fact]
public void Hlv_TBarSeriesInput_MatchesStreaming()
{
var bars = GenerateTestData(100);
// Streaming
var streamingHlv = new Hlv(14);
for (int i = 0; i < bars.Count; i++)
{
streamingHlv.Update(bars[i]);
}
// TBarSeries batch
var batchHlv = new Hlv(14);
var batchResult = batchHlv.Update(bars);
Assert.Equal(batchResult.Last.Value, streamingHlv.Last.Value, 10);
}
/// <summary>
/// Validates Span batch matches streaming.
/// </summary>
[Fact]
public void Hlv_SpanBatch_MatchesStreaming()
{
var bars = GenerateTestData(100);
// Streaming
var streamingHlv = new Hlv(14);
for (int i = 0; i < bars.Count; i++)
{
streamingHlv.Update(bars[i]);
}
// Extract H-L arrays
var highs = new double[bars.Count];
var lows = new double[bars.Count];
for (int i = 0; i < bars.Count; i++)
{
highs[i] = bars[i].High;
lows[i] = bars[i].Low;
}
// Span batch
var output = new double[bars.Count];
Hlv.Batch(highs, lows, output, 14);
Assert.Equal(output[^1], streamingHlv.Last.Value, 10);
}
/// <summary>
/// Validates annualized output is scaled correctly.
/// </summary>
[Fact]
public void Hlv_Annualized_ScaledCorrectly()
{
var bars = GenerateTestData(50);
// Non-annualized
var hlvRaw = new Hlv(14, annualize: false);
// Annualized (default 252 periods)
var hlvAnn = new Hlv(14, annualize: true, annualPeriods: 252);
for (int i = 0; i < bars.Count; i++)
{
hlvRaw.Update(bars[i]);
hlvAnn.Update(bars[i]);
}
double expectedRatio = Math.Sqrt(252);
double actualRatio = hlvAnn.Last.Value / hlvRaw.Last.Value;
Assert.Equal(expectedRatio, actualRatio, 6);
}
// === Parameter Sensitivity ===
/// <summary>
/// Validates shorter period produces more responsive volatility.
/// </summary>
[Fact]
public void Hlv_ShorterPeriod_MoreResponsive()
{
var bars = GenerateTestData(50);
var hlvShort = new Hlv(5);
var hlvLong = new Hlv(20);
var shortResults = new List<double>();
var longResults = new List<double>();
for (int i = 0; i < bars.Count; i++)
{
hlvShort.Update(bars[i]);
hlvLong.Update(bars[i]);
if (hlvShort.IsHot && hlvLong.IsHot)
{
shortResults.Add(hlvShort.Last.Value);
longResults.Add(hlvLong.Last.Value);
}
}
// Shorter period should have higher variance in results
double shortVar = Variance(shortResults);
double longVar = Variance(longResults);
Assert.True(shortResults.Count > 0, "Should have hot results");
Assert.True(shortVar > longVar * 0.5,
"Shorter period should generally be more variable");
}
/// <summary>
/// Validates different periods produce different results.
/// </summary>
[Fact]
public void Hlv_DifferentPeriods_ProduceDifferentResults()
{
var bars = GenerateTestData(50);
var hlv10 = new Hlv(10);
var hlv14 = new Hlv(14);
var hlv20 = new Hlv(20);
for (int i = 0; i < bars.Count; i++)
{
hlv10.Update(bars[i]);
hlv14.Update(bars[i]);
hlv20.Update(bars[i]);
}
Assert.NotEqual(hlv10.Last.Value, hlv14.Last.Value);
Assert.NotEqual(hlv14.Last.Value, hlv20.Last.Value);
}
// === Edge Cases ===
/// <summary>
/// Validates handling of very small ranges (tight consolidation).
/// </summary>
[Fact]
public void Hlv_VerySmallRanges_HandledCorrectly()
{
var hlv = new Hlv(14);
for (int i = 0; i < 30; i++)
{
var bar = new TBar(
DateTime.UtcNow.AddMinutes(i).Ticks,
100.0, 100.001, 99.999, 100.0, 1000.0
);
hlv.Update(bar);
}
Assert.True(double.IsFinite(hlv.Last.Value));
Assert.True(hlv.Last.Value >= 0, "Volatility should be non-negative");
}
/// <summary>
/// Validates handling of very large ranges (high volatility).
/// </summary>
[Fact]
public void Hlv_VeryLargeRanges_HandledCorrectly()
{
var hlv = new Hlv(14);
for (int i = 0; i < 30; i++)
{
var bar = new TBar(
DateTime.UtcNow.AddMinutes(i).Ticks,
100.0, 200.0, 50.0, 150.0, 1000.0
);
hlv.Update(bar);
}
Assert.True(double.IsFinite(hlv.Last.Value));
Assert.True(hlv.Last.Value > 0, "High volatility should produce positive value");
}
/// <summary>
/// Validates handling of constant bars (zero volatility).
/// </summary>
[Fact]
public void Hlv_ConstantBars_ProducesMinimalVolatility()
{
var hlv = new Hlv(14);
for (int i = 0; i < 30; i++)
{
var bar = new TBar(
DateTime.UtcNow.AddMinutes(i).Ticks,
100.0, 100.0, 100.0, 100.0, 1000.0
);
hlv.Update(bar);
}
Assert.True(double.IsFinite(hlv.Last.Value));
Assert.True(hlv.Last.Value < 0.001, "Constant price should produce near-zero volatility");
}
/// <summary>
/// Validates warmup period calculation.
/// </summary>
[Theory]
[InlineData(10)]
[InlineData(14)]
[InlineData(20)]
public void Hlv_WarmupPeriod_IsCorrect(int period)
{
var hlv = new Hlv(period);
Assert.Equal(period, hlv.WarmupPeriod);
}
/// <summary>
/// Validates output is always non-negative (volatility property).
/// </summary>
[Fact]
public void Hlv_Output_IsNonNegative()
{
var bars = GenerateTestData(100);
var hlv = new Hlv(14);
for (int i = 0; i < bars.Count; i++)
{
hlv.Update(bars[i]);
if (hlv.IsHot)
{
Assert.True(hlv.Last.Value >= 0,
$"Volatility should be non-negative at bar {i}");
}
}
}
/// <summary>
/// Validates bar correction works correctly.
/// </summary>
[Fact]
public void Hlv_BarCorrection_WorksCorrectly()
{
var hlv = new Hlv(14);
var bars = GenerateTestData(30);
// Feed initial bars
for (int i = 0; i < 20; i++)
{
hlv.Update(bars[i], isNew: true);
}
// Add new bar
hlv.Update(bars[20], isNew: true);
double afterNew = hlv.Last.Value;
// Correct with different bar (much higher volatility)
var correctedBar = new TBar(
bars[20].Time,
100, 200, 50, 150, 1000
);
hlv.Update(correctedBar, isNew: false);
double afterCorrection = hlv.Last.Value;
// Restore original
hlv.Update(bars[20], isNew: false);
double afterRestore = hlv.Last.Value;
Assert.NotEqual(afterNew, afterCorrection);
Assert.Equal(afterNew, afterRestore, 10);
}
/// <summary>
/// Validates iterative corrections converge to same result.
/// </summary>
[Fact]
public void Hlv_IterativeCorrections_Converge()
{
var hlv = new Hlv(14);
var bars = GenerateTestData(30);
// Feed bars and make corrections
for (int i = 0; i < 20; i++)
{
hlv.Update(bars[i], isNew: true);
}
// Multiple corrections on same bar
for (int j = 0; j < 5; j++)
{
var tempBar = new TBar(
bars[19].Time,
100 + j, 110 + j, 90 + j, 105 + j, 1000
);
hlv.Update(tempBar, isNew: false);
}
// Final correction back to original
hlv.Update(bars[19], isNew: false);
double afterCorrections = hlv.Last.Value;
// Fresh calculation
var hlvFresh = new Hlv(14);
for (int i = 0; i < 20; i++)
{
hlvFresh.Update(bars[i], isNew: true);
}
double freshValue = hlvFresh.Last.Value;
Assert.Equal(freshValue, afterCorrections, 10);
}
// === Comparison with Theoretical Properties ===
/// <summary>
/// Validates HLV stability over repeated runs with same seed.
/// </summary>
[Fact]
public void Hlv_Stability_ConsistentOverRepeatedRuns()
{
// Multiple runs with same seed should produce identical results
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 hlv = new Hlv(14);
for (int i = 0; i < bars.Count; i++)
{
hlv.Update(bars[i]);
}
results.Add(hlv.Last.Value);
}
// All runs should be identical
Assert.Equal(results[0], results[1], 15);
Assert.Equal(results[1], results[2], 15);
}
/// <summary>
/// Validates HLV responds to volatility regime changes.
/// </summary>
[Fact]
public void Hlv_RespondsToVolatilityRegimeChange()
{
var hlv = new Hlv(10);
// Low volatility regime
for (int i = 0; i < 20; i++)
{
var bar = new TBar(
DateTime.UtcNow.AddMinutes(i).Ticks,
100.0, 101.0, 99.0, 100.0, 1000.0 // 2% range
);
hlv.Update(bar);
}
double lowVolValue = hlv.Last.Value;
// High volatility regime
for (int i = 20; i < 40; i++)
{
var bar = new TBar(
DateTime.UtcNow.AddMinutes(i).Ticks,
100.0, 110.0, 90.0, 100.0, 1000.0 // 20% range
);
hlv.Update(bar);
}
double highVolValue = hlv.Last.Value;
Assert.True(highVolValue > lowVolValue * 2,
"HLV should significantly increase with higher volatility regime");
}
/// <summary>
/// Validates HLV vs GKV: same range, HLV ignores O-C while GKV uses it.
/// </summary>
[Fact]
public void Hlv_VsGkv_DifferentBehavior()
{
var hlv = new Hlv(14, annualize: false);
var gkv = new Gkv(14, annualize: false);
// Same bars
for (int i = 0; i < 30; i++)
{
// Directional bar (O != C)
var bar = new TBar(
DateTime.UtcNow.AddMinutes(i).Ticks,
100.0, 105.0, 95.0, 104.0, 1000.0
);
hlv.Update(bar);
gkv.Update(bar);
}
// Both should produce positive values
Assert.True(hlv.Last.Value > 0);
Assert.True(gkv.Last.Value > 0);
// They should be different since GKV uses O-C term
Assert.NotEqual(hlv.Last.Value, gkv.Last.Value);
}
// === Efficiency Comparison ===
/// <summary>
/// Validates Parkinson efficiency factor is approximately 5.2x close-to-close.
/// This is a theoretical property - we just verify HLV produces reasonable values.
/// </summary>
[Fact]
public void Hlv_ProducesReasonableVolatilityEstimate()
{
var bars = GenerateTestData(100);
var hlv = new Hlv(14, annualize: false);
for (int i = 0; i < bars.Count; i++)
{
hlv.Update(bars[i]);
}
// HLV should be positive and finite
Assert.True(double.IsFinite(hlv.Last.Value));
Assert.True(hlv.Last.Value > 0);
Assert.True(hlv.Last.Value < 10, "Raw volatility should be reasonable (< 1000%)");
}
// === Helper Methods ===
private static double ComputeParkinsonEstimator(double high, double low)
{
double lnH = Math.Log(high);
double lnL = Math.Log(low);
double coeff = 1.0 / (4.0 * Math.Log(2));
return coeff * Math.Pow(lnH - lnL, 2);
}
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));
}
}
+551
View File
@@ -0,0 +1,551 @@
// High-Low Volatility (HLV) Indicator
// A range-based volatility estimator using the Parkinson method with RMA smoothing
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
namespace QuanTAlib;
/// <summary>
/// HLV: High-Low Volatility (Parkinson)
/// A range-based volatility estimator that uses only High-Low prices,
/// providing more efficient volatility estimates than close-to-close methods.
/// </summary>
/// <remarks>
/// <b>Calculation steps:</b>
/// <list type="number">
/// <item>Calculate log prices: lnH, lnL</item>
/// <item>parkinsonEstimator = (1/(4×ln(2))) × (lnH - lnL)²</item>
/// <item>Smooth using bias-corrected RMA</item>
/// <item>volatility = √(smoothedEstimator)</item>
/// <item>If annualize: volatility × √(annualPeriods)</item>
/// </list>
///
/// <b>Key characteristics:</b>
/// <list type="bullet">
/// <item>Uses only High-Low data (simpler than Garman-Klass)</item>
/// <item>RMA (Wilder's) smoothing with bias correction</item>
/// <item>Optional annualization (default 252 trading days)</item>
/// <item>5× more efficient than close-to-close estimators</item>
/// </list>
///
/// <b>Sources:</b>
/// Michael Parkinson (1980). "The Extreme Value Method for Estimating the Variance
/// of the Rate of Return." Journal of Business, 53(1), 61-65.
/// </remarks>
[SkipLocalsInit]
public sealed class Hlv : AbstractBase
{
private const double C_4LN2_INV = 0.36067376022224085; // 1 / (4 * ln(2))
private const double Epsilon = 1e-10;
private readonly int _period;
private readonly bool _annualize;
private readonly int _annualPeriods;
private readonly double _alpha;
private readonly double _decay;
private readonly double _annualFactor;
[StructLayout(LayoutKind.Auto)]
private record struct State(
double RawRma,
double E,
double LastValidPk,
double LastValue,
int Count
);
private State _s;
private State _ps;
/// <summary>
/// Initializes a new instance of the Hlv class.
/// </summary>
/// <param name="period">The smoothing period (default 20).</param>
/// <param name="annualize">Whether to annualize the volatility (default true).</param>
/// <param name="annualPeriods">Number of periods per year (default 252).</param>
/// <exception cref="ArgumentException">
/// Thrown when period is less than 1, or annualPeriods is less than 1 when annualizing.
/// </exception>
public Hlv(int period = 20, bool annualize = true, int annualPeriods = 252)
{
if (period <= 0)
{
throw new ArgumentException("Period must be greater than 0", nameof(period));
}
if (annualize && annualPeriods <= 0)
{
throw new ArgumentException("Annual periods must be greater than 0 when annualizing", nameof(annualPeriods));
}
_period = period;
_annualize = annualize;
_annualPeriods = annualPeriods;
_alpha = 1.0 / period;
_decay = 1.0 - _alpha;
_annualFactor = annualize ? Math.Sqrt(annualPeriods) : 1.0;
WarmupPeriod = period;
Name = $"Hlv({period})";
_s = new State(0, 1.0, 0, 0, 0);
_ps = _s;
}
/// <summary>
/// Initializes a new instance of the Hlv class with a source.
/// </summary>
/// <param name="source">The data source for chaining.</param>
/// <param name="period">The smoothing period (default 20).</param>
/// <param name="annualize">Whether to annualize the volatility (default true).</param>
/// <param name="annualPeriods">Number of periods per year (default 252).</param>
public Hlv(ITValuePublisher source, int period = 20, bool annualize = true, int annualPeriods = 252)
: this(period, annualize, annualPeriods)
{
source.Pub += Handle;
}
private void Handle(object? sender, in TValueEventArgs e) => Update(e.Value, e.IsNew);
/// <summary>
/// True if the indicator has enough data for valid results.
/// </summary>
public override bool IsHot => _s.Count >= WarmupPeriod;
/// <summary>
/// The smoothing period.
/// </summary>
public int Period => _period;
/// <summary>
/// Whether volatility is annualized.
/// </summary>
public bool Annualize => _annualize;
/// <summary>
/// Number of periods per year for annualization.
/// </summary>
public int AnnualPeriods => _annualPeriods;
/// <summary>
/// Computes the Parkinson estimator for a single bar.
/// </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static double ComputeParkinsonEstimator(double high, double low)
{
double lnH = Math.Log(high);
double lnL = Math.Log(low);
double hlRange = lnH - lnL;
// parkinsonEstimator = (1/(4*ln(2))) * (lnH - lnL)^2
return C_4LN2_INV * hlRange * hlRange;
}
/// <summary>
/// Updates the indicator with a TValue input.
/// For HLV, this treats the value as a pre-computed Parkinson estimator.
/// Prefer Update(TBar) for standard OHLC data.
/// </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public override TValue Update(TValue input, bool isNew = true)
{
return UpdateCore(input.Time, input.Value, isNew);
}
/// <summary>
/// Updates the indicator with a new bar (preferred method).
/// </summary>
/// <param name="bar">The input bar.</param>
/// <param name="isNew">Whether this is a new bar or an update.</param>
/// <returns>The calculated volatility value.</returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public TValue Update(TBar bar, bool isNew = true)
{
// Handle invalid High-Low data
if (!double.IsFinite(bar.High) || !double.IsFinite(bar.Low) ||
bar.High <= 0 || bar.Low <= 0)
{
// Pass NaN to trigger last-valid-value substitution
return UpdateCore(bar.Time, double.NaN, isNew);
}
double pkEstimator = ComputeParkinsonEstimator(bar.High, bar.Low);
return UpdateCore(bar.Time, pkEstimator, isNew);
}
/// <summary>
/// Updates the indicator with a bar series.
/// </summary>
/// <param name="source">The source bar series.</param>
/// <returns>A TSeries containing the volatility values.</returns>
public TSeries Update(TBarSeries source)
{
if (source.Count == 0)
{
return [];
}
int len = source.Count;
var t = new List<long>(len);
var v = new List<double>(len);
CollectionsMarshal.SetCount(t, len);
CollectionsMarshal.SetCount(v, len);
var tSpan = CollectionsMarshal.AsSpan(t);
var vSpan = CollectionsMarshal.AsSpan(v);
// Extract High-Low data
Span<double> highs = len <= 128 ? stackalloc double[len] : new double[len];
Span<double> lows = len <= 128 ? stackalloc double[len] : new double[len];
for (int i = 0; i < len; i++)
{
highs[i] = source[i].High;
lows[i] = source[i].Low;
tSpan[i] = source[i].Time;
}
Batch(highs, lows, vSpan, _period, _annualize, _annualPeriods);
// Update internal state
for (int i = 0; i < len; i++)
{
Update(source[i], isNew: true);
}
return new TSeries(t, v);
}
/// <inheritdoc/>
public override TSeries Update(TSeries source)
{
int len = source.Count;
var t = new List<long>(len);
var v = new List<double>(len);
CollectionsMarshal.SetCount(t, len);
CollectionsMarshal.SetCount(v, len);
var tSpan = CollectionsMarshal.AsSpan(t);
var vSpan = CollectionsMarshal.AsSpan(v);
// Treat source values as pre-computed Parkinson estimators
BatchFromEstimators(source.Values, vSpan, _period, _annualize, _annualPeriods);
source.Times.CopyTo(tSpan);
// Update internal state
for (int i = 0; i < len; i++)
{
Update(new TValue(source.Times[i], source.Values[i]), isNew: true);
}
return new TSeries(t, v);
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private TValue UpdateCore(long timeTicks, double pkEstimator, bool isNew)
{
if (isNew)
{
_ps = _s;
}
else
{
_s = _ps;
}
var s = _s;
// Handle non-finite estimator - use last valid value
if (!double.IsFinite(pkEstimator))
{
pkEstimator = s.LastValidPk;
}
else
{
s.LastValidPk = pkEstimator;
}
// RMA smoothing with bias correction
double rawRma, e;
if (s.Count == 0)
{
rawRma = pkEstimator;
e = _decay;
}
else
{
// RMA: raw_rma = prev_rma * decay + alpha * value
rawRma = Math.FusedMultiplyAdd(s.RawRma, _decay, _alpha * pkEstimator);
e = _decay * s.E;
}
// Bias correction
double correctedRma = e > Epsilon ? rawRma / (1.0 - e) : rawRma;
// Calculate volatility
double volatility;
if (correctedRma < 0)
{
volatility = 0; // Can't take sqrt of negative
}
else
{
volatility = Math.Sqrt(correctedRma) * _annualFactor;
}
if (!double.IsFinite(volatility))
{
volatility = s.LastValue;
}
// Update state using direct field assignment (like Cvi pattern)
s.RawRma = rawRma;
s.E = e;
s.LastValue = volatility;
if (isNew)
{
s.Count++;
}
_s = s;
Last = new TValue(timeTicks, volatility);
PubEvent(Last, isNew);
return Last;
}
/// <inheritdoc/>
public override void Prime(ReadOnlySpan<double> source, TimeSpan? step = null)
{
for (int i = 0; i < source.Length; i++)
{
Update(new TValue(DateTime.UtcNow, source[i]), isNew: true);
}
}
/// <inheritdoc/>
public override void Reset()
{
_s = new State(0, 1.0, 0, 0, 0);
_ps = _s;
Last = default;
}
/// <summary>
/// Calculates High-Low Volatility for a bar series (static).
/// </summary>
/// <param name="source">The source bar series.</param>
/// <param name="period">The smoothing period.</param>
/// <param name="annualize">Whether to annualize.</param>
/// <param name="annualPeriods">Periods per year.</param>
/// <returns>A TSeries containing the volatility values.</returns>
public static TSeries Calculate(TBarSeries source, int period = 20, bool annualize = true, int annualPeriods = 252)
{
var hlv = new Hlv(period, annualize, annualPeriods);
return hlv.Update(source);
}
/// <summary>
/// Calculates HLV for a TSeries (treats values as pre-computed Parkinson estimators).
/// </summary>
public static TSeries Calculate(TSeries source, int period = 20, bool annualize = true, int annualPeriods = 252)
{
if (period <= 0)
{
throw new ArgumentException("Period must be greater than 0", nameof(period));
}
if (annualize && annualPeriods <= 0)
{
throw new ArgumentException("Annual periods must be greater than 0 when annualizing", nameof(annualPeriods));
}
int len = source.Count;
var t = new List<long>(len);
var v = new List<double>(len);
CollectionsMarshal.SetCount(t, len);
CollectionsMarshal.SetCount(v, len);
var tSpan = CollectionsMarshal.AsSpan(t);
var vSpan = CollectionsMarshal.AsSpan(v);
BatchFromEstimators(source.Values, vSpan, period, annualize, annualPeriods);
source.Times.CopyTo(tSpan);
return new TSeries(t, v);
}
/// <summary>
/// Batch calculation using spans for High-Low data.
/// </summary>
/// <param name="high">High prices.</param>
/// <param name="low">Low prices.</param>
/// <param name="output">Output volatility values.</param>
/// <param name="period">The smoothing period.</param>
/// <param name="annualize">Whether to annualize.</param>
/// <param name="annualPeriods">Periods per year.</param>
public static void Batch(
ReadOnlySpan<double> high,
ReadOnlySpan<double> low,
Span<double> output,
int period = 20,
bool annualize = true,
int annualPeriods = 252)
{
if (period <= 0)
{
throw new ArgumentException("Period must be greater than 0", nameof(period));
}
if (annualize && annualPeriods <= 0)
{
throw new ArgumentException("Annual periods must be greater than 0 when annualizing", nameof(annualPeriods));
}
int len = high.Length;
if (low.Length != len)
{
throw new ArgumentException("High and low spans must have the same length", nameof(low));
}
if (output.Length < len)
{
throw new ArgumentException("Output span must be at least as long as input spans", nameof(output));
}
if (len == 0)
{
return;
}
double alpha = 1.0 / period;
double decay = 1.0 - alpha;
double annualFactor = annualize ? Math.Sqrt(annualPeriods) : 1.0;
double rawRma = 0;
double e = 1.0;
double lastValidPk = 0;
double lastValue = 0;
for (int i = 0; i < len; i++)
{
double h = high[i];
double l = low[i];
double pkEstimator;
// Handle invalid data
if (!double.IsFinite(h) || !double.IsFinite(l) ||
h <= 0 || l <= 0)
{
pkEstimator = lastValidPk;
}
else
{
pkEstimator = ComputeParkinsonEstimator(h, l);
if (!double.IsFinite(pkEstimator))
{
pkEstimator = lastValidPk;
}
else
{
lastValidPk = pkEstimator;
}
}
if (i == 0)
{
rawRma = pkEstimator;
e = decay;
}
else
{
rawRma = Math.FusedMultiplyAdd(rawRma, decay, alpha * pkEstimator);
e *= decay;
}
double correctedRma = e > Epsilon ? rawRma / (1.0 - e) : rawRma;
double volatility = correctedRma < 0 ? 0 : Math.Sqrt(correctedRma) * annualFactor;
if (!double.IsFinite(volatility))
{
volatility = lastValue;
}
else
{
lastValue = volatility;
}
output[i] = volatility;
}
}
/// <summary>
/// Batch calculation from pre-computed Parkinson estimators.
/// </summary>
private static void BatchFromEstimators(
ReadOnlySpan<double> estimators,
Span<double> output,
int period,
bool annualize,
int annualPeriods)
{
if (period <= 0)
{
throw new ArgumentException("Period must be greater than 0", nameof(period));
}
if (estimators.Length != output.Length)
{
throw new ArgumentException("Source and output must have the same length", nameof(output));
}
int len = estimators.Length;
if (len == 0)
{
return;
}
double alpha = 1.0 / period;
double decay = 1.0 - alpha;
double annualFactor = annualize ? Math.Sqrt(annualPeriods) : 1.0;
double rawRma = 0;
double e = 1.0;
double lastValidPk = 0;
double lastValue = 0;
for (int i = 0; i < len; i++)
{
double pkEstimator = estimators[i];
if (!double.IsFinite(pkEstimator))
{
pkEstimator = lastValidPk;
}
else
{
lastValidPk = pkEstimator;
}
if (i == 0)
{
rawRma = pkEstimator;
e = decay;
}
else
{
rawRma = Math.FusedMultiplyAdd(rawRma, decay, alpha * pkEstimator);
e *= decay;
}
double correctedRma = e > Epsilon ? rawRma / (1.0 - e) : rawRma;
double volatility = correctedRma < 0 ? 0 : Math.Sqrt(correctedRma) * annualFactor;
if (!double.IsFinite(volatility))
{
volatility = lastValue;
}
else
{
lastValue = volatility;
}
output[i] = volatility;
}
}
}
+292
View File
@@ -0,0 +1,292 @@
# HLV: High-Low Volatility (Parkinson)
> "The simplest solution is often the most elegant. When you only need the peaks and valleys, why ask for the whole journey?"
High-Low Volatility (HLV), also known as the Parkinson estimator, is a range-based volatility measure that uses only the high and low prices of each period. Developed by Michael Parkinson in 1980, this estimator achieves approximately 5x better efficiency than close-to-close methods by exploiting the information content in the trading range. The implementation includes RMA (Wilder's) smoothing with bias correction and optional annualization.
## Historical Context
Michael Parkinson introduced this estimator in his 1980 paper "The Extreme Value Method for Estimating the Variance of the Rate of Return," published in the Journal of Business. The paper demonstrated that the high-low range of a diffusion process contains significantly more information about volatility than the closing price alone.
The Parkinson estimator is the simplest of the range-based volatility estimators, requiring only two data points per period (high and low) rather than the four required by Garman-Klass or Yang-Zhang. This simplicity makes it particularly useful when open and close prices are unavailable or unreliable, such as in some commodity markets or older historical data.
The famous coefficient $\frac{1}{4\ln 2} \approx 0.3607$ emerges from the mathematical derivation assuming prices follow a continuous geometric Brownian motion without drift. This coefficient normalizes the squared log range to produce an unbiased variance estimate.
## Architecture & Physics
### 1. Log Price Transformation
All calculations use log prices to normalize percentage returns:
$$
\ln H_t, \ln L_t
$$
where:
- $H_t, L_t$ = High, Low prices at time $t$
Log transformation ensures that equal percentage moves have equal magnitude regardless of price level.
### 2. Parkinson Estimator
The single-period Parkinson variance estimator uses only the log high-low range:
$$
\hat{\sigma}^2_{P,t} = \frac{1}{4\ln 2} \cdot (\ln H_t - \ln L_t)^2
$$
Equivalently:
$$
\hat{\sigma}^2_{P,t} = C \cdot r_{HL}^2
$$
where:
- $r_{HL} = \ln H_t - \ln L_t$ (log high-low range)
- $C = \frac{1}{4\ln 2} \approx 0.36067376$ (Parkinson coefficient)
### 3. RMA Smoothing with Bias Correction
The raw estimator is smoothed using an RMA (Wilder's Moving Average):
$$
RMA_t^{raw} = RMA_{t-1}^{raw} \cdot (1 - \alpha) + \alpha \cdot \hat{\sigma}^2_{P,t}
$$
where:
- $\alpha = 1 / period$
- Default $period = 20$
Bias correction compensates for the exponential startup:
$$
e_t = (1 - \alpha)^t
$$
$$
RMA_t^{corrected} = \frac{RMA_t^{raw}}{1 - e_t}
$$
### 4. Volatility Calculation
Convert variance to volatility (standard deviation):
$$
\sigma_t = \sqrt{RMA_t^{corrected}}
$$
### 5. Optional Annualization
If annualization is enabled (default):
$$
\sigma_{annual,t} = \sigma_t \times \sqrt{N}
$$
where $N$ = annual periods (default 252 trading days).
## Mathematical Foundation
### Parkinson Coefficient Derivation
The coefficient $\frac{1}{4\ln 2}$ arises from the distribution of the range of a standard Brownian motion. For a diffusion process $dS = \sigma S dW$ over time interval $\Delta t$:
The expected value of the squared log range is:
$$
E[(\ln H - \ln L)^2] = 4 \ln 2 \cdot \sigma^2 \cdot \Delta t
$$
Therefore, to obtain an unbiased estimator of variance:
$$
\hat{\sigma}^2 = \frac{(\ln H - \ln L)^2}{4 \ln 2}
$$
The coefficient:
$$
\frac{1}{4\ln 2} = \frac{1}{4 \times 0.693147...} \approx 0.36067376
$$
### Efficiency Comparison
| Estimator | Relative Efficiency | Data Required |
| :--- | :---: | :--- |
| Close-to-Close | 1.0 | C |
| Parkinson (HLV) | 5.2 | H, L |
| Garman-Klass (GKV) | 7.4 | O, H, L, C |
| Rogers-Satchell | 8.4 | O, H, L, C |
| Yang-Zhang | 14.0 | O, H, L, C |
HLV (Parkinson) achieves 5.2x the efficiency of close-to-close, meaning it produces the same statistical precision with 5.2x fewer observations. While less efficient than OHLC-based estimators, it requires only high-low data.
### RMA Properties
**Smoothing Factor:**
$$
\alpha = \frac{1}{period}
$$
| Period | α | Half-life (bars) |
| :---: | :---: | :---: |
| 10 | 0.100 | 6.6 |
| 14 | 0.071 | 9.4 |
| 20 | 0.050 | 13.5 |
| 30 | 0.033 | 20.5 |
RMA (Wilder's) is more responsive than SMA but slower than EMA with equivalent period.
### Annualization Factor
For daily data with 252 trading days:
$$
\sqrt{252} \approx 15.875
$$
Common annualization factors:
| Data Frequency | Periods/Year | Factor |
| :--- | :---: | :---: |
| Daily | 252 | 15.875 |
| Weekly | 52 | 7.211 |
| Monthly | 12 | 3.464 |
| Hourly (6.5h/day) | 1638 | 40.472 |
## Performance Profile
### Operation Count (Streaming Mode, Scalar)
Per-bar operations after warmup:
| Operation | Count | Cost (cycles) | Subtotal |
| :--- | :---: | :---: | :---: |
| LOG | 2 | 25 | 50 |
| SUB | 1 | 1 | 1 |
| MUL | 2 | 3 | 6 |
| FMA (RMA) | 1 | 4 | 4 |
| DIV (bias) | 1 | 15 | 15 |
| SQRT | 1 | 15 | 15 |
| MUL (annual) | 1 | 3 | 3 |
| **Total** | — | — | **~94 cycles** |
The dominant cost is the two LOG operations (53% of total). HLV is ~37% faster than GKV due to requiring only 2 logs instead of 4.
### Batch Mode (512 values, SIMD/FMA)
| Operation | Scalar Ops | SIMD Ops (AVX2) | Speedup |
| :--- | :---: | :---: | :---: |
| LOG (vectorized) | 1024 | 128 | 8× |
| Range calculations | 512 | 64 | 8× |
| RMA (sequential) | 512 | 512 | 1× |
| SQRT (vectorized) | 512 | 64 | 8× |
**Note:** RMA smoothing is inherently sequential, limiting total batch improvement. LOG operations benefit most from SIMD vectorization.
### Memory Profile
- **Per instance:** ~56 bytes (state struct)
- **No ring buffer required** (RMA is recursive)
- **100 instances:** ~5.6 KB
### Quality Metrics
| Metric | Score | Notes |
| :--- | :---: | :--- |
| **Accuracy** | 8/10 | Optimal under Brownian motion, simpler than GKV |
| **Efficiency** | 8/10 | 5.2x better than close-to-close |
| **Timeliness** | 7/10 | RMA introduces smoothing lag |
| **Smoothness** | 8/10 | RMA provides stable output |
| **Simplicity** | 10/10 | Only requires high-low data |
## Validation
HLV (Parkinson) is well-documented in academic literature but less common in technical analysis libraries:
| Library | Status | Notes |
| :--- | :---: | :--- |
| **TA-Lib** | N/A | Not implemented |
| **Skender** | N/A | Not implemented |
| **Tulip** | N/A | Not implemented |
| **OoplesFinance** | N/A | Not implemented |
| **PineScript** | ✅ | Matches hlv.pine reference |
| **Manual** | ✅ | Validated against formula |
The implementation is validated against the original Parkinson 1980 paper formula.
## Common Pitfalls
1. **Warmup period**: HLV requires $period$ bars before producing stable results. With default period=20, the first 19 values are warming up. The `IsHot` property indicates when warmup is complete.
2. **Zero range handling**: When high equals low (no trading range), the log range is zero, producing zero volatility for that bar. This can occur with limit-locked securities or thin markets.
3. **Invalid price data**: Prices must be positive for log transformation. Zero or negative prices, or logically invalid data (high < low) trigger last-valid-value substitution.
4. **Annualization assumption**: Default annualization assumes 252 trading days/year. For other frequencies (hourly, weekly), adjust the `annualPeriods` parameter accordingly.
5. **Drift bias**: The Parkinson estimator assumes zero drift (no trend). During strong trends, the estimator tends to underestimate volatility because trending prices compress the high-low range relative to the true volatility.
6. **No overnight information**: Unlike close-to-close methods, HLV doesn't capture overnight gaps at all. It only measures intraday range volatility, missing inter-day price movements.
7. **Comparison with GKV**: HLV is simpler (2 prices vs 4) but less efficient (5.2x vs 7.4x). Use GKV when OHLC data is available and reliability matters; use HLV when only high-low data exists.
## Trading Applications
### Position Sizing
Use HLV to scale position sizes inversely with volatility:
```
Position size = Risk per trade / (HLV × Price × multiplier)
```
Lower HLV allows larger positions; higher HLV requires smaller positions.
### Volatility Comparison
Compare HLV across similar assets:
```
If Asset A HLV < Asset B HLV: Asset A has lower intraday volatility
Useful for: Sector rotation, pairs trading selection
```
### Range Breakout Calibration
Use HLV to set dynamic breakout thresholds:
```
Breakout threshold = Current price ± (HLV × K × Price)
where K is a multiplier (typically 1.5-3.0)
```
### Volatility Regime Detection
Track HLV percentile rank over lookback period:
```
High rank (>80%): High volatility regime — reduce position size, widen stops
Low rank (<20%): Low volatility regime — potential for breakout
```
### Options Pricing Input
HLV provides a realized volatility estimate for comparison with implied volatility:
```
If IV > HLV significantly: Options may be overpriced (sell vol)
If IV < HLV significantly: Options may be underpriced (buy vol)
Note: HLV may underestimate true volatility due to drift bias
```
## References
- Parkinson, M. (1980). "The Extreme Value Method for Estimating the Variance of the Rate of Return." *Journal of Business*, 53(1), 61-65.
- Garman, M. B., & Klass, M. J. (1980). "On the Estimation of Security Price Volatilities from Historical Data." *Journal of Business*, 53(1), 67-78.
- Rogers, L. C. G., & Satchell, S. E. (1991). "Estimating Variance from High, Low and Closing Prices." *Annals of Applied Probability*, 1(4), 504-512.
- Alizadeh, S., Brandt, M. W., & Diebold, F. X. (2002). "Range-Based Estimation of Stochastic Volatility Models." *Journal of Finance*, 57(3), 1047-1091.