mirror of
https://github.com/mihakralj/QuanTAlib.git
synced 2026-08-22 04:28:04 +00:00
more volatilty
This commit is contained in:
@@ -0,0 +1,198 @@
|
||||
using TradingPlatform.BusinessLayer;
|
||||
using QuanTAlib;
|
||||
|
||||
namespace QuanTAlib.Tests;
|
||||
|
||||
public class JvoltynIndicatorTests
|
||||
{
|
||||
[Fact]
|
||||
public void JvoltynIndicator_Constructor_SetsDefaults()
|
||||
{
|
||||
var indicator = new JvoltynIndicator();
|
||||
|
||||
Assert.Equal(14, indicator.Period);
|
||||
Assert.Equal(SourceType.Close, indicator.Source);
|
||||
Assert.True(indicator.ShowColdValues);
|
||||
Assert.Equal("JVOLTYN - Normalized Jurik Volatility", indicator.Name);
|
||||
Assert.True(indicator.SeparateWindow);
|
||||
Assert.True(indicator.OnBackGround);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void JvoltynIndicator_ShortName_IncludesParameters()
|
||||
{
|
||||
var indicator = new JvoltynIndicator { Period = 20 };
|
||||
Assert.Contains("JVOLTYN", indicator.ShortName, StringComparison.Ordinal);
|
||||
Assert.Contains("20", indicator.ShortName, StringComparison.Ordinal);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void JvoltynIndicator_MinHistoryDepths_EqualsZero()
|
||||
{
|
||||
var indicator = new JvoltynIndicator();
|
||||
|
||||
Assert.Equal(0, JvoltynIndicator.MinHistoryDepths);
|
||||
Assert.Equal(0, ((IWatchlistIndicator)indicator).MinHistoryDepths);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void JvoltynIndicator_Initialize_CreatesInternalJvoltyn()
|
||||
{
|
||||
var indicator = new JvoltynIndicator();
|
||||
|
||||
// Initialize should not throw
|
||||
indicator.Initialize();
|
||||
|
||||
// After init, line series should exist
|
||||
Assert.Single(indicator.LinesSeries);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void JvoltynIndicator_ProcessUpdate_HistoricalBar_ComputesValue()
|
||||
{
|
||||
var indicator = new JvoltynIndicator { Period = 5 };
|
||||
indicator.Initialize();
|
||||
|
||||
// Add historical data with volatility
|
||||
var now = DateTime.UtcNow;
|
||||
for (int i = 0; i < 30; i++)
|
||||
{
|
||||
double basePrice = 100 + i * 2 + (i % 2 == 0 ? 5 : -5); // Add some volatility
|
||||
indicator.HistoricalData.AddBar(now.AddMinutes(i), basePrice, basePrice + 5, basePrice - 5, basePrice + 2, 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.0); // Jvoltyn minimum is 0
|
||||
Assert.True(val <= 100.0); // Jvoltyn maximum is 100
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void JvoltynIndicator_ProcessUpdate_NewBar_ComputesValue()
|
||||
{
|
||||
var indicator = new JvoltynIndicator { Period = 5 };
|
||||
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
|
||||
indicator.HistoricalData.AddBar(now.AddMinutes(30), 120, 128, 115, 125, 1500);
|
||||
indicator.ProcessUpdate(new UpdateArgs(UpdateReason.NewBar));
|
||||
|
||||
Assert.Equal(2, indicator.LinesSeries[0].Count);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void JvoltynIndicator_DifferentPeriods_Work()
|
||||
{
|
||||
int[] periods = { 5, 10, 14, 20, 50 };
|
||||
|
||||
foreach (var period in periods)
|
||||
{
|
||||
var indicator = new JvoltynIndicator { Period = period };
|
||||
indicator.Initialize();
|
||||
|
||||
var now = DateTime.UtcNow;
|
||||
for (int i = 0; i < 60; i++)
|
||||
{
|
||||
double basePrice = 100 + i + (i % 3 == 0 ? 10 : -5); // Add volatility
|
||||
indicator.HistoricalData.AddBar(now.AddMinutes(i), basePrice, basePrice + 5, basePrice - 5, basePrice + 2, 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.0, $"Period {period} should produce Jvoltyn >= 0");
|
||||
Assert.True(val <= 100.0, $"Period {period} should produce Jvoltyn <= 100");
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void JvoltynIndicator_DifferentSourceTypes_Work()
|
||||
{
|
||||
SourceType[] sources = { SourceType.Close, SourceType.High, SourceType.Low, SourceType.HL2, SourceType.HLC3 };
|
||||
|
||||
foreach (var source in sources)
|
||||
{
|
||||
var indicator = new JvoltynIndicator { Source = source };
|
||||
indicator.Initialize();
|
||||
|
||||
var now = DateTime.UtcNow;
|
||||
for (int i = 0; i < 40; 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));
|
||||
}
|
||||
|
||||
double val = indicator.LinesSeries[0].GetValue(0);
|
||||
Assert.True(double.IsFinite(val), $"Source {source} should produce finite value");
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void JvoltynIndicator_Period_CanBeChanged()
|
||||
{
|
||||
var indicator = new JvoltynIndicator();
|
||||
Assert.Equal(14, indicator.Period);
|
||||
|
||||
indicator.Period = 20;
|
||||
Assert.Equal(20, indicator.Period);
|
||||
|
||||
indicator.Period = 50;
|
||||
Assert.Equal(50, indicator.Period);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void JvoltynIndicator_ShowColdValues_CanBeToggled()
|
||||
{
|
||||
var indicator = new JvoltynIndicator();
|
||||
Assert.True(indicator.ShowColdValues);
|
||||
|
||||
indicator.ShowColdValues = false;
|
||||
Assert.False(indicator.ShowColdValues);
|
||||
|
||||
indicator.ShowColdValues = true;
|
||||
Assert.True(indicator.ShowColdValues);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void JvoltynIndicator_SourceCodeLink_IsValid()
|
||||
{
|
||||
var indicator = new JvoltynIndicator();
|
||||
Assert.Contains("github.com", indicator.SourceCodeLink, StringComparison.Ordinal);
|
||||
Assert.Contains("Jvoltyn.Quantower.cs", indicator.SourceCodeLink, StringComparison.Ordinal);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void JvoltynIndicator_OutputRange_IsZeroToHundred()
|
||||
{
|
||||
var indicator = new JvoltynIndicator { Period = 10 };
|
||||
indicator.Initialize();
|
||||
|
||||
var now = DateTime.UtcNow;
|
||||
for (int i = 0; i < 100; i++)
|
||||
{
|
||||
// Create varying volatility patterns
|
||||
double basePrice = 100 + (i % 10) * 5 + (i % 2 == 0 ? 20 : -15);
|
||||
indicator.HistoricalData.AddBar(now.AddMinutes(i), basePrice, basePrice + 10, basePrice - 10, basePrice + 3, 1000);
|
||||
indicator.ProcessUpdate(new UpdateArgs(UpdateReason.HistoricalBar));
|
||||
|
||||
double val = indicator.LinesSeries[0].GetValue(0);
|
||||
Assert.True(val >= 0.0, $"Bar {i}: value {val} should be >= 0");
|
||||
Assert.True(val <= 100.0, $"Bar {i}: value {val} should be <= 100");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
using System.Drawing;
|
||||
using System.Runtime.CompilerServices;
|
||||
using TradingPlatform.BusinessLayer;
|
||||
|
||||
namespace QuanTAlib;
|
||||
|
||||
[SkipLocalsInit]
|
||||
public sealed class JvoltynIndicator : Indicator, IWatchlistIndicator
|
||||
{
|
||||
[InputParameter("Period", sortIndex: 1, 1, 1000, 1, 0)]
|
||||
public int Period { get; set; } = 14;
|
||||
|
||||
[IndicatorExtensions.DataSourceInput]
|
||||
public SourceType Source { get; set; } = SourceType.Close;
|
||||
|
||||
[InputParameter("Show cold values", sortIndex: 21)]
|
||||
public bool ShowColdValues { get; set; } = true;
|
||||
|
||||
private Jvoltyn _jvoltyn = null!;
|
||||
private readonly LineSeries _series;
|
||||
private string _sourceName = null!;
|
||||
private Func<IHistoryItem, double> _priceSelector = null!;
|
||||
|
||||
public static int MinHistoryDepths => 0;
|
||||
int IWatchlistIndicator.MinHistoryDepths => MinHistoryDepths;
|
||||
|
||||
public override string ShortName => $"JVOLTYN {Period}:{_sourceName}";
|
||||
public override string SourceCodeLink => "https://github.com/mihakralj/QuanTAlib/blob/main/lib/volatility/jvoltyn/Jvoltyn.Quantower.cs";
|
||||
|
||||
public JvoltynIndicator()
|
||||
{
|
||||
OnBackGround = true;
|
||||
SeparateWindow = true;
|
||||
_sourceName = Source.ToString();
|
||||
Name = "JVOLTYN - Normalized Jurik Volatility";
|
||||
Description = "Normalized Jurik Volatility maps the raw JVOLTY dynamic exponent to a 0-100 scale, where 0 represents minimum volatility and 100 represents maximum volatility.";
|
||||
|
||||
_series = new LineSeries(name: "JVOLTYN", color: IndicatorExtensions.Volatility, width: 2, style: LineStyle.Solid);
|
||||
AddLineSeries(_series);
|
||||
}
|
||||
|
||||
protected override void OnInit()
|
||||
{
|
||||
_jvoltyn = new Jvoltyn(Period);
|
||||
_sourceName = Source.ToString();
|
||||
_priceSelector = Source.GetPriceSelector();
|
||||
base.OnInit();
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
protected override void OnUpdate(UpdateArgs args)
|
||||
{
|
||||
var item = HistoricalData[Count - 1, SeekOriginHistory.Begin];
|
||||
TValue result = _jvoltyn.Update(new TValue(item.TimeLeft.Ticks, _priceSelector(item)), isNew: args.IsNewBar());
|
||||
_series.SetValue(result.Value, _jvoltyn.IsHot, ShowColdValues);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,709 @@
|
||||
namespace QuanTAlib.Tests;
|
||||
|
||||
public class JvoltynTests
|
||||
{
|
||||
private const double Tolerance = 1e-9;
|
||||
|
||||
private static TSeries GenerateTestData(int count = 100)
|
||||
{
|
||||
var gbm = new GBM(seed: 42);
|
||||
var bars = gbm.Fetch(count, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1));
|
||||
var series = new TSeries(count);
|
||||
for (int i = 0; i < bars.Count; i++)
|
||||
{
|
||||
series.Add(new TValue(bars[i].Time, bars[i].Close));
|
||||
}
|
||||
return series;
|
||||
}
|
||||
|
||||
// ============== Constructor & Parameter Validation ==============
|
||||
|
||||
[Fact]
|
||||
public void Constructor_ValidatesInput()
|
||||
{
|
||||
Assert.Throws<ArgumentOutOfRangeException>(() => new Jvoltyn(0));
|
||||
Assert.Throws<ArgumentOutOfRangeException>(() => new Jvoltyn(-1));
|
||||
|
||||
var jvoltyn = new Jvoltyn(10);
|
||||
Assert.NotNull(jvoltyn);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Constructor_SetsCorrectName()
|
||||
{
|
||||
var jvoltyn = new Jvoltyn(7);
|
||||
Assert.Equal("Jvoltyn(7)", jvoltyn.Name);
|
||||
Assert.True(jvoltyn.WarmupPeriod > 0);
|
||||
|
||||
var jvoltyn2 = new Jvoltyn(14);
|
||||
Assert.Equal("Jvoltyn(14)", jvoltyn2.Name);
|
||||
}
|
||||
|
||||
// ============== Basic Functionality ==============
|
||||
|
||||
[Fact]
|
||||
public void BasicCalculation_DoesNotCrash()
|
||||
{
|
||||
var jvoltyn = new Jvoltyn(10);
|
||||
var series = GenerateTestData(100);
|
||||
|
||||
foreach (var value in series)
|
||||
{
|
||||
jvoltyn.Update(value);
|
||||
}
|
||||
|
||||
Assert.True(double.IsFinite(jvoltyn.Last.Value));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Calc_ReturnsNormalizedValue()
|
||||
{
|
||||
var jvoltyn = new Jvoltyn(10);
|
||||
var input = new TValue(DateTime.UtcNow, 100.0);
|
||||
|
||||
Assert.InRange(jvoltyn.Last.Value, -Tolerance, Tolerance); // Initially zero
|
||||
|
||||
TValue result = jvoltyn.Update(input);
|
||||
|
||||
// First value should be 0 (normalized from d=1)
|
||||
Assert.Equal(0.0, result.Value, Tolerance);
|
||||
Assert.Equal(result.Value, jvoltyn.Last.Value, Tolerance);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void FirstValue_ReturnsZero()
|
||||
{
|
||||
var jvoltyn = new Jvoltyn(10);
|
||||
var input = new TValue(DateTime.UtcNow, 100.0);
|
||||
|
||||
TValue result = jvoltyn.Update(input);
|
||||
|
||||
// First bar returns 0 (normalized minimum volatility)
|
||||
Assert.Equal(0.0, result.Value, Tolerance);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void OutputRange_IsZeroToHundred()
|
||||
{
|
||||
var jvoltyn = new Jvoltyn(10);
|
||||
var series = GenerateTestData(500);
|
||||
|
||||
foreach (var value in series)
|
||||
{
|
||||
var result = jvoltyn.Update(value);
|
||||
// Output should be in [0, 100] range
|
||||
Assert.True(result.Value >= 0.0 - Tolerance, $"Value {result.Value} below 0");
|
||||
Assert.True(result.Value <= 100.0 + Tolerance, $"Value {result.Value} above 100");
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Properties_Accessible()
|
||||
{
|
||||
var jvoltyn = new Jvoltyn(10);
|
||||
|
||||
Assert.InRange(jvoltyn.Last.Value, -Tolerance, Tolerance); // Initially zero
|
||||
Assert.False(jvoltyn.IsHot);
|
||||
Assert.Contains("Jvoltyn", jvoltyn.Name, StringComparison.Ordinal);
|
||||
Assert.True(jvoltyn.WarmupPeriod > 0);
|
||||
|
||||
var input = new TValue(DateTime.UtcNow, 100.0);
|
||||
jvoltyn.Update(input);
|
||||
|
||||
// After first bar, value should be 0 (minimum volatility normalized)
|
||||
Assert.Equal(0.0, jvoltyn.Last.Value, Tolerance);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void BandProperties_Accessible()
|
||||
{
|
||||
var jvoltyn = new Jvoltyn(10);
|
||||
var input = new TValue(DateTime.UtcNow, 100.0);
|
||||
|
||||
jvoltyn.Update(input);
|
||||
|
||||
// After first bar, bands should be initialized to the input value
|
||||
Assert.Equal(100.0, jvoltyn.UpperBand, Tolerance);
|
||||
Assert.Equal(100.0, jvoltyn.LowerBand, Tolerance);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void RawVolatility_Accessible()
|
||||
{
|
||||
var jvoltyn = new Jvoltyn(10);
|
||||
var input = new TValue(DateTime.UtcNow, 100.0);
|
||||
|
||||
jvoltyn.Update(input);
|
||||
|
||||
// RawVolatility should be 1.0 (minimum) after first bar
|
||||
Assert.Equal(1.0, jvoltyn.RawVolatility, Tolerance);
|
||||
}
|
||||
|
||||
// ============== State Management & Bar Correction ==============
|
||||
|
||||
[Fact]
|
||||
public void Calc_IsNew_AcceptsParameter()
|
||||
{
|
||||
var jvoltyn = new Jvoltyn(10);
|
||||
var series = GenerateTestData(50);
|
||||
|
||||
// Feed enough values to build up volatility history
|
||||
for (int i = 0; i < 49; i++)
|
||||
{
|
||||
jvoltyn.Update(series[i], isNew: true);
|
||||
}
|
||||
double valueBefore = jvoltyn.Last.Value;
|
||||
|
||||
// Add one more value with isNew=true
|
||||
jvoltyn.Update(series[49], isNew: true);
|
||||
double valueAfter = jvoltyn.Last.Value;
|
||||
|
||||
// Both should be valid volatility values
|
||||
Assert.True(double.IsFinite(valueBefore));
|
||||
Assert.True(double.IsFinite(valueAfter));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Calc_IsNew_False_UpdatesValue()
|
||||
{
|
||||
var jvoltyn = new Jvoltyn(10);
|
||||
var series = GenerateTestData(50);
|
||||
|
||||
// Feed enough bars to have meaningful volatility
|
||||
for (int i = 0; i < 49; i++)
|
||||
{
|
||||
jvoltyn.Update(series[i], isNew: true);
|
||||
}
|
||||
|
||||
// Add one more value with isNew=true
|
||||
jvoltyn.Update(series[49], isNew: true);
|
||||
double beforeUpdate = jvoltyn.Last.Value;
|
||||
|
||||
// Update same bar with different value (isNew=false)
|
||||
var modifiedInput = new TValue(series[49].Time, series[49].Value + 50.0);
|
||||
jvoltyn.Update(modifiedInput, isNew: false);
|
||||
double afterUpdate = jvoltyn.Last.Value;
|
||||
|
||||
// Values should be different after the correction
|
||||
Assert.True(Math.Abs(beforeUpdate - afterUpdate) > Tolerance);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void IsNew_Consistency()
|
||||
{
|
||||
var jvoltyn = new Jvoltyn(10);
|
||||
var series = GenerateTestData(100);
|
||||
|
||||
// Feed first 99
|
||||
for (int i = 0; i < 99; i++)
|
||||
{
|
||||
jvoltyn.Update(series[i]);
|
||||
}
|
||||
|
||||
// Update with 100th point (isNew=true)
|
||||
jvoltyn.Update(series[99], true);
|
||||
|
||||
// Update with modified 100th point (isNew=false)
|
||||
var modifiedInput = new TValue(series[99].Time, series[99].Value + 50.0);
|
||||
double val2 = jvoltyn.Update(modifiedInput, false).Value;
|
||||
|
||||
// Create new instance and feed up to modified
|
||||
var jvoltyn2 = new Jvoltyn(10);
|
||||
for (int i = 0; i < 99; i++)
|
||||
{
|
||||
jvoltyn2.Update(series[i]);
|
||||
}
|
||||
double val3 = jvoltyn2.Update(modifiedInput, true).Value;
|
||||
|
||||
Assert.Equal(val3, val2, Tolerance);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void IterativeCorrections_RestoreToOriginalState()
|
||||
{
|
||||
var jvoltyn = new Jvoltyn(5);
|
||||
var series = GenerateTestData(20);
|
||||
|
||||
// Feed 10 new values
|
||||
TValue tenthValue = default;
|
||||
for (int i = 0; i < 10; i++)
|
||||
{
|
||||
tenthValue = series[i];
|
||||
jvoltyn.Update(tenthValue, isNew: true);
|
||||
}
|
||||
|
||||
// Remember state after 10 values
|
||||
double stateAfterTen = jvoltyn.Last.Value;
|
||||
|
||||
// Generate 9 corrections with isNew=false (different values)
|
||||
for (int i = 10; i < 19; i++)
|
||||
{
|
||||
jvoltyn.Update(series[i], isNew: false);
|
||||
}
|
||||
|
||||
// Feed the remembered 10th value again with isNew=false
|
||||
TValue finalResult = jvoltyn.Update(tenthValue, isNew: false);
|
||||
|
||||
// State should match the original state after 10 values
|
||||
Assert.Equal(stateAfterTen, finalResult.Value, Tolerance);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Reset_Works()
|
||||
{
|
||||
var jvoltyn = new Jvoltyn(10);
|
||||
var series = GenerateTestData(50);
|
||||
|
||||
foreach (var value in series)
|
||||
{
|
||||
jvoltyn.Update(value);
|
||||
}
|
||||
|
||||
jvoltyn.Reset();
|
||||
Assert.InRange(jvoltyn.Last.Value, -Tolerance, Tolerance); // Reset to zero
|
||||
Assert.False(jvoltyn.IsHot);
|
||||
|
||||
// After reset, first value should be 0 (minimum normalized volatility)
|
||||
jvoltyn.Update(series[0]);
|
||||
Assert.Equal(0.0, jvoltyn.Last.Value, Tolerance);
|
||||
}
|
||||
|
||||
// ============== Warmup & Convergence ==============
|
||||
|
||||
[Fact]
|
||||
public void IsHot_BecomesTrueAfterWarmup()
|
||||
{
|
||||
var jvoltyn = new Jvoltyn(5);
|
||||
|
||||
Assert.False(jvoltyn.IsHot);
|
||||
|
||||
var series = GenerateTestData(200);
|
||||
|
||||
int steps = 0;
|
||||
while (!jvoltyn.IsHot && steps < series.Count)
|
||||
{
|
||||
jvoltyn.Update(series[steps]);
|
||||
steps++;
|
||||
}
|
||||
|
||||
Assert.True(jvoltyn.IsHot);
|
||||
Assert.True(steps > 0);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void WarmupPeriod_IsPositive()
|
||||
{
|
||||
var jvoltyn = new Jvoltyn(10);
|
||||
Assert.True(jvoltyn.WarmupPeriod > 0);
|
||||
|
||||
var jvoltyn2 = new Jvoltyn(20);
|
||||
Assert.True(jvoltyn2.WarmupPeriod > 0);
|
||||
|
||||
// WarmupPeriod should increase with the period parameter
|
||||
Assert.True(jvoltyn2.WarmupPeriod >= jvoltyn.WarmupPeriod);
|
||||
}
|
||||
|
||||
// ============== NaN/Infinity Handling ==============
|
||||
|
||||
[Fact]
|
||||
public void NaN_Input_UsesLastValidValue()
|
||||
{
|
||||
var jvoltyn = new Jvoltyn(5);
|
||||
|
||||
var input1 = new TValue(DateTime.UtcNow, 100.0);
|
||||
jvoltyn.Update(input1);
|
||||
|
||||
var input2 = new TValue(DateTime.UtcNow.AddMinutes(1), 110.0);
|
||||
jvoltyn.Update(input2);
|
||||
|
||||
// Feed NaN value
|
||||
var inputWithNaN = new TValue(DateTime.UtcNow.AddMinutes(2), double.NaN);
|
||||
var resultAfterNaN = jvoltyn.Update(inputWithNaN);
|
||||
|
||||
// Result should be finite
|
||||
Assert.True(double.IsFinite(resultAfterNaN.Value));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Infinity_Input_UsesLastValidValue()
|
||||
{
|
||||
var jvoltyn = new Jvoltyn(5);
|
||||
|
||||
var input1 = new TValue(DateTime.UtcNow, 100.0);
|
||||
jvoltyn.Update(input1);
|
||||
|
||||
var input2 = new TValue(DateTime.UtcNow.AddMinutes(1), 110.0);
|
||||
jvoltyn.Update(input2);
|
||||
|
||||
// Feed Infinity value
|
||||
var inputWithInf = new TValue(DateTime.UtcNow.AddMinutes(2), double.PositiveInfinity);
|
||||
var resultAfterInf = jvoltyn.Update(inputWithInf);
|
||||
|
||||
// Result should be finite
|
||||
Assert.True(double.IsFinite(resultAfterInf.Value));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void BatchNaN_Safe()
|
||||
{
|
||||
var jvoltyn = new Jvoltyn(5);
|
||||
var series = GenerateTestData(20);
|
||||
|
||||
// Feed some values
|
||||
for (int i = 0; i < 10; i++)
|
||||
{
|
||||
jvoltyn.Update(series[i]);
|
||||
}
|
||||
|
||||
// Feed multiple NaN values
|
||||
for (int i = 0; i < 5; i++)
|
||||
{
|
||||
var nanInput = new TValue(DateTime.UtcNow.AddMinutes(10 + i), double.NaN);
|
||||
var result = jvoltyn.Update(nanInput);
|
||||
Assert.True(double.IsFinite(result.Value));
|
||||
}
|
||||
}
|
||||
|
||||
// ============== Consistency Tests ==============
|
||||
|
||||
[Fact]
|
||||
public void BatchCalc_MatchesIterativeCalc()
|
||||
{
|
||||
var jvoltynIterative = new Jvoltyn(10);
|
||||
var series = GenerateTestData(100);
|
||||
|
||||
// Calculate iteratively
|
||||
var iterativeResults = new TSeries();
|
||||
foreach (var value in series)
|
||||
{
|
||||
iterativeResults.Add(jvoltynIterative.Update(value));
|
||||
}
|
||||
|
||||
// Calculate batch
|
||||
var batchResults = Jvoltyn.Batch(series, 10);
|
||||
|
||||
// Compare
|
||||
Assert.Equal(iterativeResults.Count, batchResults.Count);
|
||||
for (int i = 0; i < iterativeResults.Count; i++)
|
||||
{
|
||||
Assert.Equal(iterativeResults[i].Value, batchResults[i].Value, Tolerance);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void TSeries_Update_MatchesStreaming()
|
||||
{
|
||||
var jvoltyn1 = new Jvoltyn(10);
|
||||
var jvoltyn2 = new Jvoltyn(10);
|
||||
var series = GenerateTestData(100);
|
||||
|
||||
// Streaming
|
||||
foreach (var value in series)
|
||||
{
|
||||
jvoltyn1.Update(value);
|
||||
}
|
||||
|
||||
// Batch
|
||||
jvoltyn2.Update(series);
|
||||
|
||||
Assert.Equal(jvoltyn1.Last.Value, jvoltyn2.Last.Value, Tolerance);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SpanCalc_MatchesStreaming()
|
||||
{
|
||||
var jvoltyn = new Jvoltyn(10);
|
||||
var series = GenerateTestData(100);
|
||||
|
||||
// Stream all values first
|
||||
foreach (var value in series)
|
||||
{
|
||||
jvoltyn.Update(value);
|
||||
}
|
||||
double streamingLast = jvoltyn.Last.Value;
|
||||
|
||||
// Span calculation
|
||||
var output = new double[series.Count];
|
||||
Jvoltyn.Calculate(series.Values, output, 10);
|
||||
|
||||
// Compare last value (after warmup)
|
||||
Assert.Equal(streamingLast, output[series.Count - 1], 1e-6);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Chainability_Works()
|
||||
{
|
||||
var jvoltyn = new Jvoltyn(10);
|
||||
var series = GenerateTestData(50);
|
||||
|
||||
var result = jvoltyn.Update(series);
|
||||
Assert.Equal(50, result.Count);
|
||||
Assert.Equal(jvoltyn.Last.Value, result.Last.Value);
|
||||
}
|
||||
|
||||
// ============== Normalization Validation ==============
|
||||
|
||||
[Fact]
|
||||
public void NormalizedOutput_MatchesJvoltyTransformation()
|
||||
{
|
||||
var jvolty = new Jvolty(10);
|
||||
var jvoltyn = new Jvoltyn(10);
|
||||
var series = GenerateTestData(100);
|
||||
|
||||
// Feed both with same data
|
||||
foreach (var value in series)
|
||||
{
|
||||
jvolty.Update(value);
|
||||
jvoltyn.Update(value);
|
||||
}
|
||||
|
||||
// Jvoltyn output should be (Jvolty - 1) * 100 / (logParam - 1)
|
||||
// RawVolatility property gives us the raw d value
|
||||
double rawD = jvoltyn.RawVolatility;
|
||||
double expectedJvolty = jvolty.Last.Value;
|
||||
|
||||
// They should have the same raw d value
|
||||
Assert.Equal(expectedJvolty, rawD, Tolerance);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void FlatValues_ReturnsZero()
|
||||
{
|
||||
var jvoltyn = new Jvoltyn(5);
|
||||
|
||||
// All values are the same
|
||||
for (int i = 0; i < 50; i++)
|
||||
{
|
||||
var input = new TValue(DateTime.UtcNow.AddMinutes(i), 100.0);
|
||||
jvoltyn.Update(input);
|
||||
}
|
||||
|
||||
// Normalized volatility should be 0 for flat values (d=1 -> normalized=0)
|
||||
Assert.Equal(0.0, jvoltyn.Last.Value, Tolerance);
|
||||
}
|
||||
|
||||
// ============== Static Batch Method ==============
|
||||
|
||||
[Fact]
|
||||
public void StaticBatch_Works()
|
||||
{
|
||||
var series = GenerateTestData(50);
|
||||
|
||||
var results = Jvoltyn.Batch(series, 10);
|
||||
|
||||
Assert.Equal(50, results.Count);
|
||||
Assert.True(double.IsFinite(results.Last.Value));
|
||||
}
|
||||
|
||||
// ============== Span API Tests ==============
|
||||
|
||||
[Fact]
|
||||
public void Calculate_ValidatesLengths()
|
||||
{
|
||||
var source = new double[10];
|
||||
var output = new double[5]; // Wrong size
|
||||
|
||||
var ex = Assert.Throws<ArgumentException>(() => Jvoltyn.Calculate(source, output, 10));
|
||||
Assert.Equal("output", ex.ParamName);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Calculate_EmptySource_NoException()
|
||||
{
|
||||
var source = Array.Empty<double>();
|
||||
var output = Array.Empty<double>();
|
||||
|
||||
var exception = Record.Exception(() => Jvoltyn.Calculate(source, output, 10));
|
||||
Assert.Null(exception);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Calculate_InvalidPeriod_ThrowsArgumentException()
|
||||
{
|
||||
var source = new double[10];
|
||||
var output = new double[10];
|
||||
|
||||
Assert.Throws<ArgumentOutOfRangeException>(() => Jvoltyn.Calculate(source, output, 0));
|
||||
}
|
||||
|
||||
// ============== Edge Cases ==============
|
||||
|
||||
[Fact]
|
||||
public void SingleValue_ReturnsZero()
|
||||
{
|
||||
var jvoltyn = new Jvoltyn(10);
|
||||
var input = new TValue(DateTime.UtcNow, 100.0);
|
||||
|
||||
var result = jvoltyn.Update(input);
|
||||
|
||||
Assert.True(double.IsFinite(result.Value));
|
||||
Assert.Equal(0.0, result.Value, Tolerance); // First bar = normalized 0
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Period1_Works()
|
||||
{
|
||||
var jvoltyn = new Jvoltyn(1);
|
||||
var series = GenerateTestData(10);
|
||||
|
||||
foreach (var value in series)
|
||||
{
|
||||
var result = jvoltyn.Update(value);
|
||||
Assert.True(double.IsFinite(result.Value));
|
||||
Assert.True(result.Value >= 0.0 - Tolerance);
|
||||
Assert.True(result.Value <= 100.0 + Tolerance);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void HighVolatility_IncreasesValue()
|
||||
{
|
||||
var jvoltyn = new Jvoltyn(10);
|
||||
|
||||
// Start with stable values
|
||||
for (int i = 0; i < 20; i++)
|
||||
{
|
||||
var input = new TValue(DateTime.UtcNow.AddMinutes(i), 100.0 + (i * 0.1));
|
||||
jvoltyn.Update(input);
|
||||
}
|
||||
|
||||
double lowVolatility = jvoltyn.Last.Value;
|
||||
|
||||
// Create high volatility spike
|
||||
var spike = new TValue(DateTime.UtcNow.AddMinutes(21), 150.0);
|
||||
jvoltyn.Update(spike);
|
||||
|
||||
double highVolatility = jvoltyn.Last.Value;
|
||||
|
||||
// High volatility should produce higher normalized value
|
||||
Assert.True(highVolatility > lowVolatility);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Bands_TrackPrice()
|
||||
{
|
||||
var jvoltyn = new Jvoltyn(10);
|
||||
|
||||
// Feed increasing prices
|
||||
for (int i = 0; i < 20; i++)
|
||||
{
|
||||
var input = new TValue(DateTime.UtcNow.AddMinutes(i), 100.0 + i);
|
||||
jvoltyn.Update(input);
|
||||
}
|
||||
|
||||
// Upper band should track the highest recent prices
|
||||
Assert.True(jvoltyn.UpperBand > 100.0);
|
||||
// Lower band should lag behind due to adaptive decay
|
||||
Assert.True(jvoltyn.LowerBand < jvoltyn.UpperBand);
|
||||
}
|
||||
|
||||
// ============== Event Publishing ==============
|
||||
|
||||
[Fact]
|
||||
public void PubEvent_Fires()
|
||||
{
|
||||
var jvoltyn = new Jvoltyn(10);
|
||||
bool eventFired = false;
|
||||
|
||||
jvoltyn.Pub += (object? sender, in TValueEventArgs args) => eventFired = true;
|
||||
|
||||
var input = new TValue(DateTime.UtcNow, 100.0);
|
||||
jvoltyn.Update(input);
|
||||
|
||||
Assert.True(eventFired);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void EventChaining_Works()
|
||||
{
|
||||
var series = GenerateTestData(50);
|
||||
|
||||
var jvoltyn = new Jvoltyn(10);
|
||||
var sma = new Sma(jvoltyn, 5); // Chain SMA to Jvoltyn output
|
||||
|
||||
foreach (var value in series)
|
||||
{
|
||||
jvoltyn.Update(value);
|
||||
}
|
||||
|
||||
Assert.True(double.IsFinite(sma.Last.Value));
|
||||
}
|
||||
|
||||
// ============== Additional Tests ==============
|
||||
|
||||
[Fact]
|
||||
public void LargeDataset_Completes()
|
||||
{
|
||||
var jvoltyn = new Jvoltyn(20);
|
||||
var series = GenerateTestData(5000);
|
||||
|
||||
foreach (var value in series)
|
||||
{
|
||||
jvoltyn.Update(value);
|
||||
}
|
||||
|
||||
Assert.True(jvoltyn.IsHot);
|
||||
Assert.True(double.IsFinite(jvoltyn.Last.Value));
|
||||
Assert.True(jvoltyn.Last.Value >= 0.0);
|
||||
Assert.True(jvoltyn.Last.Value <= 100.0);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void DifferentPeriods_ProduceValidValues()
|
||||
{
|
||||
var series = GenerateTestData(200);
|
||||
|
||||
var jvoltyn1 = new Jvoltyn(5);
|
||||
var jvoltyn2 = new Jvoltyn(10);
|
||||
var jvoltyn3 = new Jvoltyn(20);
|
||||
|
||||
foreach (var value in series)
|
||||
{
|
||||
jvoltyn1.Update(value);
|
||||
jvoltyn2.Update(value);
|
||||
jvoltyn3.Update(value);
|
||||
}
|
||||
|
||||
Assert.True(double.IsFinite(jvoltyn1.Last.Value));
|
||||
Assert.True(double.IsFinite(jvoltyn2.Last.Value));
|
||||
Assert.True(double.IsFinite(jvoltyn3.Last.Value));
|
||||
Assert.True(jvoltyn1.Last.Value >= 0.0);
|
||||
Assert.True(jvoltyn2.Last.Value >= 0.0);
|
||||
Assert.True(jvoltyn3.Last.Value >= 0.0);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SourceChaining_Works()
|
||||
{
|
||||
var series = GenerateTestData(200);
|
||||
|
||||
// Create source TSeries that publishes events
|
||||
var sourceSeries = new TSeries();
|
||||
var jvoltyn = new Jvoltyn(sourceSeries, 10);
|
||||
|
||||
// Feed data through the source (need enough for warmup)
|
||||
foreach (var value in series)
|
||||
{
|
||||
sourceSeries.Add(value);
|
||||
}
|
||||
|
||||
// Should have valid output
|
||||
Assert.True(double.IsFinite(jvoltyn.Last.Value));
|
||||
Assert.True(jvoltyn.Last.Value >= 0.0); // Minimum normalized volatility
|
||||
}
|
||||
|
||||
#pragma warning disable S2699 // Test contains Assert.True and Assert.InRange - analyzer false positive
|
||||
[Fact]
|
||||
public void Prime_Works()
|
||||
{
|
||||
var jvoltyn = new Jvoltyn(5);
|
||||
var values = new double[] { 100.0, 101.0, 99.5, 102.0, 98.0, 103.0 };
|
||||
|
||||
jvoltyn.Prime(values);
|
||||
|
||||
double lastValue = jvoltyn.Last.Value;
|
||||
Assert.True(double.IsFinite(lastValue), "Last value should be finite after Prime");
|
||||
Assert.InRange(lastValue, 0.0, 100.0); // Normalized volatility in [0, 100]
|
||||
}
|
||||
#pragma warning restore S2699
|
||||
}
|
||||
@@ -0,0 +1,400 @@
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Runtime.InteropServices;
|
||||
|
||||
namespace QuanTAlib;
|
||||
|
||||
/// <summary>
|
||||
/// Jvoltyn: Normalized Jurik Volatility
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Normalized version of Jvolty that maps the dynamic exponent to a 0-100 scale.
|
||||
/// Output of 0 indicates minimum volatility, 100 indicates maximum volatility.
|
||||
///
|
||||
/// Normalization: <c>Jvoltyn = ((d - 1) / (logParam - 1)) × 100</c>
|
||||
/// where d is the raw Jurik dynamic exponent in range [1, logParam].
|
||||
///
|
||||
/// Key features:
|
||||
/// - Same adaptive volatility calculation as Jvolty
|
||||
/// - Output normalized to 0-100 for easy interpretation
|
||||
/// - 0 = low volatility regime, 100 = high volatility regime
|
||||
/// </remarks>
|
||||
/// <seealso href="Jvoltyn.md">Detailed documentation</seealso>
|
||||
/// <seealso cref="Jvolty">Raw Jurik Volatility indicator</seealso>
|
||||
[SkipLocalsInit]
|
||||
public sealed class Jvoltyn : AbstractBase
|
||||
{
|
||||
private const int VolWindowSize = 128; // volatility history length
|
||||
private const int DevWindowSize = 10; // short SMA length for deviation
|
||||
private const int JurikTrimCount = 65; // canonical JMA: middle 65 of 128 samples
|
||||
|
||||
// Jurik core parameters derived from period
|
||||
private readonly double _logParam; // log(sqrt(L))/log(2) + 2, clamped >= 0
|
||||
private readonly double _pExponent; // max(logParam - 2, 0.5)
|
||||
private readonly double _sqrtDivider; // sqrt(L)*logParam / (sqrt(L)*logParam + 1)
|
||||
private readonly double _normFactor; // 100 / (logParam - 1) for fast normalization
|
||||
|
||||
// Buffers
|
||||
private readonly RingBuffer _devBuffer;
|
||||
private readonly RingBuffer _volBuffer;
|
||||
private readonly TValuePublishedHandler _handler;
|
||||
private readonly ITValuePublisher? _source;
|
||||
|
||||
// Streaming state (current + previous snapshot for isNew=false)
|
||||
private State _s;
|
||||
private State _ps;
|
||||
|
||||
[StructLayout(LayoutKind.Auto)]
|
||||
private record struct State
|
||||
{
|
||||
// Jurik "envelope" anchors (volatility bands)
|
||||
public double UpperBand;
|
||||
public double LowerBand;
|
||||
|
||||
// last finite price (for NaN handling)
|
||||
public double LastPrice;
|
||||
|
||||
// last computed raw volatility (d value)
|
||||
public double LastVolty;
|
||||
|
||||
// counters
|
||||
public int Bars;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the upper volatility band value.
|
||||
/// </summary>
|
||||
public double UpperBand => _s.UpperBand;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the lower volatility band value.
|
||||
/// </summary>
|
||||
public double LowerBand => _s.LowerBand;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the raw (non-normalized) Jurik volatility value in range [1, logParam].
|
||||
/// </summary>
|
||||
public double RawVolatility => _s.LastVolty;
|
||||
|
||||
public override bool IsHot => _s.Bars >= WarmupPeriod;
|
||||
|
||||
/// <summary>
|
||||
/// Creates Jvoltyn with specified period.
|
||||
/// </summary>
|
||||
/// <param name="period">Period for volatility calculation (must be >= 1)</param>
|
||||
public Jvoltyn(int period)
|
||||
{
|
||||
if (period < 1)
|
||||
{
|
||||
throw new ArgumentOutOfRangeException(nameof(period), "Period must be >= 1.");
|
||||
}
|
||||
|
||||
// --- Length / log / divider parameters (from decompiled JMA) ---
|
||||
// L_raw ~ (period - 1)/2, with a tiny lower bound to avoid log(0)
|
||||
double lengthParam = period < 1.0000000002
|
||||
? 0.0000000001
|
||||
: (period - 1.0) / 2.0;
|
||||
|
||||
double logParam = Math.Log(Math.Sqrt(lengthParam)) / Math.Log(2.0);
|
||||
logParam = (logParam + 2.0) < 0.0 ? 0.0 : (logParam + 2.0);
|
||||
_logParam = logParam;
|
||||
_pExponent = Math.Max(_logParam - 2.0, 0.5);
|
||||
|
||||
double sqrtParam = Math.Sqrt(lengthParam) * _logParam;
|
||||
_sqrtDivider = sqrtParam / (sqrtParam + 1.0);
|
||||
|
||||
// Normalization factor: maps [1, logParam] -> [0, 100]
|
||||
// Avoid division by zero when logParam == 1
|
||||
_normFactor = Math.Abs(_logParam - 1.0) > 1e-10 ? 100.0 / (_logParam - 1.0) : 0.0;
|
||||
|
||||
// same warmup heuristic used in JMA
|
||||
WarmupPeriod = (int)Math.Ceiling(20.0 + 80.0 * Math.Pow(period, 0.36));
|
||||
|
||||
_handler = Handle;
|
||||
Name = $"Jvoltyn({period})";
|
||||
|
||||
_devBuffer = new RingBuffer(DevWindowSize);
|
||||
_volBuffer = new RingBuffer(VolWindowSize);
|
||||
|
||||
Reset();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates Jvoltyn with specified source and period.
|
||||
/// </summary>
|
||||
/// <param name="source">Source to subscribe to</param>
|
||||
/// <param name="period">Period for volatility calculation</param>
|
||||
public Jvoltyn(ITValuePublisher source, int period)
|
||||
: this(period)
|
||||
{
|
||||
_source = source;
|
||||
source.Pub += _handler;
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public override void Reset()
|
||||
{
|
||||
_s = default;
|
||||
_ps = default;
|
||||
_devBuffer.Clear();
|
||||
_volBuffer.Clear();
|
||||
Last = default;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Core streaming step: feed a single value, get normalized Jvoltyn (0-100).
|
||||
/// Honors isNew semantics by snapshotting state+buffers.
|
||||
/// </summary>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
private double Step(double value, bool isNew)
|
||||
{
|
||||
HandleStateSnapshot(isNew);
|
||||
if (!double.IsFinite(value))
|
||||
{
|
||||
if (_s.Bars == 0)
|
||||
{
|
||||
return double.NaN;
|
||||
}
|
||||
|
||||
value = _s.LastPrice;
|
||||
}
|
||||
else
|
||||
{
|
||||
_s.LastPrice = value;
|
||||
}
|
||||
|
||||
_s.Bars++;
|
||||
if (_s.Bars == 1)
|
||||
{
|
||||
return InitializeFirstBar(value);
|
||||
}
|
||||
|
||||
return CalculateJvoltyn(value);
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
private void HandleStateSnapshot(bool isNew)
|
||||
{
|
||||
if (isNew)
|
||||
{
|
||||
_ps = _s;
|
||||
_devBuffer.Snapshot();
|
||||
_volBuffer.Snapshot();
|
||||
}
|
||||
else
|
||||
{
|
||||
_s = _ps;
|
||||
_devBuffer.Restore();
|
||||
_volBuffer.Restore();
|
||||
}
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
private double InitializeFirstBar(double value)
|
||||
{
|
||||
_s.UpperBand = value;
|
||||
_s.LowerBand = value;
|
||||
_s.LastVolty = 1.0; // minimum volatility
|
||||
return 0.0; // Normalized: d=1 maps to 0
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
private double CalculateJvoltyn(double value)
|
||||
{
|
||||
// 1. Local deviation: |price - {UpperBand, LowerBand}|
|
||||
double diffA = value - _s.UpperBand;
|
||||
double diffB = value - _s.LowerBand;
|
||||
double absA = Math.Abs(diffA);
|
||||
double absB = Math.Abs(diffB);
|
||||
double absValue = absA > absB ? absA : absB;
|
||||
double deviation = absValue + 1e-10;
|
||||
|
||||
// 2. 10-bar SMA of local deviation -> "volatility"
|
||||
_devBuffer.Add(deviation);
|
||||
double volatility = _devBuffer.Average;
|
||||
|
||||
// 3. 128-bar volatility history + middle-65 trimmed mean
|
||||
_volBuffer.Add(volatility);
|
||||
double refVolatility = CalculateTrimmedMean(volatility);
|
||||
refVolatility = refVolatility <= 0.0 ? deviation : refVolatility;
|
||||
|
||||
// 4. Jurik dynamic exponent d from abs/refVolatility
|
||||
double d = CalculateJurikExponent(absValue, refVolatility);
|
||||
|
||||
// 5. Update UpperBand / LowerBand using sqrtDivider ^ sqrt(d)
|
||||
UpdateBands(value, d);
|
||||
|
||||
_s.LastVolty = d;
|
||||
|
||||
// 6. Normalize d from [1, logParam] to [0, 100]
|
||||
return (d - 1.0) * _normFactor;
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
private double CalculateJurikExponent(double absValue, double refVolatility)
|
||||
{
|
||||
double ratio = Math.Max(absValue / refVolatility, 0.0);
|
||||
double d = Math.Pow(ratio, _pExponent);
|
||||
if (d > _logParam)
|
||||
{
|
||||
d = _logParam;
|
||||
}
|
||||
|
||||
if (d < 1.0)
|
||||
{
|
||||
d = 1.0;
|
||||
}
|
||||
|
||||
return d;
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
private void UpdateBands(double value, double d)
|
||||
{
|
||||
double adapt = Math.Pow(_sqrtDivider, Math.Sqrt(d));
|
||||
_s.UpperBand = (value > _s.UpperBand)
|
||||
? value
|
||||
: Math.FusedMultiplyAdd(adapt, _s.UpperBand - value, value);
|
||||
_s.LowerBand = (value < _s.LowerBand)
|
||||
? value
|
||||
: Math.FusedMultiplyAdd(adapt, _s.LowerBand - value, value);
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public override TValue Update(TValue input, bool isNew = true)
|
||||
{
|
||||
double volty = Step(input.Value, isNew);
|
||||
Last = new TValue(input.Time, volty);
|
||||
PubEvent(Last, isNew);
|
||||
return Last;
|
||||
}
|
||||
|
||||
public override TSeries Update(TSeries 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);
|
||||
|
||||
source.Times.CopyTo(tSpan);
|
||||
|
||||
Reset();
|
||||
for (int i = 0; i < len; i++)
|
||||
{
|
||||
vSpan[i] = Step(source.Values[i], isNew: true);
|
||||
}
|
||||
|
||||
// Synchronize previous-state mirror to current state AND snapshot buffers
|
||||
_ps = _s;
|
||||
_devBuffer.Snapshot();
|
||||
_volBuffer.Snapshot();
|
||||
|
||||
Last = new TValue(tSpan[len - 1], vSpan[len - 1]);
|
||||
return new TSeries(t, v);
|
||||
}
|
||||
|
||||
private void Handle(object? sender, in TValueEventArgs args) => Update(args.Value, args.IsNew);
|
||||
|
||||
protected override void Dispose(bool disposing)
|
||||
{
|
||||
if (disposing && _source != null)
|
||||
{
|
||||
_source.Pub -= _handler;
|
||||
}
|
||||
base.Dispose(disposing);
|
||||
}
|
||||
|
||||
public override void Prime(ReadOnlySpan<double> source, TimeSpan? step = null)
|
||||
{
|
||||
foreach (var value in source)
|
||||
{
|
||||
Update(new TValue(DateTime.MinValue, value));
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Calculates Jvoltyn for the entire series using a new instance.
|
||||
/// </summary>
|
||||
public static TSeries Batch(TSeries source, int period)
|
||||
{
|
||||
var jvoltyn = new Jvoltyn(period);
|
||||
return jvoltyn.Update(source);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Static helper for span-based calculation.
|
||||
/// </summary>
|
||||
public static void Calculate(ReadOnlySpan<double> source,
|
||||
Span<double> output,
|
||||
int period)
|
||||
{
|
||||
if (output.Length != source.Length)
|
||||
{
|
||||
throw new ArgumentException("Source and output must have the same length.", nameof(output));
|
||||
}
|
||||
|
||||
if (source.Length == 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var jvoltyn = new Jvoltyn(period);
|
||||
for (int i = 0; i < source.Length; i++)
|
||||
{
|
||||
output[i] = jvoltyn.Step(source[i], isNew: true);
|
||||
}
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
private double CalculateTrimmedMean(double fallback)
|
||||
{
|
||||
int count = _volBuffer.Count;
|
||||
if (count < 16)
|
||||
{
|
||||
return fallback;
|
||||
}
|
||||
|
||||
// Stack-allocate scratch buffer for sorting (max 128 * 8 bytes = 1KB)
|
||||
Span<double> sorted = stackalloc double[count];
|
||||
_volBuffer.CopyTo(sorted);
|
||||
sorted.Sort();
|
||||
|
||||
int start, end;
|
||||
if (count >= VolWindowSize)
|
||||
{
|
||||
// canonical JMA: central 65 of 128 -> indices 32..96
|
||||
int leftSkip = (int)Math.Ceiling((VolWindowSize - JurikTrimCount) / 2.0);
|
||||
start = leftSkip;
|
||||
end = start + JurikTrimCount - 1;
|
||||
}
|
||||
else
|
||||
{
|
||||
// for shorter history, use central ~50% as a reasonable proxy
|
||||
int slice = (int)Math.Max(5, Math.Round(count * 0.5));
|
||||
int drop = (count - slice) / 2;
|
||||
start = drop;
|
||||
end = drop + slice - 1;
|
||||
}
|
||||
|
||||
if (start < 0)
|
||||
{
|
||||
start = 0;
|
||||
}
|
||||
|
||||
if (end >= count)
|
||||
{
|
||||
end = count - 1;
|
||||
}
|
||||
|
||||
int len = end - start + 1;
|
||||
return sorted.Slice(start, len).SumSIMD() / len;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,215 @@
|
||||
# JVOLTYN: Normalized Jurik Volatility
|
||||
|
||||
> "When you need to compare apples to apples, normalize your volatility—0 is calm, 100 is chaos."
|
||||
|
||||
Normalized Jurik Volatility (JVOLTYN) maps the raw JVOLTY dynamic exponent to a 0-100 scale. While JVOLTY outputs values in the range [1, logParam] (where logParam is period-dependent), JVOLTYN transforms this to a universal scale where 0 represents minimum volatility and 100 represents maximum volatility. This normalization enables direct comparison across different periods and instruments.
|
||||
|
||||
## Historical Context
|
||||
|
||||
JVOLTY extracts the adaptive volatility component from Mark Jurik's JMA algorithm. The raw output—a dynamic exponent clamped between 1.0 and logParam—is meaningful within the JMA context but awkward for standalone analysis. A period-7 JVOLTY might reach 3.5 at maximum while a period-50 JVOLTY peaks at 4.8. Comparing these raw values across instruments or timeframes requires mental gymnastics.
|
||||
|
||||
JVOLTYN applies a simple linear normalization that maps the entire valid range to [0, 100]. Now a reading of 50 means "halfway between minimum and maximum volatility" regardless of the underlying period. This makes JVOLTYN suitable for:
|
||||
|
||||
- Cross-instrument volatility comparisons
|
||||
- Threshold-based strategy rules (e.g., "if volatility > 60, reduce position size")
|
||||
- Regime classification (low: 0-30, medium: 30-70, high: 70-100)
|
||||
- Heatmap visualization across a portfolio
|
||||
|
||||
The normalization is mathematically trivial but practically essential for systematic trading applications.
|
||||
|
||||
## Architecture & Physics
|
||||
|
||||
JVOLTYN wraps the complete JVOLTY algorithm and applies a final normalization step.
|
||||
|
||||
### 1. Core JVOLTY Computation
|
||||
|
||||
The full JVOLTY pipeline executes:
|
||||
|
||||
1. **Adaptive Envelope**: Tracks price extremes with volatility-adjusted decay
|
||||
2. **Local Deviation**: Measures distance from adaptive bands
|
||||
3. **Short Volatility**: 10-bar SMA of local deviation
|
||||
4. **Distribution Buffer**: 128-sample circular buffer with trimmed mean
|
||||
5. **Dynamic Exponent**: Ratio of current to reference volatility, raised to adaptive power
|
||||
|
||||
See [JVOLTY documentation](../jvolty/Jvolty.md) for complete algorithmic details.
|
||||
|
||||
### 2. Normalization Transform
|
||||
|
||||
The raw JVOLTY output $d_t \in [1, \text{logParam}]$ is normalized:
|
||||
|
||||
$$
|
||||
\text{JVOLTYN}_t = \frac{(d_t - 1)}{\text{logParam} - 1} \times 100
|
||||
$$
|
||||
|
||||
where:
|
||||
- $\text{logParam} = \max(\log_2(\sqrt{L}) + 2, 0)$
|
||||
- $L = (N - 1) / 2$
|
||||
- $N$ is the period
|
||||
|
||||
**Boundary conditions:**
|
||||
- $d_t = 1$ → JVOLTYN = 0 (minimum volatility)
|
||||
- $d_t = \text{logParam}$ → JVOLTYN = 100 (maximum volatility)
|
||||
|
||||
### 3. Precomputed Normalization Factor
|
||||
|
||||
For efficiency, the normalization factor is computed once in the constructor:
|
||||
|
||||
$$
|
||||
\text{normFactor} = \frac{100}{\text{logParam} - 1}
|
||||
$$
|
||||
|
||||
Then each update simply computes:
|
||||
|
||||
$$
|
||||
\text{JVOLTYN}_t = (d_t - 1) \times \text{normFactor}
|
||||
$$
|
||||
|
||||
This avoids repeated division in the hot path.
|
||||
|
||||
## Mathematical Foundation
|
||||
|
||||
### Period-Dependent Scaling
|
||||
|
||||
The logParam value determines the raw JVOLTY range:
|
||||
|
||||
| Period | L | logParam | Max Raw JVOLTY | Norm Factor |
|
||||
| :---: | :---: | :---: | :---: | :---: |
|
||||
| 5 | 2.0 | 3.00 | 3.00 | 50.00 |
|
||||
| 7 | 3.0 | 3.29 | 3.29 | 43.64 |
|
||||
| 10 | 4.5 | 3.58 | 3.58 | 38.76 |
|
||||
| 14 | 6.5 | 3.85 | 3.85 | 35.09 |
|
||||
| 20 | 9.5 | 4.12 | 4.12 | 32.05 |
|
||||
| 50 | 24.5 | 4.78 | 4.78 | 26.46 |
|
||||
| 100 | 49.5 | 5.28 | 5.28 | 23.36 |
|
||||
|
||||
Longer periods have larger logParam values, meaning the raw JVOLTY has more "headroom" before hitting maximum. The normalization factor compensates for this, ensuring that 100 always represents maximum possible volatility for the given period.
|
||||
|
||||
### Edge Case: Very Short Periods
|
||||
|
||||
For period = 2 or 3, logParam approaches small values:
|
||||
- Period 2: L = 0.5, logParam = max(log₂(0.707) + 2, 0) ≈ 1.5
|
||||
- Period 3: L = 1.0, logParam = max(log₂(1.0) + 2, 0) = 2.0
|
||||
|
||||
The normalization handles these correctly, though such short periods provide limited statistical significance.
|
||||
|
||||
### RawVolatility Property
|
||||
|
||||
JVOLTYN exposes the underlying raw JVOLTY value via the `RawVolatility` property. This allows users to access both:
|
||||
- `Last.Value` → Normalized [0, 100] output
|
||||
- `RawVolatility` → Raw [1, logParam] value
|
||||
|
||||
Useful when the normalized value is needed for display but the raw value is needed for JMA adaptation or other calculations.
|
||||
|
||||
## Performance Profile
|
||||
|
||||
### Operation Count (Streaming Mode, Scalar)
|
||||
|
||||
JVOLTYN adds minimal overhead to JVOLTY:
|
||||
|
||||
| Operation | JVOLTY | JVOLTYN Addition | Total |
|
||||
| :--- | :---: | :---: | :---: |
|
||||
| ADD/SUB | 45 | 1 | 46 |
|
||||
| MUL | 5 | 1 | 6 |
|
||||
| All other ops | ~1,150 | 0 | ~1,150 |
|
||||
| **Total** | **~1,207** | **~2** | **~1,209 cycles** |
|
||||
|
||||
The normalization adds ~2 cycles per bar (<0.2% overhead).
|
||||
|
||||
### Memory Footprint
|
||||
|
||||
Identical to JVOLTY plus one double for the normalization factor:
|
||||
- State record struct: ~100 bytes
|
||||
- Two ring buffers (10 + 128 elements): ~1.1 KB
|
||||
- Normalization factor: 8 bytes
|
||||
- **Total per instance**: ~1.5 KB
|
||||
|
||||
### Quality Metrics
|
||||
|
||||
| Metric | Score | Notes |
|
||||
| :--- | :---: | :--- |
|
||||
| **Comparability** | 10/10 | Universal 0-100 scale |
|
||||
| **Spike Rejection** | 9/10 | Inherited from JVOLTY |
|
||||
| **Regime Detection** | 8/10 | Inherited from JVOLTY |
|
||||
| **Stability** | 9/10 | Inherited from JVOLTY |
|
||||
| **Interpretability** | 9/10 | Intuitive percentage scale |
|
||||
|
||||
## Validation
|
||||
|
||||
JVOLTYN is validated against JVOLTY with manual normalization verification.
|
||||
|
||||
| Library | Status | Notes |
|
||||
| :--- | :---: | :--- |
|
||||
| **TA-Lib** | N/A | Not implemented |
|
||||
| **Skender** | N/A | Not implemented |
|
||||
| **Tulip** | N/A | Not implemented |
|
||||
| **Ooples** | N/A | Not implemented |
|
||||
| **JVOLTY Reference** | ✅ | Output = (JVOLTY - 1) / (logParam - 1) × 100 |
|
||||
|
||||
## Common Pitfalls
|
||||
|
||||
1. **Not the Same as Percentile Rank**: JVOLTYN is a linear transformation of the raw exponent, not a percentile rank of historical values. A reading of 50 means "halfway between min and max possible" for the current period, not "50th percentile of historical readings."
|
||||
|
||||
2. **Period Affects Behavior, Not Scale**: While the 0-100 scale is consistent, the underlying volatility dynamics still depend on period. A period-5 JVOLTYN responds faster than period-50. The normalization doesn't change the algorithm's temporal characteristics.
|
||||
|
||||
3. **First Bar Returns 0**: On initialization, JVOLTYN returns 0 (corresponding to raw JVOLTY = 1). This is mathematically correct but may not reflect actual market volatility until the indicator warms up.
|
||||
|
||||
4. **Warmup Period Inherited**: JVOLTYN requires the same ~220 bars (for period=14) plus 128 bars for distribution buffer stability. The `IsHot` property indicates warmup completion.
|
||||
|
||||
5. **Using RawVolatility for JMA**: If feeding JVOLTYN output to JMA or other algorithms expecting raw JVOLTY values, use `RawVolatility` property, not `Last.Value`.
|
||||
|
||||
6. **Threshold Interpretation**: A threshold like "JVOLTYN > 70" means different absolute volatility levels for different periods. Period-14 at JVOLTYN=70 implies raw d ≈ 3.0, while period-50 at JVOLTYN=70 implies raw d ≈ 3.6. For cross-period consistency, this is correct—both represent "70% of maximum possible adaptation."
|
||||
|
||||
## Use Cases
|
||||
|
||||
1. **Universal Volatility Threshold**: Set position sizing rules using fixed thresholds:
|
||||
- JVOLTYN < 30: Full position size
|
||||
- JVOLTYN 30-60: 75% position size
|
||||
- JVOLTYN > 60: 50% position size
|
||||
|
||||
2. **Portfolio Heatmap**: Display JVOLTYN across multiple instruments on a 0-100 color scale. Red indicates high volatility, green indicates low volatility—no per-instrument calibration needed.
|
||||
|
||||
3. **Regime Classification**: Classify market regimes using consistent thresholds:
|
||||
```
|
||||
Low volatility: JVOLTYN < 25
|
||||
Normal volatility: 25 ≤ JVOLTYN < 60
|
||||
High volatility: 60 ≤ JVOLTYN < 85
|
||||
Extreme volatility: JVOLTYN ≥ 85
|
||||
```
|
||||
|
||||
4. **Strategy Switching**: Toggle between trend-following (JVOLTYN < 40) and mean-reversion (JVOLTYN > 60) strategies based on normalized volatility regime.
|
||||
|
||||
5. **Alert Generation**: Trigger alerts when JVOLTYN crosses specific levels (e.g., rises above 75 or falls below 20) without needing to know the underlying period or raw scale.
|
||||
|
||||
## API Reference
|
||||
|
||||
### Constructor
|
||||
|
||||
```csharp
|
||||
public Jvoltyn(int period = 14)
|
||||
```
|
||||
|
||||
**Parameters:**
|
||||
- `period`: Lookback period (default: 14, minimum: 2)
|
||||
|
||||
### Properties
|
||||
|
||||
| Property | Type | Description |
|
||||
| :--- | :--- | :--- |
|
||||
| `Last` | `TValue` | Most recent normalized output (0-100) |
|
||||
| `RawVolatility` | `double` | Raw JVOLTY value [1, logParam] |
|
||||
| `IsHot` | `bool` | True when indicator has sufficient warmup |
|
||||
| `WarmupPeriod` | `int` | Bars required for stable output |
|
||||
| `Name` | `string` | Indicator name with parameters |
|
||||
|
||||
### Methods
|
||||
|
||||
| Method | Description |
|
||||
| :--- | :--- |
|
||||
| `Update(TValue input, bool isNew = true)` | Process new price value |
|
||||
| `Update(TSeries source)` | Process entire series |
|
||||
| `Reset()` | Clear all state |
|
||||
|
||||
## References
|
||||
|
||||
- Jurik Research. (1998-2005). "JMA White Papers." *jurikres.com* (archived).
|
||||
- QuanTAlib. "JVOLTY: Jurik Volatility." [Documentation](../jvolty/Jvolty.md).
|
||||
@@ -1,51 +1,123 @@
|
||||
// The MIT License (MIT)
|
||||
// © mihakralj
|
||||
// This Pine Script™ code is subject to the terms of the Mozilla Public License 2.0 at https://mozilla.org/MPL/2.0/
|
||||
// © QuanTAlib - Normalized Jurik Volatility (JVOLTYN)
|
||||
|
||||
//@version=6
|
||||
indicator("Normalized Jurik Volatility (JVOLTYN)", "JVOLTYN", overlay=false)
|
||||
|
||||
//@function Calculates normalized JVOLTYN using adaptive techniques to adjust to market volatility
|
||||
//@param source Series to calculate Jvolty from
|
||||
//@param period Number of bars used in the calculation
|
||||
//@returns JVOLTYN volatility
|
||||
//@optimized for performance and dirty data
|
||||
jvoltyn(series float source, simple int period) =>
|
||||
var simple float LEN1 = math.max((math.log(math.sqrt(0.5*(period-1))) / math.log(2.0)) + 2.0, 0)
|
||||
var simple float POW1 = math.max(LEN1 - 2.0, 0.5)
|
||||
var simple float LEN2 = math.sqrt(0.5*(period-1))*LEN1
|
||||
var simple float AVG_VOLTY_ALPHA = 2.0 / (math.max(4.0 * period, 65) + 1.0)
|
||||
var simple float DIV = 1.0/(10.0 + 10.0*(math.min(math.max(period-10,0),100))/100.0)
|
||||
var float upperBand = nz(source)
|
||||
var float lowerBand = nz(source)
|
||||
var float vSum = 0.0
|
||||
var float avgVolty = 0.0
|
||||
if na(source)
|
||||
na
|
||||
else
|
||||
float del1 = source - upperBand
|
||||
float del2 = source - lowerBand
|
||||
float volty = math.max(math.abs(del1), math.abs(del2))
|
||||
float past_volty = na(volty[10]) ? 0.0 : volty[10]
|
||||
vSum := vSum + (volty - past_volty) * DIV
|
||||
avgVolty := na(avgVolty) ? vSum : avgVolty + AVG_VOLTY_ALPHA * (vSum - avgVolty)
|
||||
float rvolty = 1.0
|
||||
if avgVolty > 0
|
||||
rvolty := volty / avgVolty
|
||||
rvolty := math.min(math.max(rvolty, 1.0), math.pow(LEN1, 1.0 / POW1))
|
||||
float Kv = math.pow(LEN2/(LEN2+1), math.sqrt(math.pow(rvolty, POW1)))
|
||||
upperBand := del1 > 0 ? source : source - Kv * del1
|
||||
lowerBand := del2 < 0 ? source : source - Kv * del2
|
||||
1.0 / (1.0 + math.exp(-(rvolty - 1.0) * 1.5))
|
||||
|
||||
|
||||
|
||||
// ---------- Main loop ----------
|
||||
indicator("JVOLTYN - Normalized Jurik Volatility", shorttitle="JVOLTYN", overlay=false)
|
||||
|
||||
// Inputs
|
||||
i_period = input.int(10, "Period", minval=1, tooltip="Number of bars used in the calculation")
|
||||
i_source = input.source(close, "Source")
|
||||
period = input.int(14, "Period", minval=2)
|
||||
src = input.source(close, "Source")
|
||||
|
||||
// Calculation
|
||||
jvoltyn= jvoltyn(i_source, i_period)
|
||||
// Calculate logParam (same as JVOLTY)
|
||||
len1 = math.max((period - 1) / 2.0, 0)
|
||||
logParam = len1 > 0 ? math.max(math.log(math.sqrt(len1)) / math.log(2) + 2.0, 0) : 0
|
||||
|
||||
// Normalization factor: maps [1, logParam] to [0, 100]
|
||||
normFactor = logParam > 1 ? 100.0 / (logParam - 1.0) : 100.0
|
||||
|
||||
// JVOLTY core parameters
|
||||
powParam = math.max(logParam - 2.0, 0.5)
|
||||
sqrtDivider = len1 > 0 ? math.sqrt(len1) * logParam / (math.sqrt(len1) * logParam + 1.0) : 1.0
|
||||
|
||||
// State variables
|
||||
var float upperBand = na
|
||||
var float lowerBand = na
|
||||
var float[] voltyBuffer = array.new_float(10, 0.0)
|
||||
var float[] distBuffer = array.new_float(128, 0.0)
|
||||
var int distIndex = 0
|
||||
var int distCount = 0
|
||||
var float lastValid = 0.0
|
||||
|
||||
// Initialize bands
|
||||
if na(upperBand)
|
||||
upperBand := src
|
||||
lowerBand := src
|
||||
|
||||
// Finite value helper
|
||||
getFinite(float val, float fallback) =>
|
||||
na(val) or not math.isfinite(val) ? fallback : val
|
||||
|
||||
price = getFinite(src, lastValid)
|
||||
lastValid := price
|
||||
|
||||
// Calculate deviation from bands
|
||||
del1 = math.abs(price - nz(upperBand[1], price))
|
||||
del2 = math.abs(price - nz(lowerBand[1], price))
|
||||
deviation = math.max(del1, del2) + 1e-10
|
||||
|
||||
// Update 10-bar volatility buffer (ring buffer style)
|
||||
array.shift(voltyBuffer)
|
||||
array.push(voltyBuffer, deviation)
|
||||
|
||||
// Short volatility: 10-bar SMA
|
||||
shortVolty = array.avg(voltyBuffer)
|
||||
|
||||
// Update distribution buffer
|
||||
if distCount < 128
|
||||
array.set(distBuffer, distCount, shortVolty)
|
||||
distCount += 1
|
||||
else
|
||||
array.set(distBuffer, distIndex, shortVolty)
|
||||
distIndex := (distIndex + 1) % 128
|
||||
|
||||
// Calculate trimmed mean from distribution
|
||||
calcTrimmedMean() =>
|
||||
if distCount < 16
|
||||
array.avg(distBuffer)
|
||||
else
|
||||
// Sort the buffer
|
||||
sorted = array.copy(distBuffer)
|
||||
array.sort(sorted)
|
||||
|
||||
// Calculate trim indices
|
||||
if distCount >= 128
|
||||
// Full buffer: use middle 65 values (indices 32-96)
|
||||
sum = 0.0
|
||||
for i = 32 to 96
|
||||
sum += array.get(sorted, i)
|
||||
sum / 65.0
|
||||
else
|
||||
// Partial buffer: adaptive trim
|
||||
sampleSize = math.max(5, math.round(0.5 * distCount))
|
||||
startIdx = math.floor((distCount - sampleSize) / 2.0)
|
||||
sum = 0.0
|
||||
for i = 0 to sampleSize - 1
|
||||
sum += array.get(sorted, int(startIdx) + i)
|
||||
sum / sampleSize
|
||||
|
||||
refVolty = calcTrimmedMean()
|
||||
|
||||
// Calculate dynamic exponent (raw JVOLTY value)
|
||||
ratio = refVolty > 0 ? math.abs(shortVolty) / refVolty : 1.0
|
||||
rawD = math.max(1.0, math.min(math.pow(ratio, powParam), logParam))
|
||||
|
||||
// Normalize to 0-100 scale
|
||||
jvoltyn = (rawD - 1.0) * normFactor
|
||||
|
||||
// Update adaptive bands
|
||||
adapt = math.pow(sqrtDivider, math.sqrt(rawD))
|
||||
if price > upperBand
|
||||
upperBand := price
|
||||
else
|
||||
upperBand := nz(upperBand[1], price) + adapt * (price - nz(upperBand[1], price))
|
||||
|
||||
if price < lowerBand
|
||||
lowerBand := price
|
||||
else
|
||||
lowerBand := nz(lowerBand[1], price) + adapt * (price - nz(lowerBand[1], price))
|
||||
|
||||
// Plot
|
||||
plot(jvoltyn, "JVoltyN", color=color.yellow, linewidth=2)
|
||||
plot(jvoltyn, "JVOLTYN", color=color.new(color.orange, 0), linewidth=2)
|
||||
|
||||
// Reference levels
|
||||
hline(0, "Min Volatility", color=color.gray, linestyle=hline.style_dotted)
|
||||
hline(25, "Low", color=color.green, linestyle=hline.style_dotted)
|
||||
hline(50, "Medium", color=color.yellow, linestyle=hline.style_dotted)
|
||||
hline(75, "High", color=color.red, linestyle=hline.style_dotted)
|
||||
hline(100, "Max Volatility", color=color.gray, linestyle=hline.style_dotted)
|
||||
|
||||
// Background coloring for volatility regimes
|
||||
bgcolor(jvoltyn < 25 ? color.new(color.green, 90) :
|
||||
jvoltyn < 50 ? color.new(color.yellow, 90) :
|
||||
jvoltyn < 75 ? color.new(color.orange, 90) :
|
||||
color.new(color.red, 90))
|
||||
Reference in New Issue
Block a user