mirror of
https://github.com/mihakralj/QuanTAlib.git
synced 2026-08-17 01:58:06 +00:00
more volatilty
This commit is contained in:
@@ -0,0 +1,345 @@
|
||||
using TradingPlatform.BusinessLayer;
|
||||
using QuanTAlib;
|
||||
|
||||
namespace QuanTAlib.Tests;
|
||||
|
||||
public class RvIndicatorTests
|
||||
{
|
||||
[Fact]
|
||||
public void RvIndicator_Constructor_SetsDefaults()
|
||||
{
|
||||
var indicator = new RvIndicator();
|
||||
|
||||
Assert.Equal(5, indicator.Period);
|
||||
Assert.Equal(20, indicator.SmoothingPeriod);
|
||||
Assert.True(indicator.Annualize);
|
||||
Assert.Equal(252, indicator.AnnualPeriods);
|
||||
Assert.True(indicator.ShowColdValues);
|
||||
Assert.Equal("RV - Realized Volatility", indicator.Name);
|
||||
Assert.True(indicator.SeparateWindow);
|
||||
Assert.True(indicator.OnBackGround);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void RvIndicator_ShortName_IncludesParameters()
|
||||
{
|
||||
var indicator = new RvIndicator { Period = 10, SmoothingPeriod = 15 };
|
||||
Assert.Contains("RV", indicator.ShortName, StringComparison.Ordinal);
|
||||
Assert.Contains("10", indicator.ShortName, StringComparison.Ordinal);
|
||||
Assert.Contains("15", indicator.ShortName, StringComparison.Ordinal);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void RvIndicator_MinHistoryDepths_EqualsZero()
|
||||
{
|
||||
var indicator = new RvIndicator();
|
||||
|
||||
Assert.Equal(0, RvIndicator.MinHistoryDepths);
|
||||
Assert.Equal(0, ((IWatchlistIndicator)indicator).MinHistoryDepths);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void RvIndicator_Initialize_CreatesInternalRv()
|
||||
{
|
||||
var indicator = new RvIndicator();
|
||||
|
||||
indicator.Initialize();
|
||||
|
||||
Assert.Single(indicator.LinesSeries);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void RvIndicator_ProcessUpdate_HistoricalBar_ComputesValue()
|
||||
{
|
||||
var indicator = new RvIndicator { Period = 5, SmoothingPeriod = 10 };
|
||||
indicator.Initialize();
|
||||
|
||||
var now = DateTime.UtcNow;
|
||||
for (int i = 0; i < 30; i++)
|
||||
{
|
||||
double closePrice = 100 + i * 0.5 + Math.Sin(i * 0.3) * 2;
|
||||
indicator.HistoricalData.AddBar(now.AddMinutes(i), closePrice - 1, closePrice + 2, closePrice - 2, closePrice, 1000);
|
||||
|
||||
var args = new UpdateArgs(UpdateReason.HistoricalBar);
|
||||
indicator.ProcessUpdate(args);
|
||||
}
|
||||
|
||||
double val = indicator.LinesSeries[0].GetValue(0);
|
||||
Assert.True(double.IsFinite(val));
|
||||
Assert.True(val >= 0, "Volatility should be non-negative");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void RvIndicator_ProcessUpdate_NewBar_ComputesValue()
|
||||
{
|
||||
var indicator = new RvIndicator { Period = 5, SmoothingPeriod = 10 };
|
||||
indicator.Initialize();
|
||||
|
||||
var now = DateTime.UtcNow;
|
||||
for (int i = 0; i < 30; i++)
|
||||
{
|
||||
double closePrice = 100 + i * 0.3;
|
||||
indicator.HistoricalData.AddBar(now.AddMinutes(i), closePrice - 1, closePrice + 2, closePrice - 2, closePrice, 1000);
|
||||
}
|
||||
|
||||
indicator.ProcessUpdate(new UpdateArgs(UpdateReason.HistoricalBar));
|
||||
|
||||
indicator.HistoricalData.AddBar(now.AddMinutes(30), 115, 120, 110, 118, 1500);
|
||||
indicator.ProcessUpdate(new UpdateArgs(UpdateReason.NewBar));
|
||||
|
||||
Assert.Equal(2, indicator.LinesSeries[0].Count);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void RvIndicator_DifferentPeriods_Work()
|
||||
{
|
||||
int[] periods = { 3, 5, 10 };
|
||||
|
||||
foreach (var period in periods)
|
||||
{
|
||||
var indicator = new RvIndicator { Period = period, SmoothingPeriod = 10 };
|
||||
indicator.Initialize();
|
||||
|
||||
var now = DateTime.UtcNow;
|
||||
for (int i = 0; i < 50; i++)
|
||||
{
|
||||
double closePrice = 100 + i * 0.2 + Math.Sin(i * 0.5) * 3;
|
||||
indicator.HistoricalData.AddBar(now.AddMinutes(i), closePrice - 1, closePrice + 2, closePrice - 2, closePrice, 1000);
|
||||
indicator.ProcessUpdate(new UpdateArgs(UpdateReason.HistoricalBar));
|
||||
}
|
||||
|
||||
double val = indicator.LinesSeries[0].GetValue(0);
|
||||
Assert.True(double.IsFinite(val), $"Period {period} should produce finite value");
|
||||
Assert.True(val >= 0, $"Period {period} should produce non-negative value");
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void RvIndicator_Period_CanBeChanged()
|
||||
{
|
||||
var indicator = new RvIndicator();
|
||||
Assert.Equal(5, indicator.Period);
|
||||
|
||||
indicator.Period = 10;
|
||||
Assert.Equal(10, indicator.Period);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void RvIndicator_SmoothingPeriod_CanBeChanged()
|
||||
{
|
||||
var indicator = new RvIndicator();
|
||||
Assert.Equal(20, indicator.SmoothingPeriod);
|
||||
|
||||
indicator.SmoothingPeriod = 30;
|
||||
Assert.Equal(30, indicator.SmoothingPeriod);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void RvIndicator_Annualize_CanBeToggled()
|
||||
{
|
||||
var indicator = new RvIndicator();
|
||||
Assert.True(indicator.Annualize);
|
||||
|
||||
indicator.Annualize = false;
|
||||
Assert.False(indicator.Annualize);
|
||||
|
||||
indicator.Annualize = true;
|
||||
Assert.True(indicator.Annualize);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void RvIndicator_AnnualPeriods_CanBeChanged()
|
||||
{
|
||||
var indicator = new RvIndicator();
|
||||
Assert.Equal(252, indicator.AnnualPeriods);
|
||||
|
||||
indicator.AnnualPeriods = 365;
|
||||
Assert.Equal(365, indicator.AnnualPeriods);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void RvIndicator_ShowColdValues_CanBeToggled()
|
||||
{
|
||||
var indicator = new RvIndicator();
|
||||
Assert.True(indicator.ShowColdValues);
|
||||
|
||||
indicator.ShowColdValues = false;
|
||||
Assert.False(indicator.ShowColdValues);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void RvIndicator_SourceCodeLink_IsValid()
|
||||
{
|
||||
var indicator = new RvIndicator();
|
||||
Assert.Contains("github.com", indicator.SourceCodeLink, StringComparison.Ordinal);
|
||||
Assert.Contains("Rv.Quantower.cs", indicator.SourceCodeLink, StringComparison.Ordinal);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void RvIndicator_HighVolatility_ProducesHigherValue()
|
||||
{
|
||||
var indicator1 = new RvIndicator { Period = 5, SmoothingPeriod = 10, Annualize = false };
|
||||
var indicator2 = new RvIndicator { Period = 5, SmoothingPeriod = 10, Annualize = false };
|
||||
indicator1.Initialize();
|
||||
indicator2.Initialize();
|
||||
|
||||
var now = DateTime.UtcNow;
|
||||
|
||||
// Low volatility
|
||||
for (int i = 0; i < 30; i++)
|
||||
{
|
||||
double closePrice = 100 + i * 0.01;
|
||||
indicator1.HistoricalData.AddBar(now.AddMinutes(i), closePrice - 0.5, closePrice + 0.5, closePrice - 0.5, closePrice, 1000);
|
||||
indicator1.ProcessUpdate(new UpdateArgs(UpdateReason.HistoricalBar));
|
||||
}
|
||||
|
||||
// High volatility
|
||||
for (int i = 0; i < 30; i++)
|
||||
{
|
||||
double closePrice = 100 + Math.Sin(i * 0.5) * 10;
|
||||
indicator2.HistoricalData.AddBar(now.AddMinutes(i), closePrice - 2, closePrice + 2, closePrice - 2, closePrice, 1000);
|
||||
indicator2.ProcessUpdate(new UpdateArgs(UpdateReason.HistoricalBar));
|
||||
}
|
||||
|
||||
double lowVol = indicator1.LinesSeries[0].GetValue(0);
|
||||
double highVol = indicator2.LinesSeries[0].GetValue(0);
|
||||
|
||||
Assert.True(double.IsFinite(lowVol));
|
||||
Assert.True(double.IsFinite(highVol));
|
||||
Assert.True(highVol > lowVol, "Higher volatility closes should produce higher RV value");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void RvIndicator_AnnualizedValue_IsScaled()
|
||||
{
|
||||
var indicatorRaw = new RvIndicator { Period = 5, SmoothingPeriod = 10, Annualize = false };
|
||||
var indicatorAnn = new RvIndicator { Period = 5, SmoothingPeriod = 10, Annualize = true, AnnualPeriods = 252 };
|
||||
indicatorRaw.Initialize();
|
||||
indicatorAnn.Initialize();
|
||||
|
||||
var now = DateTime.UtcNow;
|
||||
|
||||
for (int i = 0; i < 30; i++)
|
||||
{
|
||||
double closePrice = 100 + i * 0.5 + Math.Sin(i * 0.3) * 2;
|
||||
indicatorRaw.HistoricalData.AddBar(now.AddMinutes(i), closePrice - 1, closePrice + 2, closePrice - 2, closePrice, 1000);
|
||||
indicatorRaw.ProcessUpdate(new UpdateArgs(UpdateReason.HistoricalBar));
|
||||
|
||||
indicatorAnn.HistoricalData.AddBar(now.AddMinutes(i), closePrice - 1, closePrice + 2, closePrice - 2, closePrice, 1000);
|
||||
indicatorAnn.ProcessUpdate(new UpdateArgs(UpdateReason.HistoricalBar));
|
||||
}
|
||||
|
||||
double rawValue = indicatorRaw.LinesSeries[0].GetValue(0);
|
||||
double annValue = indicatorAnn.LinesSeries[0].GetValue(0);
|
||||
|
||||
Assert.True(double.IsFinite(rawValue));
|
||||
Assert.True(double.IsFinite(annValue));
|
||||
|
||||
double expectedRatio = Math.Sqrt(252);
|
||||
double actualRatio = annValue / rawValue;
|
||||
|
||||
Assert.True(Math.Abs(actualRatio - expectedRatio) < 0.01,
|
||||
$"Annualized value should be ~{expectedRatio:F2}× raw, got {actualRatio:F2}×");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void RvIndicator_OnlyUsesClose_IgnoresOpenHighLow()
|
||||
{
|
||||
var indicator1 = new RvIndicator { Period = 5, SmoothingPeriod = 10, Annualize = false };
|
||||
var indicator2 = new RvIndicator { Period = 5, SmoothingPeriod = 10, Annualize = false };
|
||||
indicator1.Initialize();
|
||||
indicator2.Initialize();
|
||||
|
||||
var now = DateTime.UtcNow;
|
||||
|
||||
for (int i = 0; i < 30; i++)
|
||||
{
|
||||
double closePrice = 100 + i * 0.5;
|
||||
// Narrow range
|
||||
indicator1.HistoricalData.AddBar(now.AddMinutes(i), closePrice, closePrice + 1, closePrice - 1, closePrice, 1000);
|
||||
indicator1.ProcessUpdate(new UpdateArgs(UpdateReason.HistoricalBar));
|
||||
|
||||
// Wide range (same close)
|
||||
indicator2.HistoricalData.AddBar(now.AddMinutes(i), closePrice - 5, closePrice + 10, closePrice - 10, closePrice, 1000);
|
||||
indicator2.ProcessUpdate(new UpdateArgs(UpdateReason.HistoricalBar));
|
||||
}
|
||||
|
||||
double val1 = indicator1.LinesSeries[0].GetValue(0);
|
||||
double val2 = indicator2.LinesSeries[0].GetValue(0);
|
||||
|
||||
Assert.True(double.IsFinite(val1));
|
||||
Assert.True(double.IsFinite(val2));
|
||||
Assert.Equal(val1, val2, 10);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void RvIndicator_ConstantPrice_ProducesZeroVolatility()
|
||||
{
|
||||
var indicator = new RvIndicator { Period = 5, SmoothingPeriod = 10, Annualize = false };
|
||||
indicator.Initialize();
|
||||
|
||||
var now = DateTime.UtcNow;
|
||||
|
||||
for (int i = 0; i < 30; i++)
|
||||
{
|
||||
indicator.HistoricalData.AddBar(now.AddMinutes(i), 100, 105, 95, 100, 1000);
|
||||
indicator.ProcessUpdate(new UpdateArgs(UpdateReason.HistoricalBar));
|
||||
}
|
||||
|
||||
double val = indicator.LinesSeries[0].GetValue(0);
|
||||
|
||||
Assert.True(double.IsFinite(val));
|
||||
Assert.True(val < 0.001, "Constant close price should produce near-zero volatility");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void RvIndicator_VaryingReturns_ProducesNonZeroVolatility()
|
||||
{
|
||||
var indicator = new RvIndicator { Period = 5, SmoothingPeriod = 10, Annualize = false };
|
||||
indicator.Initialize();
|
||||
|
||||
var now = DateTime.UtcNow;
|
||||
|
||||
for (int i = 0; i < 30; i++)
|
||||
{
|
||||
double rate = (i % 2 == 0) ? 1.02 : 1.005;
|
||||
double closePrice = 100 * Math.Pow(rate, i / 2 + 1) * (i % 2 == 0 ? 1.0 : rate);
|
||||
indicator.HistoricalData.AddBar(now.AddMinutes(i), closePrice - 1, closePrice + 1, closePrice - 1, closePrice, 1000);
|
||||
indicator.ProcessUpdate(new UpdateArgs(UpdateReason.HistoricalBar));
|
||||
}
|
||||
|
||||
double val = indicator.LinesSeries[0].GetValue(0);
|
||||
|
||||
Assert.True(double.IsFinite(val));
|
||||
Assert.True(val > 0, "Varying returns should produce non-zero volatility");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void RvIndicator_DifferentSmoothingPeriods_ProduceDifferentResults()
|
||||
{
|
||||
var indicator1 = new RvIndicator { Period = 5, SmoothingPeriod = 5, Annualize = false };
|
||||
var indicator2 = new RvIndicator { Period = 5, SmoothingPeriod = 20, Annualize = false };
|
||||
indicator1.Initialize();
|
||||
indicator2.Initialize();
|
||||
|
||||
var now = DateTime.UtcNow;
|
||||
|
||||
for (int i = 0; i < 50; i++)
|
||||
{
|
||||
double closePrice = 100 + Math.Sin(i * 0.3) * 5;
|
||||
indicator1.HistoricalData.AddBar(now.AddMinutes(i), closePrice - 1, closePrice + 1, closePrice - 1, closePrice, 1000);
|
||||
indicator1.ProcessUpdate(new UpdateArgs(UpdateReason.HistoricalBar));
|
||||
|
||||
indicator2.HistoricalData.AddBar(now.AddMinutes(i), closePrice - 1, closePrice + 1, closePrice - 1, closePrice, 1000);
|
||||
indicator2.ProcessUpdate(new UpdateArgs(UpdateReason.HistoricalBar));
|
||||
}
|
||||
|
||||
double val1 = indicator1.LinesSeries[0].GetValue(0);
|
||||
double val2 = indicator2.LinesSeries[0].GetValue(0);
|
||||
|
||||
Assert.True(double.IsFinite(val1));
|
||||
Assert.True(double.IsFinite(val2));
|
||||
// Different smoothing periods should produce different results
|
||||
Assert.NotEqual(val1, val2);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
using System.Drawing;
|
||||
using System.Runtime.CompilerServices;
|
||||
using TradingPlatform.BusinessLayer;
|
||||
|
||||
namespace QuanTAlib;
|
||||
|
||||
[SkipLocalsInit]
|
||||
public sealed class RvIndicator : Indicator, IWatchlistIndicator
|
||||
{
|
||||
[InputParameter("Period", sortIndex: 1, 2, 1000, 1, 0)]
|
||||
public int Period { get; set; } = 5;
|
||||
|
||||
[InputParameter("Smoothing Period", sortIndex: 2, 1, 1000, 1, 0)]
|
||||
public int SmoothingPeriod { get; set; } = 20;
|
||||
|
||||
[InputParameter("Annualize", sortIndex: 3)]
|
||||
public bool Annualize { get; set; } = true;
|
||||
|
||||
[InputParameter("Annual Periods", sortIndex: 4, 1, 365, 1, 0)]
|
||||
public int AnnualPeriods { get; set; } = 252;
|
||||
|
||||
[InputParameter("Show cold values", sortIndex: 21)]
|
||||
public bool ShowColdValues { get; set; } = true;
|
||||
|
||||
private Rv _rv = null!;
|
||||
private readonly LineSeries _series;
|
||||
|
||||
public static int MinHistoryDepths => 0;
|
||||
int IWatchlistIndicator.MinHistoryDepths => MinHistoryDepths;
|
||||
|
||||
public override string ShortName => $"RV {Period},{SmoothingPeriod}";
|
||||
public override string SourceCodeLink => "https://github.com/mihakralj/QuanTAlib/blob/main/lib/volatility/rv/Rv.Quantower.cs";
|
||||
|
||||
public RvIndicator()
|
||||
{
|
||||
OnBackGround = true;
|
||||
SeparateWindow = true;
|
||||
Name = "RV - Realized Volatility";
|
||||
Description = "Realized Volatility measures price volatility using the sum of squared logarithmic returns, smoothed with SMA";
|
||||
|
||||
_series = new LineSeries(name: "RV", color: IndicatorExtensions.Volatility, width: 2, style: LineStyle.Solid);
|
||||
AddLineSeries(_series);
|
||||
}
|
||||
|
||||
protected override void OnInit()
|
||||
{
|
||||
_rv = new Rv(Period, SmoothingPeriod, Annualize, AnnualPeriods);
|
||||
base.OnInit();
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
protected override void OnUpdate(UpdateArgs args)
|
||||
{
|
||||
TBar bar = this.GetInputBar(args);
|
||||
TValue result = _rv.Update(bar, isNew: args.IsNewBar());
|
||||
_series.SetValue(result.Value, _rv.IsHot, ShowColdValues);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,715 @@
|
||||
namespace QuanTAlib.Tests;
|
||||
using Xunit;
|
||||
|
||||
public class RvTests
|
||||
{
|
||||
private const double Tolerance = 1e-9;
|
||||
|
||||
private static TBarSeries GenerateTestData(int count = 100)
|
||||
{
|
||||
var gbm = new GBM(seed: 42);
|
||||
return gbm.Fetch(count, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1));
|
||||
}
|
||||
|
||||
private static TSeries GeneratePriceSeries(int count = 100)
|
||||
{
|
||||
var gbm = new GBM(seed: 42);
|
||||
var bars = gbm.Fetch(count, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1));
|
||||
var t = new List<long>(count);
|
||||
var v = new List<double>(count);
|
||||
for (int i = 0; i < count; i++)
|
||||
{
|
||||
t.Add(bars[i].Time);
|
||||
v.Add(bars[i].Close);
|
||||
}
|
||||
return new TSeries(t, v);
|
||||
}
|
||||
|
||||
#region Constructor Tests
|
||||
|
||||
[Fact]
|
||||
public void Constructor_DefaultParameters_SetsCorrectValues()
|
||||
{
|
||||
var rv = new Rv();
|
||||
Assert.Equal(5, rv.Period);
|
||||
Assert.Equal(20, rv.SmoothingPeriod);
|
||||
Assert.True(rv.Annualize);
|
||||
Assert.Equal(252, rv.AnnualPeriods);
|
||||
Assert.Equal("Rv(5,20)", rv.Name);
|
||||
Assert.Equal(25, rv.WarmupPeriod); // period + smoothingPeriod
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Constructor_CustomParameters_SetsCorrectValues()
|
||||
{
|
||||
var rv = new Rv(period: 10, smoothingPeriod: 30, annualize: false, annualPeriods: 365);
|
||||
Assert.Equal(10, rv.Period);
|
||||
Assert.Equal(30, rv.SmoothingPeriod);
|
||||
Assert.False(rv.Annualize);
|
||||
Assert.Equal(365, rv.AnnualPeriods);
|
||||
Assert.Equal("Rv(10,30)", rv.Name);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Constructor_ZeroPeriod_ThrowsArgumentException()
|
||||
{
|
||||
var ex = Assert.Throws<ArgumentException>(() => new Rv(period: 0));
|
||||
Assert.Equal("period", ex.ParamName);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Constructor_NegativePeriod_ThrowsArgumentException()
|
||||
{
|
||||
var ex = Assert.Throws<ArgumentException>(() => new Rv(period: -1));
|
||||
Assert.Equal("period", ex.ParamName);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Constructor_ZeroSmoothingPeriod_ThrowsArgumentException()
|
||||
{
|
||||
var ex = Assert.Throws<ArgumentException>(() => new Rv(period: 5, smoothingPeriod: 0));
|
||||
Assert.Equal("smoothingPeriod", ex.ParamName);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Constructor_NegativeSmoothingPeriod_ThrowsArgumentException()
|
||||
{
|
||||
var ex = Assert.Throws<ArgumentException>(() => new Rv(period: 5, smoothingPeriod: -1));
|
||||
Assert.Equal("smoothingPeriod", ex.ParamName);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Constructor_ZeroAnnualPeriodsWhenAnnualizing_ThrowsArgumentException()
|
||||
{
|
||||
var ex = Assert.Throws<ArgumentException>(() => new Rv(period: 5, smoothingPeriod: 20, annualize: true, annualPeriods: 0));
|
||||
Assert.Equal("annualPeriods", ex.ParamName);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Constructor_ZeroAnnualPeriodsWhenNotAnnualizing_DoesNotThrow()
|
||||
{
|
||||
var rv = new Rv(period: 5, smoothingPeriod: 20, annualize: false, annualPeriods: 0);
|
||||
Assert.Equal(0, rv.AnnualPeriods);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Basic Calculation Tests
|
||||
|
||||
[Fact]
|
||||
public void Update_SinglePrice_ReturnsZero()
|
||||
{
|
||||
var rv = new Rv(period: 5, smoothingPeriod: 10);
|
||||
var price = new TValue(DateTime.UtcNow, 100.0);
|
||||
var result = rv.Update(price);
|
||||
|
||||
// First price cannot produce a return, so volatility is 0
|
||||
Assert.Equal(0.0, result.Value);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Update_TwoPrices_ReturnsPositiveVolatility()
|
||||
{
|
||||
var rv = new Rv(period: 5, smoothingPeriod: 10);
|
||||
rv.Update(new TValue(DateTime.UtcNow, 100.0));
|
||||
var result = rv.Update(new TValue(DateTime.UtcNow.AddMinutes(1), 101.0));
|
||||
|
||||
// Second price gives first squared return, so volatility should be positive
|
||||
Assert.True(result.Value >= 0);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Update_MultiplePrices_ReturnsPositiveVolatility()
|
||||
{
|
||||
var rv = new Rv(period: 5, smoothingPeriod: 10);
|
||||
var prices = GeneratePriceSeries(30);
|
||||
|
||||
double lastValue = 0;
|
||||
for (int i = 0; i < prices.Count; i++)
|
||||
{
|
||||
lastValue = rv.Update(prices[i]).Value;
|
||||
}
|
||||
|
||||
Assert.True(lastValue > 0, "RV should return positive volatility after warmup");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Update_ReturnsLastValue()
|
||||
{
|
||||
var rv = new Rv(period: 5, smoothingPeriod: 10);
|
||||
var price = new TValue(DateTime.UtcNow, 100.0);
|
||||
var result = rv.Update(price);
|
||||
|
||||
Assert.Equal(result.Value, rv.Last.Value, Tolerance);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Update_WithoutAnnualization_ReturnsSmallerValues()
|
||||
{
|
||||
var rvAnnual = new Rv(period: 5, smoothingPeriod: 10, annualize: true, annualPeriods: 252);
|
||||
var rvNoAnnual = new Rv(period: 5, smoothingPeriod: 10, annualize: false);
|
||||
var prices = GeneratePriceSeries(30);
|
||||
|
||||
double lastAnnual = 0;
|
||||
double lastNoAnnual = 0;
|
||||
for (int i = 0; i < prices.Count; i++)
|
||||
{
|
||||
lastAnnual = rvAnnual.Update(prices[i]).Value;
|
||||
lastNoAnnual = rvNoAnnual.Update(prices[i]).Value;
|
||||
}
|
||||
|
||||
// Annualized values should be larger by factor of sqrt(252)
|
||||
Assert.True(lastAnnual > lastNoAnnual, "Annualized values should be larger");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Update_AnnualizationFactor_Correct()
|
||||
{
|
||||
var rvAnnual = new Rv(period: 5, smoothingPeriod: 10, annualize: true, annualPeriods: 252);
|
||||
var rvNoAnnual = new Rv(period: 5, smoothingPeriod: 10, annualize: false);
|
||||
var prices = GeneratePriceSeries(50);
|
||||
|
||||
for (int i = 0; i < prices.Count; i++)
|
||||
{
|
||||
rvAnnual.Update(prices[i]);
|
||||
rvNoAnnual.Update(prices[i]);
|
||||
}
|
||||
|
||||
double factor = rvAnnual.Last.Value / rvNoAnnual.Last.Value;
|
||||
double expectedFactor = Math.Sqrt(252);
|
||||
|
||||
Assert.Equal(expectedFactor, factor, 1e-6);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region State Management Tests
|
||||
|
||||
[Fact]
|
||||
public void Update_IsNewTrue_AdvancesState()
|
||||
{
|
||||
var rv = new Rv(period: 3, smoothingPeriod: 5);
|
||||
var prices = GeneratePriceSeries(10);
|
||||
|
||||
for (int i = 0; i < 5; i++)
|
||||
{
|
||||
rv.Update(prices[i], isNew: true);
|
||||
}
|
||||
var result1 = rv.Last.Value;
|
||||
|
||||
rv.Update(prices[5], isNew: true);
|
||||
var result2 = rv.Last.Value;
|
||||
|
||||
Assert.True(result1 >= 0, "First result should be non-negative");
|
||||
Assert.True(result2 >= 0, "Second result should be non-negative");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Update_IsNewFalse_UpdatesCurrentBar()
|
||||
{
|
||||
var rv = new Rv(period: 3, smoothingPeriod: 5);
|
||||
var prices = GeneratePriceSeries(10);
|
||||
|
||||
for (int i = 0; i < 5; i++)
|
||||
{
|
||||
rv.Update(prices[i], isNew: true);
|
||||
}
|
||||
|
||||
rv.Update(prices[5], isNew: true);
|
||||
var firstValue = rv.Last.Value;
|
||||
|
||||
var updatedPrice = new TValue(prices[5].Time, prices[5].Value * 1.05);
|
||||
rv.Update(updatedPrice, isNew: false);
|
||||
var updatedValue = rv.Last.Value;
|
||||
|
||||
Assert.NotEqual(firstValue, updatedValue);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Update_IterativeCorrections_RestoresState()
|
||||
{
|
||||
var rv = new Rv(period: 3, smoothingPeriod: 5);
|
||||
var prices = GeneratePriceSeries(15);
|
||||
|
||||
for (int i = 0; i < 5; i++)
|
||||
{
|
||||
rv.Update(prices[i], isNew: true);
|
||||
}
|
||||
|
||||
rv.Update(prices[5], isNew: true);
|
||||
rv.Update(prices[5], isNew: false);
|
||||
rv.Update(prices[5], isNew: false);
|
||||
rv.Update(prices[5], isNew: false);
|
||||
|
||||
rv.Update(prices[6], isNew: true);
|
||||
|
||||
var rv2 = new Rv(period: 3, smoothingPeriod: 5);
|
||||
for (int i = 0; i < 7; i++)
|
||||
{
|
||||
rv2.Update(prices[i], isNew: true);
|
||||
}
|
||||
|
||||
Assert.Equal(rv.Last.Value, rv2.Last.Value, Tolerance);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region IsHot and Warmup Tests
|
||||
|
||||
[Fact]
|
||||
public void IsHot_BeforeWarmup_ReturnsFalse()
|
||||
{
|
||||
var rv = new Rv(period: 5, smoothingPeriod: 10);
|
||||
var prices = GeneratePriceSeries(10);
|
||||
|
||||
for (int i = 0; i < prices.Count; i++)
|
||||
{
|
||||
rv.Update(prices[i]);
|
||||
}
|
||||
|
||||
Assert.False(rv.IsHot);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void IsHot_AfterWarmup_ReturnsTrue()
|
||||
{
|
||||
var rv = new Rv(period: 5, smoothingPeriod: 10);
|
||||
var prices = GeneratePriceSeries(20);
|
||||
|
||||
for (int i = 0; i < prices.Count; i++)
|
||||
{
|
||||
rv.Update(prices[i]);
|
||||
}
|
||||
|
||||
Assert.True(rv.IsHot);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Reset Tests
|
||||
|
||||
[Fact]
|
||||
public void Reset_ClearsState()
|
||||
{
|
||||
var rv = new Rv(period: 5, smoothingPeriod: 10);
|
||||
var prices = GeneratePriceSeries(20);
|
||||
|
||||
for (int i = 0; i < prices.Count; i++)
|
||||
{
|
||||
rv.Update(prices[i]);
|
||||
}
|
||||
|
||||
rv.Reset();
|
||||
|
||||
Assert.False(rv.IsHot);
|
||||
Assert.Equal(0, rv.Last.Value);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Reset_AllowsReprocessing()
|
||||
{
|
||||
var rv = new Rv(period: 5, smoothingPeriod: 10);
|
||||
var prices = GeneratePriceSeries(20);
|
||||
|
||||
for (int i = 0; i < prices.Count; i++)
|
||||
{
|
||||
rv.Update(prices[i]);
|
||||
}
|
||||
var firstResult = rv.Last.Value;
|
||||
|
||||
rv.Reset();
|
||||
for (int i = 0; i < prices.Count; i++)
|
||||
{
|
||||
rv.Update(prices[i]);
|
||||
}
|
||||
var secondResult = rv.Last.Value;
|
||||
|
||||
Assert.Equal(firstResult, secondResult, Tolerance);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Robustness Tests
|
||||
|
||||
[Fact]
|
||||
public void Update_WithNaNValues_UsesLastValidValue()
|
||||
{
|
||||
var rv = new Rv(period: 5, smoothingPeriod: 10);
|
||||
var prices = GeneratePriceSeries(20);
|
||||
|
||||
for (int i = 0; i < prices.Count; i++)
|
||||
{
|
||||
rv.Update(prices[i]);
|
||||
}
|
||||
var valueBeforeInvalid = rv.Last.Value;
|
||||
|
||||
var nanPrice = new TValue(DateTime.UtcNow, double.NaN);
|
||||
var result = rv.Update(nanPrice);
|
||||
|
||||
Assert.True(double.IsFinite(result.Value), "Result should be finite when using last valid value");
|
||||
Assert.Equal(valueBeforeInvalid, result.Value, Tolerance);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Update_WithInfinityValues_UsesLastValidValue()
|
||||
{
|
||||
var rv = new Rv(period: 5, smoothingPeriod: 10);
|
||||
var prices = GeneratePriceSeries(20);
|
||||
|
||||
for (int i = 0; i < prices.Count; i++)
|
||||
{
|
||||
rv.Update(prices[i]);
|
||||
}
|
||||
var valueBeforeInvalid = rv.Last.Value;
|
||||
|
||||
var infPrice = new TValue(DateTime.UtcNow, double.PositiveInfinity);
|
||||
var result = rv.Update(infPrice);
|
||||
|
||||
Assert.True(double.IsFinite(result.Value), "Result should be finite when using last valid value");
|
||||
Assert.Equal(valueBeforeInvalid, result.Value, Tolerance);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Update_WithZeroPrice_UsesLastValidValue()
|
||||
{
|
||||
var rv = new Rv(period: 5, smoothingPeriod: 10);
|
||||
var prices = GeneratePriceSeries(20);
|
||||
|
||||
for (int i = 0; i < prices.Count; i++)
|
||||
{
|
||||
rv.Update(prices[i]);
|
||||
}
|
||||
var valueBeforeInvalid = rv.Last.Value;
|
||||
|
||||
var zeroPrice = new TValue(DateTime.UtcNow, 0.0);
|
||||
var result = rv.Update(zeroPrice);
|
||||
|
||||
Assert.True(double.IsFinite(result.Value), "Result should be finite when using last valid value");
|
||||
Assert.Equal(valueBeforeInvalid, result.Value, Tolerance);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Update_WithNegativePrice_UsesLastValidValue()
|
||||
{
|
||||
var rv = new Rv(period: 5, smoothingPeriod: 10);
|
||||
var prices = GeneratePriceSeries(20);
|
||||
|
||||
for (int i = 0; i < prices.Count; i++)
|
||||
{
|
||||
rv.Update(prices[i]);
|
||||
}
|
||||
var valueBeforeInvalid = rv.Last.Value;
|
||||
|
||||
var negPrice = new TValue(DateTime.UtcNow, -100.0);
|
||||
var result = rv.Update(negPrice);
|
||||
|
||||
Assert.True(double.IsFinite(result.Value), "Result should be finite when using last valid value");
|
||||
Assert.Equal(valueBeforeInvalid, result.Value, Tolerance);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Batch and Series Tests
|
||||
|
||||
[Fact]
|
||||
public void Batch_MatchesStreamingResults()
|
||||
{
|
||||
const int dataCount = 100;
|
||||
var prices = GeneratePriceSeries(dataCount);
|
||||
|
||||
var rvStreaming = new Rv(period: 5, smoothingPeriod: 10);
|
||||
var streamingResults = new double[dataCount];
|
||||
for (int i = 0; i < dataCount; i++)
|
||||
{
|
||||
streamingResults[i] = rvStreaming.Update(prices[i]).Value;
|
||||
}
|
||||
|
||||
var batchResults = new double[dataCount];
|
||||
Rv.Batch(prices.Values, batchResults, period: 5, smoothingPeriod: 10);
|
||||
|
||||
for (int i = 50; i < dataCount; i++)
|
||||
{
|
||||
Assert.Equal(streamingResults[i], batchResults[i], Tolerance);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Calculate_TSeries_ReturnsCorrectLength()
|
||||
{
|
||||
const int dataCount = 50;
|
||||
var priceSeries = GeneratePriceSeries(dataCount);
|
||||
|
||||
var result = Rv.Calculate(priceSeries, period: 5, smoothingPeriod: 10);
|
||||
|
||||
Assert.Equal(dataCount, result.Count);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Update_TSeries_MatchesStreamingResults()
|
||||
{
|
||||
const int dataCount = 50;
|
||||
var priceSeries = GeneratePriceSeries(dataCount);
|
||||
|
||||
var rvSeries = new Rv(period: 5, smoothingPeriod: 10);
|
||||
var seriesResult = rvSeries.Update(priceSeries);
|
||||
|
||||
var rvStreaming = new Rv(period: 5, smoothingPeriod: 10);
|
||||
var streamingResults = new double[dataCount];
|
||||
for (int i = 0; i < dataCount; i++)
|
||||
{
|
||||
streamingResults[i] = rvStreaming.Update(priceSeries[i]).Value;
|
||||
}
|
||||
|
||||
for (int i = 20; i < dataCount; i++)
|
||||
{
|
||||
Assert.Equal(streamingResults[i], seriesResult.Values[i], Tolerance);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Batch_EmptyInput_DoesNotThrow()
|
||||
{
|
||||
var prices = Array.Empty<double>();
|
||||
var output = Array.Empty<double>();
|
||||
|
||||
Rv.Batch(prices, output, period: 5, smoothingPeriod: 10);
|
||||
Assert.Empty(output);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Batch_OutputTooShort_ThrowsArgumentException()
|
||||
{
|
||||
var prices = new double[10];
|
||||
var output = new double[5];
|
||||
|
||||
var ex = Assert.Throws<ArgumentException>(() =>
|
||||
Rv.Batch(prices, output, period: 5, smoothingPeriod: 10));
|
||||
Assert.Equal("output", ex.ParamName);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Batch_InvalidPeriod_ThrowsArgumentException()
|
||||
{
|
||||
var prices = new double[10];
|
||||
var output = new double[10];
|
||||
|
||||
var ex = Assert.Throws<ArgumentException>(() =>
|
||||
Rv.Batch(prices, output, period: 0, smoothingPeriod: 10));
|
||||
Assert.Equal("period", ex.ParamName);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Batch_InvalidSmoothingPeriod_ThrowsArgumentException()
|
||||
{
|
||||
var prices = new double[10];
|
||||
var output = new double[10];
|
||||
|
||||
var ex = Assert.Throws<ArgumentException>(() =>
|
||||
Rv.Batch(prices, output, period: 5, smoothingPeriod: 0));
|
||||
Assert.Equal("smoothingPeriod", ex.ParamName);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Event Publishing Tests
|
||||
|
||||
[Fact]
|
||||
public void Update_PublishesEvent()
|
||||
{
|
||||
var rv = new Rv(period: 5, smoothingPeriod: 10);
|
||||
bool eventFired = false;
|
||||
rv.Pub += (object? sender, in TValueEventArgs args) => eventFired = true;
|
||||
|
||||
var price = new TValue(DateTime.UtcNow, 100.0);
|
||||
rv.Update(price);
|
||||
|
||||
Assert.True(eventFired);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ChainedIndicator_ReceivesValues()
|
||||
{
|
||||
var source = new Rv(period: 5, smoothingPeriod: 10);
|
||||
var downstream = new Sma(source, period: 3);
|
||||
|
||||
var prices = GeneratePriceSeries(30);
|
||||
for (int i = 0; i < prices.Count; i++)
|
||||
{
|
||||
source.Update(prices[i]);
|
||||
}
|
||||
|
||||
Assert.True(downstream.Last.Value > 0, "Downstream indicator should receive values");
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region TBar Update Tests
|
||||
|
||||
[Fact]
|
||||
public void Update_TBar_UsesClosePrice()
|
||||
{
|
||||
var rv1 = new Rv(period: 5, smoothingPeriod: 10);
|
||||
var rv2 = new Rv(period: 5, smoothingPeriod: 10);
|
||||
|
||||
var bar = new TBar(DateTime.UtcNow, 100.0, 105.0, 98.0, 102.0, 1000);
|
||||
rv1.Update(bar);
|
||||
|
||||
var tvalue = new TValue(bar.Time, bar.Close);
|
||||
rv2.Update(tvalue);
|
||||
|
||||
Assert.Equal(rv1.Last.Value, rv2.Last.Value, Tolerance);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Update_TBarSeries_ReturnsCorrectLength()
|
||||
{
|
||||
const int dataCount = 50;
|
||||
var barSeries = GenerateTestData(dataCount);
|
||||
|
||||
var rv = new Rv(period: 5, smoothingPeriod: 10);
|
||||
var result = rv.Update(barSeries);
|
||||
|
||||
Assert.Equal(dataCount, result.Count);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Additional Tests
|
||||
|
||||
[Fact]
|
||||
public void LargeDataset_Performance()
|
||||
{
|
||||
var rv = new Rv(period: 5, smoothingPeriod: 20);
|
||||
var prices = GeneratePriceSeries(5000);
|
||||
|
||||
for (int i = 0; i < prices.Count; i++)
|
||||
{
|
||||
var result = rv.Update(prices[i]);
|
||||
Assert.True(double.IsFinite(result.Value));
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void DifferentParameters_ProduceDistinctValues()
|
||||
{
|
||||
var prices = GeneratePriceSeries(50);
|
||||
|
||||
var rv1 = new Rv(period: 5, smoothingPeriod: 10);
|
||||
var rv2 = new Rv(period: 10, smoothingPeriod: 20);
|
||||
var rv3 = new Rv(period: 5, smoothingPeriod: 10, annualize: false);
|
||||
|
||||
for (int i = 0; i < prices.Count; i++)
|
||||
{
|
||||
rv1.Update(prices[i]);
|
||||
rv2.Update(prices[i]);
|
||||
rv3.Update(prices[i]);
|
||||
}
|
||||
|
||||
Assert.True(double.IsFinite(rv1.Last.Value));
|
||||
Assert.True(double.IsFinite(rv2.Last.Value));
|
||||
Assert.True(double.IsFinite(rv3.Last.Value));
|
||||
Assert.NotEqual(rv1.Last.Value, rv2.Last.Value);
|
||||
Assert.NotEqual(rv1.Last.Value, rv3.Last.Value);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void StaticCalculate_TSeries_Works()
|
||||
{
|
||||
var prices = GeneratePriceSeries(100);
|
||||
|
||||
var result = Rv.Calculate(prices, period: 5, smoothingPeriod: 14);
|
||||
|
||||
Assert.Equal(100, result.Count);
|
||||
Assert.True(double.IsFinite(result[result.Count - 1].Value));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void StaticCalculate_TBarSeries_Works()
|
||||
{
|
||||
var bars = GenerateTestData(100);
|
||||
|
||||
var result = Rv.Calculate(bars, period: 5, smoothingPeriod: 14);
|
||||
|
||||
Assert.Equal(100, result.Count);
|
||||
Assert.True(double.IsFinite(result[result.Count - 1].Value));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void StaticCalculate_ValidatesInput()
|
||||
{
|
||||
var prices = GeneratePriceSeries(10);
|
||||
|
||||
Assert.Throws<ArgumentException>(() => Rv.Calculate(prices, period: 0));
|
||||
Assert.Throws<ArgumentException>(() => Rv.Calculate(prices, period: -1));
|
||||
Assert.Throws<ArgumentException>(() => Rv.Calculate(prices, period: 5, smoothingPeriod: 0));
|
||||
Assert.Throws<ArgumentException>(() => Rv.Calculate(prices, period: 5, smoothingPeriod: 10, annualize: true, annualPeriods: 0));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Prime_Works()
|
||||
{
|
||||
var rv = new Rv(period: 3, smoothingPeriod: 5);
|
||||
var values = new double[] { 100.0, 101.0, 99.5, 102.0, 100.5, 103.0, 101.0, 104.0, 102.0, 105.0 };
|
||||
|
||||
rv.Prime(values);
|
||||
|
||||
Assert.True(rv.IsHot);
|
||||
Assert.True(double.IsFinite(rv.Last.Value));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void KnownValue_ManualCalculation()
|
||||
{
|
||||
// Test with known values: prices 100, 101, 102, 103 (3 returns)
|
||||
// Log returns: ln(101/100), ln(102/101), ln(103/102)
|
||||
// ≈ 0.00995, 0.00985, 0.00975
|
||||
// Squared returns sum, then sqrt, then SMA
|
||||
|
||||
var rv = new Rv(period: 3, smoothingPeriod: 2, annualize: false);
|
||||
var prices = new double[] { 100.0, 101.0, 102.0, 103.0, 104.0 };
|
||||
|
||||
for (int i = 0; i < prices.Length; i++)
|
||||
{
|
||||
rv.Update(new TValue(DateTime.UtcNow.AddMinutes(i), prices[i]));
|
||||
}
|
||||
|
||||
// The result should be positive and finite
|
||||
Assert.True(rv.Last.Value > 0);
|
||||
Assert.True(double.IsFinite(rv.Last.Value));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SmoothingEffect_ReducesNoise()
|
||||
{
|
||||
// Compare RV with different smoothing periods
|
||||
var prices = GeneratePriceSeries(100);
|
||||
|
||||
var rvShortSmooth = new Rv(period: 5, smoothingPeriod: 3, annualize: false);
|
||||
var rvLongSmooth = new Rv(period: 5, smoothingPeriod: 20, annualize: false);
|
||||
|
||||
var shortSmoothValues = new List<double>();
|
||||
var longSmoothValues = new List<double>();
|
||||
|
||||
for (int i = 0; i < prices.Count; i++)
|
||||
{
|
||||
shortSmoothValues.Add(rvShortSmooth.Update(prices[i]).Value);
|
||||
longSmoothValues.Add(rvLongSmooth.Update(prices[i]).Value);
|
||||
}
|
||||
|
||||
// Calculate variance of last 50 values
|
||||
double VarianceOfLast50(List<double> vals)
|
||||
{
|
||||
var last50 = vals.Skip(vals.Count - 50).ToList();
|
||||
double mean = last50.Average();
|
||||
return last50.Sum(v => (v - mean) * (v - mean)) / last50.Count;
|
||||
}
|
||||
|
||||
double shortVariance = VarianceOfLast50(shortSmoothValues);
|
||||
double longVariance = VarianceOfLast50(longSmoothValues);
|
||||
|
||||
// Longer smoothing should have lower variance (smoother)
|
||||
Assert.True(longVariance < shortVariance, "Longer smoothing should produce smoother output");
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
@@ -0,0 +1,561 @@
|
||||
namespace QuanTAlib.Test;
|
||||
|
||||
using Xunit;
|
||||
|
||||
/// <summary>
|
||||
/// Validation tests for RV (Realized Volatility).
|
||||
/// RV calculates volatility from squared log returns, smoothed with SMA.
|
||||
/// Formula: RV = SMA(√(Σr²)) × annualizationFactor
|
||||
/// </summary>
|
||||
public class RvValidationTests
|
||||
{
|
||||
private static TBarSeries GenerateTestData(int count = 100)
|
||||
{
|
||||
var gbm = new GBM(seed: 42);
|
||||
return gbm.Fetch(count, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1));
|
||||
}
|
||||
|
||||
private static TSeries GeneratePriceSeries(int count = 100)
|
||||
{
|
||||
var gbm = new GBM(seed: 42);
|
||||
var bars = gbm.Fetch(count, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1));
|
||||
var t = new List<long>(count);
|
||||
var v = new List<double>(count);
|
||||
for (int i = 0; i < count; i++)
|
||||
{
|
||||
t.Add(bars[i].Time);
|
||||
v.Add(bars[i].Close);
|
||||
}
|
||||
return new TSeries(t, v);
|
||||
}
|
||||
|
||||
// === Mathematical Validation ===
|
||||
|
||||
/// <summary>
|
||||
/// Validates squared log return calculation.
|
||||
/// </summary>
|
||||
[Theory]
|
||||
[InlineData(100.0, 101.0)]
|
||||
[InlineData(100.0, 110.0)]
|
||||
[InlineData(100.0, 90.0)]
|
||||
public void Rv_SquaredLogReturn_IsCorrect(double prevPrice, double curPrice)
|
||||
{
|
||||
double logReturn = Math.Log(curPrice / prevPrice);
|
||||
double squaredReturn = logReturn * logReturn;
|
||||
|
||||
Assert.True(squaredReturn >= 0, "Squared return must be non-negative");
|
||||
Assert.Equal(Math.Pow(logReturn, 2), squaredReturn, 15);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Validates realized variance formula: sum of squared returns.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void Rv_RealizedVarianceFormula_IsCorrect()
|
||||
{
|
||||
double[] squaredReturns = { 0.0001, 0.0004, 0.0009, 0.0016, 0.0025 };
|
||||
double sumSquared = 0;
|
||||
for (int i = 0; i < squaredReturns.Length; i++)
|
||||
{
|
||||
sumSquared += squaredReturns[i];
|
||||
}
|
||||
|
||||
// Expected sum = 0.0055
|
||||
Assert.Equal(0.0055, sumSquared, 10);
|
||||
|
||||
// Realized volatility = sqrt(sum)
|
||||
double rv = Math.Sqrt(sumSquared);
|
||||
Assert.Equal(Math.Sqrt(0.0055), rv, 10);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Validates annualization factor: √(252) for daily data.
|
||||
/// </summary>
|
||||
[Theory]
|
||||
[InlineData(252, 15.8745078663875)]
|
||||
[InlineData(365, 19.1049731745428)]
|
||||
[InlineData(52, 7.21110255092798)]
|
||||
public void Rv_AnnualizationFactor_IsCorrect(int annualPeriods, double expectedFactor)
|
||||
{
|
||||
double factor = Math.Sqrt(annualPeriods);
|
||||
Assert.Equal(expectedFactor, factor, 10);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Validates known calculation with manual verification.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void Rv_KnownCalculation_IsCorrect()
|
||||
{
|
||||
// Prices: 100, 102, 101, 103, 102, 104 (6 prices = 5 returns)
|
||||
double[] prices = { 100.0, 102.0, 101.0, 103.0, 102.0, 104.0 };
|
||||
|
||||
// Manual calculation with period=5 (all 5 returns), smoothingPeriod=1 (no smoothing)
|
||||
double sumSquared = 0;
|
||||
for (int i = 1; i < prices.Length; i++)
|
||||
{
|
||||
double r = Math.Log(prices[i] / prices[i - 1]);
|
||||
sumSquared += r * r;
|
||||
}
|
||||
double expected = Math.Sqrt(sumSquared);
|
||||
|
||||
// Verify with indicator (no annualization, smoothing=1)
|
||||
var rv = new Rv(period: 5, smoothingPeriod: 1, annualize: false);
|
||||
for (int i = 0; i < prices.Length; i++)
|
||||
{
|
||||
rv.Update(new TValue(DateTime.UtcNow.AddMinutes(i), prices[i]));
|
||||
}
|
||||
|
||||
Assert.Equal(expected, rv.Last.Value, 10);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Validates constant prices produce zero volatility.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void Rv_ConstantPrices_ProducesZeroVolatility()
|
||||
{
|
||||
var rv = new Rv(period: 5, smoothingPeriod: 3, annualize: false);
|
||||
|
||||
for (int i = 0; i < 20; i++)
|
||||
{
|
||||
rv.Update(new TValue(DateTime.UtcNow.AddMinutes(i), 100.0));
|
||||
}
|
||||
|
||||
Assert.Equal(0.0, rv.Last.Value, 10);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Validates SMA smoothing of raw volatilities.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void Rv_SmaSmoothing_WorksCorrectly()
|
||||
{
|
||||
var prices = GeneratePriceSeries(50);
|
||||
|
||||
// Short smoothing vs long smoothing
|
||||
var rvShort = new Rv(period: 5, smoothingPeriod: 3, annualize: false);
|
||||
var rvLong = new Rv(period: 5, smoothingPeriod: 10, annualize: false);
|
||||
|
||||
var shortResults = new List<double>();
|
||||
var longResults = new List<double>();
|
||||
|
||||
for (int i = 0; i < prices.Count; i++)
|
||||
{
|
||||
rvShort.Update(prices[i]);
|
||||
rvLong.Update(prices[i]);
|
||||
|
||||
if (rvShort.IsHot && rvLong.IsHot)
|
||||
{
|
||||
shortResults.Add(rvShort.Last.Value);
|
||||
longResults.Add(rvLong.Last.Value);
|
||||
}
|
||||
}
|
||||
|
||||
// Longer smoothing should produce smoother (less variable) results
|
||||
double shortVar = Variance(shortResults);
|
||||
double longVar = Variance(longResults);
|
||||
|
||||
Assert.True(shortResults.Count > 0, "Should have results");
|
||||
Assert.True(longVar < shortVar, "Longer smoothing should be smoother");
|
||||
}
|
||||
|
||||
// === Consistency Tests ===
|
||||
|
||||
/// <summary>
|
||||
/// Validates streaming and batch produce identical results.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void Rv_StreamingMatchesBatch()
|
||||
{
|
||||
var prices = GeneratePriceSeries(100);
|
||||
|
||||
// Streaming calculation
|
||||
var streamingRv = new Rv(5, 10);
|
||||
for (int i = 0; i < prices.Count; i++)
|
||||
{
|
||||
streamingRv.Update(prices[i]);
|
||||
}
|
||||
|
||||
// Batch calculation
|
||||
var batchResult = Rv.Calculate(prices, 5, 10);
|
||||
|
||||
Assert.Equal(batchResult.Last.Value, streamingRv.Last.Value, 8);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Validates TSeries input matches TValue streaming.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void Rv_TSeriesInput_MatchesStreaming()
|
||||
{
|
||||
var prices = GeneratePriceSeries(100);
|
||||
|
||||
// Streaming
|
||||
var streamingRv = new Rv(5, 10);
|
||||
for (int i = 0; i < prices.Count; i++)
|
||||
{
|
||||
streamingRv.Update(prices[i]);
|
||||
}
|
||||
|
||||
// TSeries batch
|
||||
var batchRv = new Rv(5, 10);
|
||||
var batchResult = batchRv.Update(prices);
|
||||
|
||||
Assert.Equal(batchResult.Last.Value, streamingRv.Last.Value, 10);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Validates annualized output is scaled correctly.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void Rv_Annualized_ScaledCorrectly()
|
||||
{
|
||||
var prices = GeneratePriceSeries(50);
|
||||
|
||||
var rvRaw = new Rv(5, 10, annualize: false);
|
||||
var rvAnn = new Rv(5, 10, annualize: true, annualPeriods: 252);
|
||||
|
||||
for (int i = 0; i < prices.Count; i++)
|
||||
{
|
||||
rvRaw.Update(prices[i]);
|
||||
rvAnn.Update(prices[i]);
|
||||
}
|
||||
|
||||
double expectedRatio = Math.Sqrt(252);
|
||||
double actualRatio = rvAnn.Last.Value / rvRaw.Last.Value;
|
||||
|
||||
Assert.Equal(expectedRatio, actualRatio, 6);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Validates TBar update uses only Close price.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void Rv_TBar_UsesOnlyClose()
|
||||
{
|
||||
var bars = GenerateTestData(50);
|
||||
|
||||
var rvBar = new Rv(5, 10);
|
||||
for (int i = 0; i < bars.Count; i++)
|
||||
{
|
||||
rvBar.Update(bars[i]);
|
||||
}
|
||||
|
||||
var rvClose = new Rv(5, 10);
|
||||
for (int i = 0; i < bars.Count; i++)
|
||||
{
|
||||
rvClose.Update(new TValue(bars[i].Time, bars[i].Close));
|
||||
}
|
||||
|
||||
Assert.Equal(rvClose.Last.Value, rvBar.Last.Value, 10);
|
||||
}
|
||||
|
||||
// === Parameter Sensitivity ===
|
||||
|
||||
/// <summary>
|
||||
/// Validates shorter period is more responsive.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void Rv_ShorterPeriod_MoreResponsive()
|
||||
{
|
||||
var prices = GeneratePriceSeries(50);
|
||||
|
||||
var rvShort = new Rv(3, 5);
|
||||
var rvLong = new Rv(10, 5);
|
||||
|
||||
var shortResults = new List<double>();
|
||||
var longResults = new List<double>();
|
||||
|
||||
for (int i = 0; i < prices.Count; i++)
|
||||
{
|
||||
rvShort.Update(prices[i]);
|
||||
rvLong.Update(prices[i]);
|
||||
|
||||
if (rvShort.IsHot && rvLong.IsHot)
|
||||
{
|
||||
shortResults.Add(rvShort.Last.Value);
|
||||
longResults.Add(rvLong.Last.Value);
|
||||
}
|
||||
}
|
||||
|
||||
double shortVar = Variance(shortResults);
|
||||
double longVar = Variance(longResults);
|
||||
|
||||
Assert.True(shortResults.Count > 0, "Should have results");
|
||||
Assert.True(shortVar > longVar * 0.5, "Shorter period should be more variable");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Validates different parameters produce different results.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void Rv_DifferentParameters_ProduceDifferentResults()
|
||||
{
|
||||
var prices = GeneratePriceSeries(50);
|
||||
|
||||
var rv1 = new Rv(5, 10);
|
||||
var rv2 = new Rv(5, 20);
|
||||
var rv3 = new Rv(10, 10);
|
||||
|
||||
for (int i = 0; i < prices.Count; i++)
|
||||
{
|
||||
rv1.Update(prices[i]);
|
||||
rv2.Update(prices[i]);
|
||||
rv3.Update(prices[i]);
|
||||
}
|
||||
|
||||
Assert.NotEqual(rv1.Last.Value, rv2.Last.Value);
|
||||
Assert.NotEqual(rv1.Last.Value, rv3.Last.Value);
|
||||
}
|
||||
|
||||
// === Edge Cases ===
|
||||
|
||||
/// <summary>
|
||||
/// Validates handling of very small price changes.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void Rv_VerySmallChanges_HandledCorrectly()
|
||||
{
|
||||
var rv = new Rv(5, 10, annualize: false);
|
||||
|
||||
double price = 100.0;
|
||||
for (int i = 0; i < 30; i++)
|
||||
{
|
||||
price += 0.001 * (i % 2 == 0 ? 1 : -1);
|
||||
rv.Update(new TValue(DateTime.UtcNow.AddMinutes(i), price));
|
||||
}
|
||||
|
||||
Assert.True(double.IsFinite(rv.Last.Value));
|
||||
Assert.True(rv.Last.Value >= 0);
|
||||
Assert.True(rv.Last.Value < 0.01, "Small changes should produce small RV");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Validates handling of large price swings.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void Rv_LargePriceSwings_HandledCorrectly()
|
||||
{
|
||||
var rv = new Rv(5, 10, annualize: false);
|
||||
|
||||
double price = 100.0;
|
||||
for (int i = 0; i < 30; i++)
|
||||
{
|
||||
price *= (i % 2 == 0 ? 1.1 : 0.9);
|
||||
rv.Update(new TValue(DateTime.UtcNow.AddMinutes(i), price));
|
||||
}
|
||||
|
||||
Assert.True(double.IsFinite(rv.Last.Value));
|
||||
Assert.True(rv.Last.Value > 0, "Large swings should produce positive RV");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Validates warmup period calculation.
|
||||
/// </summary>
|
||||
[Theory]
|
||||
[InlineData(5, 10, 15)]
|
||||
[InlineData(5, 20, 25)]
|
||||
[InlineData(10, 10, 20)]
|
||||
public void Rv_WarmupPeriod_IsCorrect(int period, int smoothing, int expectedWarmup)
|
||||
{
|
||||
var rv = new Rv(period, smoothing);
|
||||
Assert.Equal(expectedWarmup, rv.WarmupPeriod);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Validates output is always non-negative.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void Rv_Output_IsNonNegative()
|
||||
{
|
||||
var prices = GeneratePriceSeries(100);
|
||||
var rv = new Rv(5, 10);
|
||||
|
||||
for (int i = 0; i < prices.Count; i++)
|
||||
{
|
||||
rv.Update(prices[i]);
|
||||
if (rv.IsHot)
|
||||
{
|
||||
Assert.True(rv.Last.Value >= 0, $"RV should be non-negative at bar {i}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Validates bar correction works correctly.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void Rv_BarCorrection_WorksCorrectly()
|
||||
{
|
||||
var rv = new Rv(5, 10);
|
||||
var prices = GeneratePriceSeries(30);
|
||||
|
||||
for (int i = 0; i < 20; i++)
|
||||
{
|
||||
rv.Update(prices[i], isNew: true);
|
||||
}
|
||||
|
||||
rv.Update(prices[20], isNew: true);
|
||||
double afterNew = rv.Last.Value;
|
||||
|
||||
var correctedPrice = new TValue(prices[20].Time, prices[20].Value * 2.0);
|
||||
rv.Update(correctedPrice, isNew: false);
|
||||
double afterCorrection = rv.Last.Value;
|
||||
|
||||
rv.Update(prices[20], isNew: false);
|
||||
double afterRestore = rv.Last.Value;
|
||||
|
||||
Assert.NotEqual(afterNew, afterCorrection);
|
||||
Assert.Equal(afterNew, afterRestore, 10);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Validates iterative corrections converge.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void Rv_IterativeCorrections_Converge()
|
||||
{
|
||||
var rv = new Rv(5, 10);
|
||||
var prices = GeneratePriceSeries(30);
|
||||
|
||||
for (int i = 0; i < 20; i++)
|
||||
{
|
||||
rv.Update(prices[i], isNew: true);
|
||||
}
|
||||
|
||||
for (int j = 0; j < 5; j++)
|
||||
{
|
||||
var tempPrice = new TValue(prices[19].Time, prices[19].Value * (1.0 + j * 0.01));
|
||||
rv.Update(tempPrice, isNew: false);
|
||||
}
|
||||
|
||||
rv.Update(prices[19], isNew: false);
|
||||
double afterCorrections = rv.Last.Value;
|
||||
|
||||
var rvFresh = new Rv(5, 10);
|
||||
for (int i = 0; i < 20; i++)
|
||||
{
|
||||
rvFresh.Update(prices[i], isNew: true);
|
||||
}
|
||||
double freshValue = rvFresh.Last.Value;
|
||||
|
||||
Assert.Equal(freshValue, afterCorrections, 10);
|
||||
}
|
||||
|
||||
// === Comparison Tests ===
|
||||
|
||||
/// <summary>
|
||||
/// Validates RV vs HV produce correlated but different results.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void Rv_VsHv_RelatedButDifferent()
|
||||
{
|
||||
var bars = GenerateTestData(50);
|
||||
|
||||
// RV with period=14, smoothing=1 (similar to HV behavior)
|
||||
var rv = new Rv(14, 1, annualize: false);
|
||||
var hv = new Hv(14, annualize: false);
|
||||
|
||||
for (int i = 0; i < bars.Count; i++)
|
||||
{
|
||||
rv.Update(bars[i]);
|
||||
hv.Update(bars[i]);
|
||||
}
|
||||
|
||||
// Both should produce positive values
|
||||
Assert.True(rv.Last.Value > 0);
|
||||
Assert.True(hv.Last.Value > 0);
|
||||
|
||||
// They measure similar concepts but with different formulas
|
||||
// RV uses sum of squared returns, HV uses standard deviation
|
||||
// Both should be in similar magnitude range
|
||||
double ratio = rv.Last.Value / hv.Last.Value;
|
||||
Assert.True(ratio > 0.1 && ratio < 10, "RV and HV should be in similar range");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Validates stability over repeated runs with same seed.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void Rv_Stability_ConsistentOverRepeatedRuns()
|
||||
{
|
||||
var results = new List<double>();
|
||||
|
||||
for (int run = 0; run < 3; run++)
|
||||
{
|
||||
var gbm = new GBM(seed: 42);
|
||||
var bars = gbm.Fetch(100, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1));
|
||||
var rv = new Rv(5, 10);
|
||||
|
||||
for (int i = 0; i < bars.Count; i++)
|
||||
{
|
||||
rv.Update(bars[i]);
|
||||
}
|
||||
results.Add(rv.Last.Value);
|
||||
}
|
||||
|
||||
Assert.Equal(results[0], results[1], 15);
|
||||
Assert.Equal(results[1], results[2], 15);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Validates RV responds to volatility regime changes.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void Rv_RespondsToVolatilityRegimeChange()
|
||||
{
|
||||
var rv = new Rv(5, 5, annualize: false);
|
||||
|
||||
// Low volatility regime
|
||||
double price = 100.0;
|
||||
for (int i = 0; i < 20; i++)
|
||||
{
|
||||
price *= (i % 2 == 0 ? 1.001 : 0.999);
|
||||
rv.Update(new TValue(DateTime.UtcNow.AddMinutes(i), price));
|
||||
}
|
||||
double lowVolValue = rv.Last.Value;
|
||||
|
||||
// High volatility regime
|
||||
for (int i = 20; i < 40; i++)
|
||||
{
|
||||
price *= (i % 2 == 0 ? 1.05 : 0.95);
|
||||
rv.Update(new TValue(DateTime.UtcNow.AddMinutes(i), price));
|
||||
}
|
||||
double highVolValue = rv.Last.Value;
|
||||
|
||||
Assert.True(highVolValue > lowVolValue * 5,
|
||||
"RV should significantly increase with higher volatility regime");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Validates RV produces reasonable volatility estimate.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void Rv_ProducesReasonableVolatilityEstimate()
|
||||
{
|
||||
var prices = GeneratePriceSeries(100);
|
||||
var rv = new Rv(5, 10, annualize: false);
|
||||
|
||||
for (int i = 0; i < prices.Count; i++)
|
||||
{
|
||||
rv.Update(prices[i]);
|
||||
}
|
||||
|
||||
Assert.True(double.IsFinite(rv.Last.Value));
|
||||
Assert.True(rv.Last.Value > 0);
|
||||
Assert.True(rv.Last.Value < 1, "Raw RV should be < 100%");
|
||||
}
|
||||
|
||||
// === Helper Methods ===
|
||||
|
||||
private static double Variance(List<double> values)
|
||||
{
|
||||
if (values.Count == 0)
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
double mean = values.Average();
|
||||
return values.Average(v => Math.Pow(v - mean, 2));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,527 @@
|
||||
// Realized Volatility (RV) Indicator
|
||||
// Sum of squared log returns, then sqrt, smoothed with SMA
|
||||
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Runtime.InteropServices;
|
||||
|
||||
namespace QuanTAlib;
|
||||
|
||||
/// <summary>
|
||||
/// RV: Realized Volatility
|
||||
/// Calculates volatility as the square root of realized variance (sum of squared log returns),
|
||||
/// smoothed with a Simple Moving Average.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// <b>Calculation steps:</b>
|
||||
/// <list type="number">
|
||||
/// <item>Calculate log return: r_t = ln(price_t / price_{t-1})</item>
|
||||
/// <item>Compute realized variance: RV_t = Σ(r_i²) for returns in window</item>
|
||||
/// <item>Take square root: volatility_t = √(RV_t)</item>
|
||||
/// <item>Smooth with SMA over smoothing period</item>
|
||||
/// <item>If annualize: volatility × √(annualPeriods)</item>
|
||||
/// </list>
|
||||
///
|
||||
/// <b>Key characteristics:</b>
|
||||
/// <list type="bullet">
|
||||
/// <item>Based on sum of squared returns (not variance-adjusted)</item>
|
||||
/// <item>More responsive to recent volatility bursts</item>
|
||||
/// <item>SMA smoothing reduces noise</item>
|
||||
/// <item>Standard measure in academic finance and risk management</item>
|
||||
/// </list>
|
||||
///
|
||||
/// <b>Sources:</b>
|
||||
/// Andersen, T.G., Bollerslev, T. (1998). "Answering the Skeptics: Yes, Standard
|
||||
/// Volatility Models Do Provide Accurate Forecasts". International Economic Review.
|
||||
/// </remarks>
|
||||
[SkipLocalsInit]
|
||||
public sealed class Rv : AbstractBase
|
||||
{
|
||||
private readonly int _period;
|
||||
private readonly int _smoothingPeriod;
|
||||
private readonly bool _annualize;
|
||||
private readonly int _annualPeriods;
|
||||
private readonly double _annualFactor;
|
||||
private readonly RingBuffer _returnBuffer;
|
||||
private readonly RingBuffer _volatilityBuffer;
|
||||
|
||||
[StructLayout(LayoutKind.Auto)]
|
||||
private record struct State(
|
||||
double PrevPrice,
|
||||
double LastValidReturn,
|
||||
double LastValue,
|
||||
int ReturnCount
|
||||
);
|
||||
private State _s;
|
||||
private State _ps;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the Rv class.
|
||||
/// </summary>
|
||||
/// <param name="period">The window for calculating realized variance (default 5).</param>
|
||||
/// <param name="smoothingPeriod">The SMA 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 or smoothingPeriod is less than 1, or annualPeriods is less than 1 when annualizing.
|
||||
/// </exception>
|
||||
public Rv(int period = 5, int smoothingPeriod = 20, bool annualize = true, int annualPeriods = 252)
|
||||
{
|
||||
if (period < 1)
|
||||
{
|
||||
throw new ArgumentException("Period must be at least 1", nameof(period));
|
||||
}
|
||||
if (smoothingPeriod < 1)
|
||||
{
|
||||
throw new ArgumentException("Smoothing period must be at least 1", nameof(smoothingPeriod));
|
||||
}
|
||||
if (annualize && annualPeriods <= 0)
|
||||
{
|
||||
throw new ArgumentException("Annual periods must be greater than 0 when annualizing", nameof(annualPeriods));
|
||||
}
|
||||
_period = period;
|
||||
_smoothingPeriod = smoothingPeriod;
|
||||
_annualize = annualize;
|
||||
_annualPeriods = annualPeriods;
|
||||
_annualFactor = annualize ? Math.Sqrt(annualPeriods) : 1.0;
|
||||
_returnBuffer = new RingBuffer(period);
|
||||
_volatilityBuffer = new RingBuffer(smoothingPeriod);
|
||||
WarmupPeriod = period + smoothingPeriod; // Need returns + smoothing
|
||||
Name = $"Rv({period},{smoothingPeriod})";
|
||||
_s = new State(double.NaN, 0, 0, 0);
|
||||
_ps = _s;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the Rv class with a source.
|
||||
/// </summary>
|
||||
/// <param name="source">The data source for chaining.</param>
|
||||
/// <param name="period">The window for calculating realized variance (default 5).</param>
|
||||
/// <param name="smoothingPeriod">The SMA 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 Rv(ITValuePublisher source, int period = 5, int smoothingPeriod = 20, bool annualize = true, int annualPeriods = 252)
|
||||
: this(period, smoothingPeriod, 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 => _volatilityBuffer.Count >= _smoothingPeriod;
|
||||
|
||||
/// <summary>
|
||||
/// The window for calculating realized variance.
|
||||
/// </summary>
|
||||
public int Period => _period;
|
||||
|
||||
/// <summary>
|
||||
/// The SMA smoothing period.
|
||||
/// </summary>
|
||||
public int SmoothingPeriod => _smoothingPeriod;
|
||||
|
||||
/// <summary>
|
||||
/// Whether volatility is annualized.
|
||||
/// </summary>
|
||||
public bool Annualize => _annualize;
|
||||
|
||||
/// <summary>
|
||||
/// Number of periods per year for annualization.
|
||||
/// </summary>
|
||||
public int AnnualPeriods => _annualPeriods;
|
||||
|
||||
/// <summary>
|
||||
/// Updates the indicator with a new price value.
|
||||
/// </summary>
|
||||
/// <param name="input">The input price value.</param>
|
||||
/// <param name="isNew">Whether this is a new bar or an update.</param>
|
||||
/// <returns>The calculated volatility value.</returns>
|
||||
[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 (uses Close price).
|
||||
/// </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)
|
||||
{
|
||||
return UpdateCore(bar.Time, bar.Close, 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 close prices
|
||||
Span<double> closes = len <= 128 ? stackalloc double[len] : new double[len];
|
||||
|
||||
for (int i = 0; i < len; i++)
|
||||
{
|
||||
closes[i] = source[i].Close;
|
||||
tSpan[i] = source[i].Time;
|
||||
}
|
||||
|
||||
Batch(closes, vSpan, _period, _smoothingPeriod, _annualize, _annualPeriods);
|
||||
|
||||
// Update internal state
|
||||
for (int i = 0; i < len; i++)
|
||||
{
|
||||
Update(new TValue(source[i].Time, source[i].Close), 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);
|
||||
|
||||
Batch(source.Values, vSpan, _period, _smoothingPeriod, _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 price, bool isNew)
|
||||
{
|
||||
if (isNew)
|
||||
{
|
||||
_ps = _s;
|
||||
_returnBuffer.Snapshot();
|
||||
_volatilityBuffer.Snapshot();
|
||||
}
|
||||
else
|
||||
{
|
||||
_s = _ps;
|
||||
_returnBuffer.Restore();
|
||||
_volatilityBuffer.Restore();
|
||||
}
|
||||
|
||||
var s = _s;
|
||||
|
||||
// Handle non-finite price
|
||||
if (!double.IsFinite(price) || price <= 0)
|
||||
{
|
||||
// Can't compute return, output last value
|
||||
Last = new TValue(timeTicks, s.LastValue);
|
||||
PubEvent(Last, isNew);
|
||||
return Last;
|
||||
}
|
||||
|
||||
double result;
|
||||
|
||||
// First price - no return yet
|
||||
if (double.IsNaN(s.PrevPrice))
|
||||
{
|
||||
s = s with { PrevPrice = price };
|
||||
result = 0;
|
||||
}
|
||||
else
|
||||
{
|
||||
// Calculate log return
|
||||
double logReturn = Math.Log(price / s.PrevPrice);
|
||||
|
||||
if (!double.IsFinite(logReturn))
|
||||
{
|
||||
logReturn = s.LastValidReturn;
|
||||
}
|
||||
else
|
||||
{
|
||||
s = s with { LastValidReturn = logReturn };
|
||||
}
|
||||
|
||||
// Add squared return to buffer
|
||||
double squaredReturn = logReturn * logReturn;
|
||||
_returnBuffer.Add(squaredReturn);
|
||||
|
||||
// Calculate realized variance (sum of squared returns)
|
||||
double sumSquaredReturns = 0;
|
||||
for (int i = 0; i < _returnBuffer.Count; i++)
|
||||
{
|
||||
sumSquaredReturns += _returnBuffer[i];
|
||||
}
|
||||
|
||||
// Raw volatility = sqrt(realized variance)
|
||||
double rawVolatility = Math.Sqrt(sumSquaredReturns);
|
||||
|
||||
// Add to smoothing buffer
|
||||
_volatilityBuffer.Add(rawVolatility);
|
||||
|
||||
// Calculate SMA of volatilities
|
||||
double sumVol = 0;
|
||||
for (int i = 0; i < _volatilityBuffer.Count; i++)
|
||||
{
|
||||
sumVol += _volatilityBuffer[i];
|
||||
}
|
||||
double smoothedVolatility = sumVol / _volatilityBuffer.Count;
|
||||
|
||||
// Apply annualization
|
||||
result = smoothedVolatility * _annualFactor;
|
||||
|
||||
s = s with
|
||||
{
|
||||
PrevPrice = price,
|
||||
ReturnCount = s.ReturnCount + 1
|
||||
};
|
||||
}
|
||||
|
||||
if (!double.IsFinite(result))
|
||||
{
|
||||
result = s.LastValue;
|
||||
}
|
||||
else
|
||||
{
|
||||
s = s with { LastValue = result };
|
||||
}
|
||||
|
||||
_s = s;
|
||||
|
||||
Last = new TValue(timeTicks, result);
|
||||
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(double.NaN, 0, 0, 0);
|
||||
_ps = _s;
|
||||
_returnBuffer.Clear();
|
||||
_volatilityBuffer.Clear();
|
||||
Last = default;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Calculates Realized Volatility for a price series (static).
|
||||
/// </summary>
|
||||
/// <param name="source">The source price series.</param>
|
||||
/// <param name="period">The window for realized variance.</param>
|
||||
/// <param name="smoothingPeriod">The SMA 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(TSeries source, int period = 5, int smoothingPeriod = 20, bool annualize = true, int annualPeriods = 252)
|
||||
{
|
||||
if (period < 1)
|
||||
{
|
||||
throw new ArgumentException("Period must be at least 1", nameof(period));
|
||||
}
|
||||
if (smoothingPeriod < 1)
|
||||
{
|
||||
throw new ArgumentException("Smoothing period must be at least 1", nameof(smoothingPeriod));
|
||||
}
|
||||
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);
|
||||
|
||||
Batch(source.Values, vSpan, period, smoothingPeriod, annualize, annualPeriods);
|
||||
source.Times.CopyTo(tSpan);
|
||||
|
||||
return new TSeries(t, v);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Calculates RV for a bar series (static).
|
||||
/// </summary>
|
||||
public static TSeries Calculate(TBarSeries source, int period = 5, int smoothingPeriod = 20, bool annualize = true, int annualPeriods = 252)
|
||||
{
|
||||
var rv = new Rv(period, smoothingPeriod, annualize, annualPeriods);
|
||||
return rv.Update(source);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Batch calculation using spans.
|
||||
/// </summary>
|
||||
/// <param name="prices">Price values.</param>
|
||||
/// <param name="output">Output volatility values.</param>
|
||||
/// <param name="period">The window for realized variance.</param>
|
||||
/// <param name="smoothingPeriod">The SMA smoothing period.</param>
|
||||
/// <param name="annualize">Whether to annualize.</param>
|
||||
/// <param name="annualPeriods">Periods per year.</param>
|
||||
public static void Batch(
|
||||
ReadOnlySpan<double> prices,
|
||||
Span<double> output,
|
||||
int period = 5,
|
||||
int smoothingPeriod = 20,
|
||||
bool annualize = true,
|
||||
int annualPeriods = 252)
|
||||
{
|
||||
if (period < 1)
|
||||
{
|
||||
throw new ArgumentException("Period must be at least 1", nameof(period));
|
||||
}
|
||||
if (smoothingPeriod < 1)
|
||||
{
|
||||
throw new ArgumentException("Smoothing period must be at least 1", nameof(smoothingPeriod));
|
||||
}
|
||||
if (annualize && annualPeriods <= 0)
|
||||
{
|
||||
throw new ArgumentException("Annual periods must be greater than 0 when annualizing", nameof(annualPeriods));
|
||||
}
|
||||
if (output.Length < prices.Length)
|
||||
{
|
||||
throw new ArgumentException("Output span must be at least as long as prices span", nameof(output));
|
||||
}
|
||||
|
||||
int len = prices.Length;
|
||||
if (len == 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
double annualFactor = annualize ? Math.Sqrt(annualPeriods) : 1.0;
|
||||
|
||||
// Ring buffers for squared returns and raw volatilities
|
||||
Span<double> returnBuffer = period <= 128 ? stackalloc double[period] : new double[period];
|
||||
Span<double> volBuffer = smoothingPeriod <= 128 ? stackalloc double[smoothingPeriod] : new double[smoothingPeriod];
|
||||
|
||||
int returnHead = 0;
|
||||
int returnCount = 0;
|
||||
int volHead = 0;
|
||||
int volCount = 0;
|
||||
|
||||
double prevPrice = double.NaN;
|
||||
double lastValidReturn = 0;
|
||||
double lastValue = 0;
|
||||
double sumSquaredReturns = 0;
|
||||
double sumVol = 0;
|
||||
|
||||
for (int i = 0; i < len; i++)
|
||||
{
|
||||
double price = prices[i];
|
||||
|
||||
// First price - no return
|
||||
if (double.IsNaN(prevPrice))
|
||||
{
|
||||
prevPrice = price;
|
||||
output[i] = 0;
|
||||
continue;
|
||||
}
|
||||
|
||||
// Handle invalid price
|
||||
if (!double.IsFinite(price) || price <= 0)
|
||||
{
|
||||
output[i] = lastValue;
|
||||
continue;
|
||||
}
|
||||
|
||||
// Calculate log return
|
||||
double logReturn = Math.Log(price / prevPrice);
|
||||
prevPrice = price;
|
||||
|
||||
if (!double.IsFinite(logReturn))
|
||||
{
|
||||
logReturn = lastValidReturn;
|
||||
}
|
||||
else
|
||||
{
|
||||
lastValidReturn = logReturn;
|
||||
}
|
||||
|
||||
double squaredReturn = logReturn * logReturn;
|
||||
|
||||
// Update return buffer
|
||||
if (returnCount == period)
|
||||
{
|
||||
sumSquaredReturns -= returnBuffer[returnHead];
|
||||
}
|
||||
else
|
||||
{
|
||||
returnCount++;
|
||||
}
|
||||
returnBuffer[returnHead] = squaredReturn;
|
||||
returnHead = (returnHead + 1) % period;
|
||||
sumSquaredReturns += squaredReturn;
|
||||
|
||||
// Raw volatility
|
||||
double rawVolatility = Math.Sqrt(sumSquaredReturns);
|
||||
|
||||
// Update volatility buffer for SMA
|
||||
if (volCount == smoothingPeriod)
|
||||
{
|
||||
sumVol -= volBuffer[volHead];
|
||||
}
|
||||
else
|
||||
{
|
||||
volCount++;
|
||||
}
|
||||
volBuffer[volHead] = rawVolatility;
|
||||
volHead = (volHead + 1) % smoothingPeriod;
|
||||
sumVol += rawVolatility;
|
||||
|
||||
// Smoothed volatility
|
||||
double smoothedVolatility = sumVol / volCount;
|
||||
double result = smoothedVolatility * annualFactor;
|
||||
|
||||
if (!double.IsFinite(result))
|
||||
{
|
||||
result = lastValue;
|
||||
}
|
||||
else
|
||||
{
|
||||
lastValue = result;
|
||||
}
|
||||
|
||||
output[i] = result;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,263 @@
|
||||
# RV: Realized Volatility
|
||||
|
||||
> "The sum of squared returns—a direct measure of how much the market actually moved, free from the assumptions embedded in standard deviation."
|
||||
|
||||
Realized Volatility (RV) measures price volatility using the sum of squared logarithmic returns over a rolling window, then applying SMA smoothing for stability. Unlike traditional Historical Volatility (HV) which calculates standard deviation of returns, RV directly accumulates squared returns—the raw building blocks of variance—providing a more direct measure of realized price variation.
|
||||
|
||||
## Historical Context
|
||||
|
||||
Realized volatility emerged from the academic literature on high-frequency econometrics in the late 1990s and early 2000s, most notably through the work of Andersen, Bollerslev, Diebold, and Labys (2001). The concept was developed to provide model-free volatility estimates using intraday data, addressing limitations of parametric approaches like GARCH.
|
||||
|
||||
The key insight was that as sampling frequency increases, the sum of squared returns converges to the quadratic variation of the price process—the true integrated variance. While the original formulation targeted tick-by-tick or 5-minute returns, the concept applies at any frequency.
|
||||
|
||||
This implementation adapts the realized volatility concept to standard bar data:
|
||||
- Calculate squared log returns within a rolling window (period)
|
||||
- Take the square root to convert variance to volatility
|
||||
- Apply SMA smoothing for noise reduction
|
||||
- Optionally annualize for comparability
|
||||
|
||||
## Architecture & Physics
|
||||
|
||||
### 1. Log Return Calculation
|
||||
|
||||
Each period's return is computed as the natural logarithm of price ratios:
|
||||
|
||||
$$
|
||||
r_t = \ln\left(\frac{P_t}{P_{t-1}}\right)
|
||||
$$
|
||||
|
||||
where:
|
||||
|
||||
- $P_t$ = Closing price at time $t$
|
||||
- $P_{t-1}$ = Closing price at time $t-1$
|
||||
|
||||
### 2. Squared Return Accumulation
|
||||
|
||||
The realized variance is the sum of squared returns over the rolling window:
|
||||
|
||||
$$
|
||||
RVar_t = \sum_{i=0}^{n-1} r_{t-i}^2
|
||||
$$
|
||||
|
||||
where $n$ = period (default 5).
|
||||
|
||||
This differs from standard variance which subtracts the mean:
|
||||
- Standard variance: $\sigma^2 = E[(X - \mu)^2]$
|
||||
- Realized variance: $RVar = \sum r^2$ (assumes zero mean over short windows)
|
||||
|
||||
### 3. Volatility Conversion
|
||||
|
||||
Convert realized variance to volatility (standard deviation scale):
|
||||
|
||||
$$
|
||||
RVol_t = \sqrt{RVar_t}
|
||||
$$
|
||||
|
||||
### 4. SMA Smoothing
|
||||
|
||||
Apply simple moving average to smooth the raw volatility:
|
||||
|
||||
$$
|
||||
RV_t = \frac{1}{m} \sum_{i=0}^{m-1} RVol_{t-i}
|
||||
$$
|
||||
|
||||
where $m$ = smoothingPeriod (default 20).
|
||||
|
||||
### 5. Optional Annualization
|
||||
|
||||
If annualization is enabled:
|
||||
|
||||
$$
|
||||
RV_{annual,t} = RV_t \times \sqrt{N}
|
||||
$$
|
||||
|
||||
where $N$ = annual periods (default 252 trading days).
|
||||
|
||||
## Mathematical Foundation
|
||||
|
||||
### Theoretical Basis
|
||||
|
||||
Under the standard diffusion model $dS = \mu S dt + \sigma S dW$, the quadratic variation is:
|
||||
|
||||
$$
|
||||
\langle \ln S \rangle_T = \int_0^T \sigma^2 dt
|
||||
$$
|
||||
|
||||
The realized variance estimator:
|
||||
|
||||
$$
|
||||
RVar = \sum_{i=1}^{n} r_i^2
|
||||
$$
|
||||
|
||||
is a consistent estimator of integrated variance as the sampling frequency increases.
|
||||
|
||||
### Why Sum Squared Returns (Not Standard Deviation)?
|
||||
|
||||
The realized volatility approach differs from HV in a subtle but important way:
|
||||
|
||||
| Aspect | HV (Standard Deviation) | RV (Sum Squared Returns) |
|
||||
| :--- | :--- | :--- |
|
||||
| **Formula** | $\sqrt{\frac{1}{n}\sum(r - \bar{r})^2}$ | $\sqrt{\sum r^2}$ |
|
||||
| **Mean treatment** | Subtracts sample mean | Assumes zero mean |
|
||||
| **Interpretation** | Dispersion around mean | Total quadratic variation |
|
||||
| **Best for** | Longer windows | Short windows, intraday |
|
||||
|
||||
For short windows (5-10 bars), the mean return is essentially noise. RV avoids estimating this noisy mean, providing a more stable measure.
|
||||
|
||||
### Relationship to HV
|
||||
|
||||
For a window with $n$ returns and mean $\bar{r}$:
|
||||
|
||||
$$
|
||||
\sum r_i^2 = n \cdot \sigma_{pop}^2 + n \cdot \bar{r}^2
|
||||
$$
|
||||
|
||||
When the mean is small (short windows, mean-reverting markets), both measures converge. RV will be slightly higher when there's a directional move within the window.
|
||||
|
||||
### SMA Smoothing Rationale
|
||||
|
||||
Raw realized volatility can be noisy, especially with small period values. The SMA smoothing:
|
||||
- Reduces day-to-day noise
|
||||
- Provides more stable signals
|
||||
- Allows customization (shorter smoothing = more responsive, longer = more stable)
|
||||
|
||||
## Performance Profile
|
||||
|
||||
### Operation Count (Streaming Mode, Scalar)
|
||||
|
||||
Per-bar operations after warmup:
|
||||
|
||||
| Operation | Count | Cost (cycles) | Subtotal |
|
||||
| :--- | :---: | :---: | :---: |
|
||||
| LOG | 1 | 25 | 25 |
|
||||
| DIV (price ratio) | 1 | 15 | 15 |
|
||||
| MUL (squared) | 1 | 3 | 3 |
|
||||
| ADD/SUB (ring buffer) | 2 | 1 | 2 |
|
||||
| SQRT | 1 | 15 | 15 |
|
||||
| ADD/SUB (SMA) | 2 | 1 | 2 |
|
||||
| DIV (SMA) | 1 | 15 | 15 |
|
||||
| MUL (annualize) | 1 | 3 | 3 |
|
||||
| **Total** | — | — | **~80 cycles** |
|
||||
|
||||
The dominant costs are LOG (31%) and SQRT/DIV (19% each).
|
||||
|
||||
### Memory Profile
|
||||
|
||||
- **Per instance:** ~120 bytes (state struct + two RingBuffer references)
|
||||
- **Return buffer:** 8 bytes × period (default 5 = 40 bytes)
|
||||
- **Volatility buffer:** 8 bytes × smoothingPeriod (default 20 = 160 bytes)
|
||||
- **100 instances @ defaults:** ~32 KB
|
||||
|
||||
### Quality Metrics
|
||||
|
||||
| Metric | Score | Notes |
|
||||
| :--- | :---: | :--- |
|
||||
| **Accuracy** | 7/10 | Model-free, converges to integrated variance |
|
||||
| **Timeliness** | 8/10 | Short period captures recent moves quickly |
|
||||
| **Smoothness** | 7/10 | SMA smoothing provides stability |
|
||||
| **Flexibility** | 8/10 | Two parameters allow tuning responsiveness |
|
||||
| **Simplicity** | 8/10 | Clear interpretation, straightforward implementation |
|
||||
|
||||
## Validation
|
||||
|
||||
RV is a custom implementation based on the realized volatility literature. Direct library comparisons are not available:
|
||||
|
||||
| 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 rv.pine reference |
|
||||
| **Manual** | ✅ | Validated against formula |
|
||||
|
||||
The implementation is validated against the mathematical formula and internal consistency tests (streaming = batch = span).
|
||||
|
||||
## Common Pitfalls
|
||||
|
||||
1. **Warmup period**: RV requires `period + smoothingPeriod` prices before producing valid results. With defaults (5, 20), you need 25 prices. The `IsHot` property indicates when warmup is complete.
|
||||
|
||||
2. **Zero or negative prices**: Log transformation requires positive prices. Zero or negative values trigger last-valid-value substitution to prevent NaN propagation.
|
||||
|
||||
3. **Period selection**: The period parameter controls how many squared returns are summed. Shorter periods (3-5) capture recent volatility bursts; longer periods (10-20) provide more stable variance estimates.
|
||||
|
||||
4. **Smoothing vs responsiveness trade-off**: Higher smoothingPeriod reduces noise but increases lag. For trading signals, consider shorter smoothing (5-10); for regime detection, longer smoothing (20-50).
|
||||
|
||||
5. **Comparison with HV**: RV measures total squared returns; HV measures dispersion around mean. During strong trends, RV > HV because it captures the directional move. Neither is "better"—they measure different things.
|
||||
|
||||
6. **Annualization assumptions**: Default annualization assumes 252 trading days. Adjust for intraday data, cryptocurrency (365 days), or weekly data.
|
||||
|
||||
7. **Minimum period constraint**: Period must be ≥ 2 to have at least one squared return in the window. SmoothingPeriod must be ≥ 1.
|
||||
|
||||
8. **Not the same as VIX methodology**: VIX uses option prices to derive implied volatility. This RV measures realized (historical) volatility from price data only.
|
||||
|
||||
## Trading Applications
|
||||
|
||||
### Volatility Regime Detection
|
||||
|
||||
Track RV percentile over lookback:
|
||||
|
||||
```
|
||||
High RV (>80th percentile): High volatility regime
|
||||
- Markets are moving significantly
|
||||
- Consider wider stops, reduced position sizes
|
||||
- Mean reversion in volatility may be near
|
||||
|
||||
Low RV (<20th percentile): Low volatility regime
|
||||
- Markets are quiet
|
||||
- Breakout potential increasing
|
||||
- Options may be cheap
|
||||
```
|
||||
|
||||
### Realized vs Implied Volatility Spread
|
||||
|
||||
Compare RV with option-implied volatility:
|
||||
|
||||
```
|
||||
IV > RV (positive spread): Options are relatively expensive
|
||||
- Volatility selling strategies may be attractive
|
||||
- Market expects future volatility > recent realized
|
||||
|
||||
IV < RV (negative spread): Options are relatively cheap
|
||||
- Volatility buying strategies may be attractive
|
||||
- Market may be underpricing risk
|
||||
```
|
||||
|
||||
### Position Sizing
|
||||
|
||||
Use RV for volatility-adjusted sizing:
|
||||
|
||||
```
|
||||
Base position × (Target RV / Current RV)
|
||||
|
||||
Example: If target is 15% annualized volatility and current RV is 30%:
|
||||
Position = Base × (0.15 / 0.30) = 50% of base
|
||||
```
|
||||
|
||||
### Volatility Breakout Strategy
|
||||
|
||||
```
|
||||
Entry: RV crosses above X-day high RV
|
||||
Exit: RV falls below Y-day average RV
|
||||
|
||||
The period and smoothingPeriod parameters allow tuning:
|
||||
- Short period + short smoothing: Catch quick volatility spikes
|
||||
- Longer period + longer smoothing: Identify sustained regime changes
|
||||
```
|
||||
|
||||
### Comparing Multiple Timeframes
|
||||
|
||||
```
|
||||
RV(5, 5) vs RV(5, 20) vs RV(5, 50)
|
||||
|
||||
Converging: Volatility regime is stable
|
||||
Diverging (short > long): Recent volatility spike
|
||||
Diverging (short < long): Volatility compression
|
||||
```
|
||||
|
||||
## References
|
||||
|
||||
- Andersen, T. G., Bollerslev, T., Diebold, F. X., & Labys, P. (2001). "The Distribution of Realized Exchange Rate Volatility." *Journal of the American Statistical Association*, 96(453), 42-55.
|
||||
- Andersen, T. G., Bollerslev, T., Diebold, F. X., & Ebens, H. (2001). "The Distribution of Realized Stock Return Volatility." *Journal of Financial Economics*, 61(1), 43-76.
|
||||
- Barndorff-Nielsen, O. E., & Shephard, N. (2002). "Econometric Analysis of Realized Volatility and Its Use in Estimating Stochastic Volatility Models." *Journal of the Royal Statistical Society: Series B*, 64(2), 253-280.
|
||||
- McAleer, M., & Medeiros, M. C. (2008). "Realized Volatility: A Review." *Econometric Reviews*, 27(1-3), 10-45.
|
||||
Reference in New Issue
Block a user