mirror of
https://github.com/mihakralj/QuanTAlib.git
synced 2026-08-20 19:48:05 +00:00
more volatilty
This commit is contained in:
@@ -0,0 +1,176 @@
|
||||
using TradingPlatform.BusinessLayer;
|
||||
using QuanTAlib;
|
||||
|
||||
namespace QuanTAlib.Tests;
|
||||
|
||||
public class JvoltyIndicatorTests
|
||||
{
|
||||
[Fact]
|
||||
public void JvoltyIndicator_Constructor_SetsDefaults()
|
||||
{
|
||||
var indicator = new JvoltyIndicator();
|
||||
|
||||
Assert.Equal(14, indicator.Period);
|
||||
Assert.Equal(SourceType.Close, indicator.Source);
|
||||
Assert.True(indicator.ShowColdValues);
|
||||
Assert.Equal("JVOLTY - Jurik Volatility", indicator.Name);
|
||||
Assert.True(indicator.SeparateWindow);
|
||||
Assert.True(indicator.OnBackGround);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void JvoltyIndicator_ShortName_IncludesParameters()
|
||||
{
|
||||
var indicator = new JvoltyIndicator { Period = 20 };
|
||||
Assert.Contains("JVOLTY", indicator.ShortName, StringComparison.Ordinal);
|
||||
Assert.Contains("20", indicator.ShortName, StringComparison.Ordinal);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void JvoltyIndicator_MinHistoryDepths_EqualsZero()
|
||||
{
|
||||
var indicator = new JvoltyIndicator();
|
||||
|
||||
Assert.Equal(0, JvoltyIndicator.MinHistoryDepths);
|
||||
Assert.Equal(0, ((IWatchlistIndicator)indicator).MinHistoryDepths);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void JvoltyIndicator_Initialize_CreatesInternalJvolty()
|
||||
{
|
||||
var indicator = new JvoltyIndicator();
|
||||
|
||||
// Initialize should not throw
|
||||
indicator.Initialize();
|
||||
|
||||
// After init, line series should exist
|
||||
Assert.Single(indicator.LinesSeries);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void JvoltyIndicator_ProcessUpdate_HistoricalBar_ComputesValue()
|
||||
{
|
||||
var indicator = new JvoltyIndicator { 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 >= 1.0); // Jvolty minimum is 1.0
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void JvoltyIndicator_ProcessUpdate_NewBar_ComputesValue()
|
||||
{
|
||||
var indicator = new JvoltyIndicator { 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 JvoltyIndicator_DifferentPeriods_Work()
|
||||
{
|
||||
int[] periods = { 5, 10, 14, 20, 50 };
|
||||
|
||||
foreach (var period in periods)
|
||||
{
|
||||
var indicator = new JvoltyIndicator { 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 >= 1.0, $"Period {period} should produce Jvolty >= 1.0");
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void JvoltyIndicator_DifferentSourceTypes_Work()
|
||||
{
|
||||
SourceType[] sources = { SourceType.Close, SourceType.High, SourceType.Low, SourceType.HL2, SourceType.HLC3 };
|
||||
|
||||
foreach (var source in sources)
|
||||
{
|
||||
var indicator = new JvoltyIndicator { 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 JvoltyIndicator_Period_CanBeChanged()
|
||||
{
|
||||
var indicator = new JvoltyIndicator();
|
||||
Assert.Equal(14, indicator.Period);
|
||||
|
||||
indicator.Period = 20;
|
||||
Assert.Equal(20, indicator.Period);
|
||||
|
||||
indicator.Period = 50;
|
||||
Assert.Equal(50, indicator.Period);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void JvoltyIndicator_ShowColdValues_CanBeToggled()
|
||||
{
|
||||
var indicator = new JvoltyIndicator();
|
||||
Assert.True(indicator.ShowColdValues);
|
||||
|
||||
indicator.ShowColdValues = false;
|
||||
Assert.False(indicator.ShowColdValues);
|
||||
|
||||
indicator.ShowColdValues = true;
|
||||
Assert.True(indicator.ShowColdValues);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void JvoltyIndicator_SourceCodeLink_IsValid()
|
||||
{
|
||||
var indicator = new JvoltyIndicator();
|
||||
Assert.Contains("github.com", indicator.SourceCodeLink, StringComparison.Ordinal);
|
||||
Assert.Contains("Jvolty.Quantower.cs", indicator.SourceCodeLink, StringComparison.Ordinal);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
using System.Drawing;
|
||||
using System.Runtime.CompilerServices;
|
||||
using TradingPlatform.BusinessLayer;
|
||||
|
||||
namespace QuanTAlib;
|
||||
|
||||
[SkipLocalsInit]
|
||||
public sealed class JvoltyIndicator : 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 Jvolty _jvolty = 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 => $"JVOLTY {Period}:{_sourceName}";
|
||||
public override string SourceCodeLink => "https://github.com/mihakralj/QuanTAlib/blob/main/lib/volatility/jvolty/Jvolty.Quantower.cs";
|
||||
|
||||
public JvoltyIndicator()
|
||||
{
|
||||
OnBackGround = true;
|
||||
SeparateWindow = true;
|
||||
_sourceName = Source.ToString();
|
||||
Name = "JVOLTY - Jurik Volatility";
|
||||
Description = "Jurik Volatility is an adaptive volatility measure extracted from the JMA algorithm, providing normalized volatility bands with dynamic exponent calculation.";
|
||||
|
||||
_series = new LineSeries(name: "JVOLTY", color: IndicatorExtensions.Volatility, width: 2, style: LineStyle.Solid);
|
||||
AddLineSeries(_series);
|
||||
}
|
||||
|
||||
protected override void OnInit()
|
||||
{
|
||||
_jvolty = new Jvolty(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 = _jvolty.Update(new TValue(item.TimeLeft.Ticks, _priceSelector(item)), isNew: args.IsNewBar());
|
||||
_series.SetValue(result.Value, _jvolty.IsHot, ShowColdValues);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,653 @@
|
||||
namespace QuanTAlib.Tests;
|
||||
|
||||
public class JvoltyTests
|
||||
{
|
||||
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 Jvolty(0));
|
||||
Assert.Throws<ArgumentOutOfRangeException>(() => new Jvolty(-1));
|
||||
|
||||
var jvolty = new Jvolty(10);
|
||||
Assert.NotNull(jvolty);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Constructor_SetsCorrectName()
|
||||
{
|
||||
var jvolty = new Jvolty(7);
|
||||
Assert.Equal("Jvolty(7)", jvolty.Name);
|
||||
Assert.True(jvolty.WarmupPeriod > 0);
|
||||
|
||||
var jvolty2 = new Jvolty(14);
|
||||
Assert.Equal("Jvolty(14)", jvolty2.Name);
|
||||
}
|
||||
|
||||
// ============== Basic Functionality ==============
|
||||
|
||||
[Fact]
|
||||
public void BasicCalculation_DoesNotCrash()
|
||||
{
|
||||
var jvolty = new Jvolty(10);
|
||||
var series = GenerateTestData(100);
|
||||
|
||||
foreach (var value in series)
|
||||
{
|
||||
jvolty.Update(value);
|
||||
}
|
||||
|
||||
Assert.True(double.IsFinite(jvolty.Last.Value));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Calc_ReturnsValue()
|
||||
{
|
||||
var jvolty = new Jvolty(10);
|
||||
var input = new TValue(DateTime.UtcNow, 100.0);
|
||||
|
||||
Assert.InRange(jvolty.Last.Value, -Tolerance, Tolerance); // Initially zero
|
||||
|
||||
TValue result = jvolty.Update(input);
|
||||
|
||||
Assert.True(result.Value >= 1.0); // Minimum volatility is 1.0
|
||||
Assert.Equal(result.Value, jvolty.Last.Value, Tolerance);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void FirstValue_ReturnsMinimumVolatility()
|
||||
{
|
||||
var jvolty = new Jvolty(10);
|
||||
var input = new TValue(DateTime.UtcNow, 100.0);
|
||||
|
||||
TValue result = jvolty.Update(input);
|
||||
|
||||
Assert.Equal(1.0, result.Value, Tolerance); // First bar returns minimum volatility
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Properties_Accessible()
|
||||
{
|
||||
var jvolty = new Jvolty(10);
|
||||
|
||||
Assert.InRange(jvolty.Last.Value, -Tolerance, Tolerance); // Initially zero
|
||||
Assert.False(jvolty.IsHot);
|
||||
Assert.Contains("Jvolty", jvolty.Name, StringComparison.Ordinal);
|
||||
Assert.True(jvolty.WarmupPeriod > 0);
|
||||
|
||||
var input = new TValue(DateTime.UtcNow, 100.0);
|
||||
jvolty.Update(input);
|
||||
|
||||
Assert.True(Math.Abs(jvolty.Last.Value) > Tolerance); // No longer zero
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void BandProperties_Accessible()
|
||||
{
|
||||
var jvolty = new Jvolty(10);
|
||||
var input = new TValue(DateTime.UtcNow, 100.0);
|
||||
|
||||
jvolty.Update(input);
|
||||
|
||||
// After first bar, bands should be initialized to the input value
|
||||
Assert.Equal(100.0, jvolty.UpperBand, Tolerance);
|
||||
Assert.Equal(100.0, jvolty.LowerBand, Tolerance);
|
||||
}
|
||||
|
||||
// ============== State Management & Bar Correction ==============
|
||||
|
||||
[Fact]
|
||||
public void Calc_IsNew_AcceptsParameter()
|
||||
{
|
||||
var jvolty = new Jvolty(10);
|
||||
var series = GenerateTestData(50);
|
||||
|
||||
// Feed enough values to build up volatility history
|
||||
for (int i = 0; i < 49; i++)
|
||||
{
|
||||
jvolty.Update(series[i], isNew: true);
|
||||
}
|
||||
double valueBefore = jvolty.Last.Value;
|
||||
|
||||
// Add one more value with isNew=true
|
||||
jvolty.Update(series[49], isNew: true);
|
||||
double valueAfter = jvolty.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 jvolty = new Jvolty(10);
|
||||
var series = GenerateTestData(50);
|
||||
|
||||
// Feed enough bars to have meaningful volatility
|
||||
for (int i = 0; i < 49; i++)
|
||||
{
|
||||
jvolty.Update(series[i], isNew: true);
|
||||
}
|
||||
|
||||
// Add one more value with isNew=true
|
||||
jvolty.Update(series[49], isNew: true);
|
||||
double beforeUpdate = jvolty.Last.Value;
|
||||
|
||||
// Update same bar with different value (isNew=false)
|
||||
var modifiedInput = new TValue(series[49].Time, series[49].Value + 50.0);
|
||||
jvolty.Update(modifiedInput, isNew: false);
|
||||
double afterUpdate = jvolty.Last.Value;
|
||||
|
||||
// Values should be different after the correction
|
||||
Assert.True(Math.Abs(beforeUpdate - afterUpdate) > Tolerance);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void IsNew_Consistency()
|
||||
{
|
||||
var jvolty = new Jvolty(10);
|
||||
var series = GenerateTestData(100);
|
||||
|
||||
// Feed first 99
|
||||
for (int i = 0; i < 99; i++)
|
||||
{
|
||||
jvolty.Update(series[i]);
|
||||
}
|
||||
|
||||
// Update with 100th point (isNew=true)
|
||||
jvolty.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 = jvolty.Update(modifiedInput, false).Value;
|
||||
|
||||
// Create new instance and feed up to modified
|
||||
var jvolty2 = new Jvolty(10);
|
||||
for (int i = 0; i < 99; i++)
|
||||
{
|
||||
jvolty2.Update(series[i]);
|
||||
}
|
||||
double val3 = jvolty2.Update(modifiedInput, true).Value;
|
||||
|
||||
Assert.Equal(val3, val2, Tolerance);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void IterativeCorrections_RestoreToOriginalState()
|
||||
{
|
||||
var jvolty = new Jvolty(5);
|
||||
var series = GenerateTestData(20);
|
||||
|
||||
// Feed 10 new values
|
||||
TValue tenthValue = default;
|
||||
for (int i = 0; i < 10; i++)
|
||||
{
|
||||
tenthValue = series[i];
|
||||
jvolty.Update(tenthValue, isNew: true);
|
||||
}
|
||||
|
||||
// Remember state after 10 values
|
||||
double stateAfterTen = jvolty.Last.Value;
|
||||
|
||||
// Generate 9 corrections with isNew=false (different values)
|
||||
for (int i = 10; i < 19; i++)
|
||||
{
|
||||
jvolty.Update(series[i], isNew: false);
|
||||
}
|
||||
|
||||
// Feed the remembered 10th value again with isNew=false
|
||||
TValue finalResult = jvolty.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 jvolty = new Jvolty(10);
|
||||
var series = GenerateTestData(50);
|
||||
|
||||
foreach (var value in series)
|
||||
{
|
||||
jvolty.Update(value);
|
||||
}
|
||||
|
||||
double lastVal = jvolty.Last.Value;
|
||||
Assert.True(Math.Abs(lastVal) > Tolerance); // Not zero
|
||||
|
||||
jvolty.Reset();
|
||||
Assert.InRange(jvolty.Last.Value, -Tolerance, Tolerance); // Reset to zero
|
||||
Assert.False(jvolty.IsHot);
|
||||
|
||||
// After reset, should accept new values
|
||||
jvolty.Update(series[0]);
|
||||
Assert.True(Math.Abs(jvolty.Last.Value) > Tolerance); // No longer zero
|
||||
}
|
||||
|
||||
// ============== Warmup & Convergence ==============
|
||||
|
||||
[Fact]
|
||||
public void IsHot_BecomesTrueAfterWarmup()
|
||||
{
|
||||
var jvolty = new Jvolty(5);
|
||||
|
||||
Assert.False(jvolty.IsHot);
|
||||
|
||||
var series = GenerateTestData(200);
|
||||
|
||||
int steps = 0;
|
||||
while (!jvolty.IsHot && steps < series.Count)
|
||||
{
|
||||
jvolty.Update(series[steps]);
|
||||
steps++;
|
||||
}
|
||||
|
||||
Assert.True(jvolty.IsHot);
|
||||
Assert.True(steps > 0);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void WarmupPeriod_IsPositive()
|
||||
{
|
||||
var jvolty = new Jvolty(10);
|
||||
Assert.True(jvolty.WarmupPeriod > 0);
|
||||
|
||||
var jvolty2 = new Jvolty(20);
|
||||
Assert.True(jvolty2.WarmupPeriod > 0);
|
||||
|
||||
// WarmupPeriod should increase with the period parameter
|
||||
Assert.True(jvolty2.WarmupPeriod >= jvolty.WarmupPeriod);
|
||||
}
|
||||
|
||||
// ============== NaN/Infinity Handling ==============
|
||||
|
||||
[Fact]
|
||||
public void NaN_Input_UsesLastValidValue()
|
||||
{
|
||||
var jvolty = new Jvolty(5);
|
||||
|
||||
var input1 = new TValue(DateTime.UtcNow, 100.0);
|
||||
jvolty.Update(input1);
|
||||
|
||||
var input2 = new TValue(DateTime.UtcNow.AddMinutes(1), 110.0);
|
||||
jvolty.Update(input2);
|
||||
|
||||
// Feed NaN value
|
||||
var inputWithNaN = new TValue(DateTime.UtcNow.AddMinutes(2), double.NaN);
|
||||
var resultAfterNaN = jvolty.Update(inputWithNaN);
|
||||
|
||||
// Result should be finite
|
||||
Assert.True(double.IsFinite(resultAfterNaN.Value));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Infinity_Input_UsesLastValidValue()
|
||||
{
|
||||
var jvolty = new Jvolty(5);
|
||||
|
||||
var input1 = new TValue(DateTime.UtcNow, 100.0);
|
||||
jvolty.Update(input1);
|
||||
|
||||
var input2 = new TValue(DateTime.UtcNow.AddMinutes(1), 110.0);
|
||||
jvolty.Update(input2);
|
||||
|
||||
// Feed Infinity value
|
||||
var inputWithInf = new TValue(DateTime.UtcNow.AddMinutes(2), double.PositiveInfinity);
|
||||
var resultAfterInf = jvolty.Update(inputWithInf);
|
||||
|
||||
// Result should be finite
|
||||
Assert.True(double.IsFinite(resultAfterInf.Value));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void BatchNaN_Safe()
|
||||
{
|
||||
var jvolty = new Jvolty(5);
|
||||
var series = GenerateTestData(20);
|
||||
|
||||
// Feed some values
|
||||
for (int i = 0; i < 10; i++)
|
||||
{
|
||||
jvolty.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 = jvolty.Update(nanInput);
|
||||
Assert.True(double.IsFinite(result.Value));
|
||||
}
|
||||
}
|
||||
|
||||
// ============== Consistency Tests ==============
|
||||
|
||||
[Fact]
|
||||
public void BatchCalc_MatchesIterativeCalc()
|
||||
{
|
||||
var jvoltyIterative = new Jvolty(10);
|
||||
var series = GenerateTestData(100);
|
||||
|
||||
// Calculate iteratively
|
||||
var iterativeResults = new TSeries();
|
||||
foreach (var value in series)
|
||||
{
|
||||
iterativeResults.Add(jvoltyIterative.Update(value));
|
||||
}
|
||||
|
||||
// Calculate batch
|
||||
var batchResults = Jvolty.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 jvolty1 = new Jvolty(10);
|
||||
var jvolty2 = new Jvolty(10);
|
||||
var series = GenerateTestData(100);
|
||||
|
||||
// Streaming
|
||||
foreach (var value in series)
|
||||
{
|
||||
jvolty1.Update(value);
|
||||
}
|
||||
|
||||
// Batch
|
||||
jvolty2.Update(series);
|
||||
|
||||
Assert.Equal(jvolty1.Last.Value, jvolty2.Last.Value, Tolerance);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SpanCalc_MatchesStreaming()
|
||||
{
|
||||
var jvolty = new Jvolty(10);
|
||||
var series = GenerateTestData(100);
|
||||
|
||||
// Stream all values first
|
||||
foreach (var value in series)
|
||||
{
|
||||
jvolty.Update(value);
|
||||
}
|
||||
double streamingLast = jvolty.Last.Value;
|
||||
|
||||
// Span calculation
|
||||
var output = new double[series.Count];
|
||||
Jvolty.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 jvolty = new Jvolty(10);
|
||||
var series = GenerateTestData(50);
|
||||
|
||||
var result = jvolty.Update(series);
|
||||
Assert.Equal(50, result.Count);
|
||||
Assert.Equal(jvolty.Last.Value, result.Last.Value);
|
||||
}
|
||||
|
||||
// ============== Static Batch Method ==============
|
||||
|
||||
[Fact]
|
||||
public void StaticBatch_Works()
|
||||
{
|
||||
var series = GenerateTestData(50);
|
||||
|
||||
var results = Jvolty.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>(() => Jvolty.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(() => Jvolty.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>(() => Jvolty.Calculate(source, output, 0));
|
||||
}
|
||||
|
||||
// ============== Edge Cases ==============
|
||||
|
||||
[Fact]
|
||||
public void SingleValue_ReturnsValidResult()
|
||||
{
|
||||
var jvolty = new Jvolty(10);
|
||||
var input = new TValue(DateTime.UtcNow, 100.0);
|
||||
|
||||
var result = jvolty.Update(input);
|
||||
|
||||
Assert.True(double.IsFinite(result.Value));
|
||||
Assert.Equal(1.0, result.Value, Tolerance); // First bar = minimum volatility
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Period1_Works()
|
||||
{
|
||||
var jvolty = new Jvolty(1);
|
||||
var series = GenerateTestData(10);
|
||||
|
||||
foreach (var value in series)
|
||||
{
|
||||
var result = jvolty.Update(value);
|
||||
Assert.True(double.IsFinite(result.Value));
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void FlatValues_MinimumVolatility()
|
||||
{
|
||||
var jvolty = new Jvolty(5);
|
||||
|
||||
// All values are the same
|
||||
for (int i = 0; i < 50; i++)
|
||||
{
|
||||
var input = new TValue(DateTime.UtcNow.AddMinutes(i), 100.0);
|
||||
jvolty.Update(input);
|
||||
}
|
||||
|
||||
// Volatility should be at minimum (1.0) for flat values
|
||||
Assert.Equal(1.0, jvolty.Last.Value, Tolerance);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void HighVolatility_IncreasesExponent()
|
||||
{
|
||||
var jvolty = new Jvolty(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));
|
||||
jvolty.Update(input);
|
||||
}
|
||||
|
||||
double lowVolatility = jvolty.Last.Value;
|
||||
|
||||
// Create high volatility spike
|
||||
var spike = new TValue(DateTime.UtcNow.AddMinutes(21), 150.0);
|
||||
jvolty.Update(spike);
|
||||
|
||||
double highVolatility = jvolty.Last.Value;
|
||||
|
||||
// High volatility should be greater than low volatility
|
||||
Assert.True(highVolatility > lowVolatility);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Bands_TrackPrice()
|
||||
{
|
||||
var jvolty = new Jvolty(10);
|
||||
|
||||
// Feed increasing prices
|
||||
for (int i = 0; i < 20; i++)
|
||||
{
|
||||
var input = new TValue(DateTime.UtcNow.AddMinutes(i), 100.0 + i);
|
||||
jvolty.Update(input);
|
||||
}
|
||||
|
||||
// Upper band should track the highest recent prices
|
||||
Assert.True(jvolty.UpperBand > 100.0);
|
||||
// Lower band should lag behind due to adaptive decay
|
||||
Assert.True(jvolty.LowerBand < jvolty.UpperBand);
|
||||
}
|
||||
|
||||
// ============== Event Publishing ==============
|
||||
|
||||
[Fact]
|
||||
public void PubEvent_Fires()
|
||||
{
|
||||
var jvolty = new Jvolty(10);
|
||||
bool eventFired = false;
|
||||
|
||||
jvolty.Pub += (object? sender, in TValueEventArgs args) => eventFired = true;
|
||||
|
||||
var input = new TValue(DateTime.UtcNow, 100.0);
|
||||
jvolty.Update(input);
|
||||
|
||||
Assert.True(eventFired);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void EventChaining_Works()
|
||||
{
|
||||
var series = GenerateTestData(50);
|
||||
|
||||
var jvolty = new Jvolty(10);
|
||||
var sma = new Sma(jvolty, 5); // Chain SMA to Jvolty output
|
||||
|
||||
foreach (var value in series)
|
||||
{
|
||||
jvolty.Update(value);
|
||||
}
|
||||
|
||||
Assert.True(double.IsFinite(sma.Last.Value));
|
||||
}
|
||||
|
||||
// ============== Additional Tests ==============
|
||||
|
||||
[Fact]
|
||||
public void LargeDataset_Completes()
|
||||
{
|
||||
var jvolty = new Jvolty(20);
|
||||
var series = GenerateTestData(5000);
|
||||
|
||||
foreach (var value in series)
|
||||
{
|
||||
jvolty.Update(value);
|
||||
}
|
||||
|
||||
Assert.True(jvolty.IsHot);
|
||||
Assert.True(double.IsFinite(jvolty.Last.Value));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void DifferentPeriods_ProduceValidValues()
|
||||
{
|
||||
var series = GenerateTestData(200);
|
||||
|
||||
var jvolty1 = new Jvolty(5);
|
||||
var jvolty2 = new Jvolty(10);
|
||||
var jvolty3 = new Jvolty(20);
|
||||
|
||||
foreach (var value in series)
|
||||
{
|
||||
jvolty1.Update(value);
|
||||
jvolty2.Update(value);
|
||||
jvolty3.Update(value);
|
||||
}
|
||||
|
||||
Assert.True(double.IsFinite(jvolty1.Last.Value));
|
||||
Assert.True(double.IsFinite(jvolty2.Last.Value));
|
||||
Assert.True(double.IsFinite(jvolty3.Last.Value));
|
||||
Assert.True(jvolty1.Last.Value >= 1.0);
|
||||
Assert.True(jvolty2.Last.Value >= 1.0);
|
||||
Assert.True(jvolty3.Last.Value >= 1.0);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SourceChaining_Works()
|
||||
{
|
||||
var series = GenerateTestData(200);
|
||||
|
||||
// Create source TSeries that publishes events
|
||||
var sourceSeries = new TSeries();
|
||||
var jvolty = new Jvolty(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(jvolty.Last.Value));
|
||||
Assert.True(jvolty.Last.Value >= 1.0); // Minimum volatility
|
||||
}
|
||||
|
||||
#pragma warning disable S2699 // Test contains Assert.True and Assert.InRange - analyzer false positive
|
||||
[Fact]
|
||||
public void Prime_Works()
|
||||
{
|
||||
var jvolty = new Jvolty(5);
|
||||
var values = new double[] { 100.0, 101.0, 99.5, 102.0, 98.0, 103.0 };
|
||||
|
||||
jvolty.Prime(values);
|
||||
|
||||
double lastValue = jvolty.Last.Value;
|
||||
Assert.True(double.IsFinite(lastValue), "Last value should be finite after Prime");
|
||||
Assert.InRange(lastValue, 1.0, double.MaxValue); // Volatility >= minimum (1.0)
|
||||
}
|
||||
#pragma warning restore S2699
|
||||
}
|
||||
@@ -0,0 +1,388 @@
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Runtime.InteropServices;
|
||||
|
||||
namespace QuanTAlib;
|
||||
|
||||
/// <summary>
|
||||
/// Jvolty: Jurik Volatility Bands
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Extracted volatility component from JMA (Jurik Moving Average).
|
||||
/// Provides adaptive volatility bands and a normalized volatility measure.
|
||||
///
|
||||
/// Key features:
|
||||
/// - Adaptive bands that track price with volatility-adjusted decay
|
||||
/// - 10-bar local deviation smoothing
|
||||
/// - 128-bar trimmed mean for reference volatility
|
||||
/// - Dynamic exponent normalized to [1, logParam] range
|
||||
///
|
||||
/// Output: Normalized volatility (1 = low volatility, logParam = high volatility)
|
||||
/// </remarks>
|
||||
/// <seealso href="Jvolty.md">Detailed documentation</seealso>
|
||||
/// <seealso href="jvolty.pine">Reference Pine Script implementation</seealso>
|
||||
[SkipLocalsInit]
|
||||
public sealed class Jvolty : 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)
|
||||
|
||||
// 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 volatility
|
||||
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;
|
||||
|
||||
public override bool IsHot => _s.Bars >= WarmupPeriod;
|
||||
|
||||
/// <summary>
|
||||
/// Creates Jvolty with specified period.
|
||||
/// </summary>
|
||||
/// <param name="period">Period for volatility calculation (must be >= 1)</param>
|
||||
public Jvolty(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);
|
||||
|
||||
// same warmup heuristic used in JMA
|
||||
WarmupPeriod = (int)Math.Ceiling(20.0 + 80.0 * Math.Pow(period, 0.36));
|
||||
|
||||
_handler = Handle;
|
||||
Name = $"Jvolty({period})";
|
||||
|
||||
_devBuffer = new RingBuffer(DevWindowSize);
|
||||
_volBuffer = new RingBuffer(VolWindowSize);
|
||||
|
||||
Reset();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates Jvolty with specified source and period.
|
||||
/// </summary>
|
||||
/// <param name="source">Source to subscribe to</param>
|
||||
/// <param name="period">Period for volatility calculation</param>
|
||||
public Jvolty(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 Jvolty.
|
||||
/// 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 CalculateJvolty(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 1.0;
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
private double CalculateJvolty(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;
|
||||
return d;
|
||||
}
|
||||
|
||||
[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 Jvolty for the entire series using a new instance.
|
||||
/// </summary>
|
||||
public static TSeries Batch(TSeries source, int period)
|
||||
{
|
||||
var jvolty = new Jvolty(period);
|
||||
return jvolty.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 jvolty = new Jvolty(period);
|
||||
for (int i = 0; i < source.Length; i++)
|
||||
{
|
||||
output[i] = jvolty.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,242 @@
|
||||
# JVOLTY: Jurik Volatility
|
||||
|
||||
> "The volatility measure that ignores the noise—because sometimes, the best signal comes from knowing what to throw away."
|
||||
|
||||
Jurik Volatility (JVOLTY) is the adaptive volatility component extracted from Mark Jurik's JMA algorithm. Unlike traditional volatility measures that treat all price movements equally, JVOLTY uses a 128-bar trimmed mean distribution to compute a robust volatility reference that rejects outliers by design. The result: a volatility measure that remains stable during flash crashes, earnings surprises, and 5-sigma events while still tracking genuine regime changes.
|
||||
|
||||
## Historical Context
|
||||
|
||||
When Mark Jurik developed JMA in the 1990s, he embedded a sophisticated volatility measurement system within it. This wasn't documented. It wasn't marketed separately. It was just part of the compiled DLL that powered the adaptive smoothing.
|
||||
|
||||
The reverse-engineering efforts that revealed JMA's true algorithm also exposed this volatility component. While the forum approximations used simple exponential smoothing for volatility (easy to implement, reasonable results), Jurik's actual approach was radically different: maintain a 128-sample distribution of recent volatility readings and compute a trimmed mean that discards the tails.
|
||||
|
||||
JVOLTY extracts this volatility measurement system as a standalone indicator. The same percentile trimming that makes JMA stable during market dislocations now provides a standalone volatility metric. For traders who need JMA's volatility reference without the smoothed price output, JVOLTY delivers exactly that.
|
||||
|
||||
The implementation matches the decompiled reference within floating-point tolerance.
|
||||
|
||||
## Architecture & Physics
|
||||
|
||||
JVOLTY computes volatility through a four-stage pipeline: adaptive band tracking, local deviation measurement, short-term smoothing, and distribution-based trimmed mean calculation.
|
||||
|
||||
### 1. Adaptive Envelope (UpperBand / LowerBand)
|
||||
|
||||
Two asymmetric bands track price extremes:
|
||||
|
||||
$$
|
||||
U_t = \begin{cases}
|
||||
P_t & \text{if } P_t > U_{t-1} \\
|
||||
U_{t-1} + \beta_t (P_t - U_{t-1}) & \text{otherwise}
|
||||
\end{cases}
|
||||
$$
|
||||
|
||||
$$
|
||||
L_t = \begin{cases}
|
||||
P_t & \text{if } P_t < L_{t-1} \\
|
||||
L_{t-1} + \beta_t (P_t - L_{t-1}) & \text{otherwise}
|
||||
\end{cases}
|
||||
$$
|
||||
|
||||
where $\beta_t$ is the adaptive decay rate derived from the volatility-adjusted exponent.
|
||||
|
||||
When price breaks a band, it snaps immediately. Otherwise, the band decays toward price at rate $\beta$. This creates an envelope that expands quickly during breakouts and contracts slowly during consolidation.
|
||||
|
||||
### 2. Local Deviation
|
||||
|
||||
The instantaneous deviation measures distance from the adaptive bands:
|
||||
|
||||
$$
|
||||
\Delta_t = \max(|P_t - U_{t-1}|, |P_t - L_{t-1}|) + 10^{-10}
|
||||
$$
|
||||
|
||||
The epsilon prevents division by zero in downstream calculations. This deviation captures how far price has moved relative to the recent range established by the bands.
|
||||
|
||||
### 3. Short Volatility (10-bar SMA)
|
||||
|
||||
The local deviation is smoothed with a 10-bar simple moving average:
|
||||
|
||||
$$
|
||||
V_t = \frac{1}{10} \sum_{i=0}^{9} \Delta_{t-i}
|
||||
$$
|
||||
|
||||
This smoothed deviation represents the "raw" volatility reading that feeds into the distribution buffer. The 10-bar window provides enough smoothing to avoid tick-level noise while remaining responsive to genuine volatility changes.
|
||||
|
||||
### 4. Volatility Distribution (128-sample trimmed mean)
|
||||
|
||||
The core innovation: instead of exponential smoothing, JVOLTY maintains a 128-sample circular buffer of raw volatility readings. On each bar, the buffer is sorted and a trimmed mean is computed:
|
||||
|
||||
**Full buffer (128 samples):**
|
||||
$$
|
||||
\hat{V}_t = \frac{1}{65} \sum_{i=32}^{96} \text{sorted}[i]
|
||||
$$
|
||||
|
||||
The middle 65 values (indices 32-96) represent approximately the 25th-75th percentile. Extreme values on both tails are discarded.
|
||||
|
||||
**Partial buffer (16-127 samples):**
|
||||
$$
|
||||
s = \max(5, \text{round}(0.5 \times \text{count}))
|
||||
$$
|
||||
$$
|
||||
k = \lfloor(\text{count} - s) / 2\rfloor
|
||||
$$
|
||||
$$
|
||||
\hat{V}_t = \frac{1}{s} \sum_{i=k}^{k+s-1} \text{sorted}[i]
|
||||
$$
|
||||
|
||||
During warmup, the trim ratio adapts dynamically based on available samples.
|
||||
|
||||
**Why trimmed mean?** A 5% gap-up creates a massive spike in traditional volatility measures. With exponential smoothing, this spike persists—half its effect remains after the EMA period, a quarter after two periods. With trimmed mean, a single spike falls outside the 25th-75th percentile and gets discarded entirely. JVOLTY asks: "Is this volatility reading unusual relative to recent history?" If yes, ignore it.
|
||||
|
||||
### 5. Dynamic Exponent (Output)
|
||||
|
||||
JVOLTY outputs the ratio of current volatility to reference volatility, raised to an adaptive power and clamped:
|
||||
|
||||
$$
|
||||
r_t = \frac{|V_t|}{\hat{V}_t}
|
||||
$$
|
||||
|
||||
$$
|
||||
d_t = \text{clamp}(r_t^{P_{exp}}, 1, \text{logParam})
|
||||
$$
|
||||
|
||||
where:
|
||||
- $P_{exp} = \max(\text{logParam} - 2, 0.5)$
|
||||
- $\text{logParam} = \max(\log_2(\sqrt{L}) + 2, 0)$
|
||||
- $L = (N - 1) / 2$, and $N$ is the period
|
||||
|
||||
This dynamic exponent:
|
||||
- Returns 1.0 during normal volatility (no adaptation needed)
|
||||
- Increases toward `logParam` during high volatility (faster response)
|
||||
- Never drops below 1.0 (bounded minimum smoothing)
|
||||
|
||||
## Mathematical Foundation
|
||||
|
||||
### Adaptive Decay Rate
|
||||
|
||||
The bands decay toward price at a volatility-adjusted rate:
|
||||
|
||||
$$
|
||||
\text{adapt} = \text{sqrtDivider}^{\sqrt{d}}
|
||||
$$
|
||||
|
||||
where:
|
||||
$$
|
||||
\text{sqrtDivider} = \frac{\sqrt{L} \times \text{logParam}}{\sqrt{L} \times \text{logParam} + 1}
|
||||
$$
|
||||
|
||||
Higher volatility (higher $d$) produces faster band decay (more responsive envelope).
|
||||
|
||||
### Warmup Period
|
||||
|
||||
JVOLTY requires substantial warmup to fill the distribution buffer:
|
||||
|
||||
$$
|
||||
W = \lceil 20 + 80 \times N^{0.36} \rceil
|
||||
$$
|
||||
|
||||
For JVOLTY(14): $W = \lceil 20 + 80 \times 14^{0.36} \rceil = \lceil 20 + 80 \times 2.49 \rceil = 220$ bars.
|
||||
|
||||
Additionally, the 128-bar distribution buffer needs to fill before trimmed mean calculations are fully robust. Allow 220 + 128 = 348 bars for maximum accuracy.
|
||||
|
||||
### Trimmed Mean Statistics
|
||||
|
||||
The trimmed mean (Winsorized estimator) has well-known statistical properties:
|
||||
- **Breakdown point**: 25% (robust to contamination up to 25% of samples)
|
||||
- **Efficiency**: ~90% of sample mean under normality
|
||||
- **Bias**: Negligible for symmetric distributions
|
||||
|
||||
For fat-tailed financial returns, trimmed mean significantly outperforms sample mean in terms of mean squared error.
|
||||
|
||||
## Performance Profile
|
||||
|
||||
### Operation Count (Streaming Mode, Scalar)
|
||||
|
||||
Per-bar operations:
|
||||
|
||||
| Operation | Count | Cost (cycles) | Subtotal |
|
||||
| :--- | :---: | :---: | :---: |
|
||||
| ADD/SUB | 45 | 1 | 45 |
|
||||
| MUL | 5 | 3 | 15 |
|
||||
| DIV | 2 | 15 | 30 |
|
||||
| CMP/ABS | 7 | 1 | 7 |
|
||||
| SQRT | 2 | 15 | 30 |
|
||||
| EXP | 2 | 50 | 100 |
|
||||
| POW | 1 | 80 | 80 |
|
||||
| SORT (128 elem) | 1 | ~900 | 900 |
|
||||
| **Total** | **65** | — | **~1,207 cycles** |
|
||||
|
||||
The 128-element sort dominates (~75% of total cycles).
|
||||
|
||||
### Batch Mode (512 values, SIMD/FMA)
|
||||
|
||||
JVOLTY is inherently recursive—each bar depends on previous state. Within-bar vectorization is limited:
|
||||
|
||||
| Optimization | Cycles Saved | New Total |
|
||||
| :--- | :---: | :---: |
|
||||
| SumSIMD for trimmed mean | ~56 | 1,151 |
|
||||
| FMA in band update | ~8 | 1,143 |
|
||||
| **Total SIMD/FMA savings** | **~64 cycles** | **~1,143 cycles** |
|
||||
|
||||
**Batch efficiency (512 bars):**
|
||||
|
||||
| Mode | Cycles/bar | Total (512 bars) | Improvement |
|
||||
| :--- | :---: | :---: | :---: |
|
||||
| Scalar streaming | 1,207 | 617,984 | — |
|
||||
| SIMD/FMA streaming | 1,143 | 585,216 | 5.3% |
|
||||
|
||||
The modest improvement reflects sort dominance and recursive dependencies.
|
||||
|
||||
### Quality Metrics
|
||||
|
||||
| Metric | Score | Notes |
|
||||
| :--- | :---: | :--- |
|
||||
| **Spike Rejection** | 9/10 | Distribution trimming discards outliers |
|
||||
| **Regime Detection** | 8/10 | Tracks sustained changes, ignores noise |
|
||||
| **Stability** | 9/10 | No explosive growth during dislocations |
|
||||
| **Responsiveness** | 7/10 | 10-bar SMA adds slight lag |
|
||||
| **Interpretability** | 8/10 | Output directly measures volatility regime |
|
||||
|
||||
## Validation
|
||||
|
||||
JVOLTY is proprietary. No open-source library implements it. Validation is performed against the decompiled JMA reference implementation.
|
||||
|
||||
| Library | Status | Notes |
|
||||
| :--- | :---: | :--- |
|
||||
| **TA-Lib** | N/A | Not implemented |
|
||||
| **Skender** | N/A | Not implemented |
|
||||
| **Tulip** | N/A | Not implemented |
|
||||
| **Ooples** | N/A | Not implemented |
|
||||
| **JMA Reference** | ✅ | Extracted from JMA; matches decompiled algorithm |
|
||||
|
||||
## Common Pitfalls
|
||||
|
||||
1. **Warmup Period Is Long**: JVOLTY requires ~220 bars (for period=14) plus 128 bars to fill the distribution buffer. The `IsHot` property indicates when the indicator has sufficient data, but full distribution stability requires 348+ bars.
|
||||
|
||||
2. **Output Range**: JVOLTY returns values ≥1.0. A value of 1.0 means "normal volatility" (current matches historical reference). Values above 1.0 indicate elevated volatility relative to the trimmed mean reference. The maximum is bounded by `logParam` (period-dependent).
|
||||
|
||||
3. **Not Traditional Volatility**: JVOLTY is not standard deviation, ATR, or any conventional volatility measure. It's a relative measure: "How does current volatility compare to the robust historical reference?" Direct comparison to other volatility indicators requires normalization.
|
||||
|
||||
4. **Computational Cost**: ~1,200 cycles per bar, dominated by the 128-element sort. For high-frequency applications scanning thousands of symbols, consider caching or reduced update frequency.
|
||||
|
||||
5. **Memory Footprint**: ~1.5 KB per instance (two ring buffers + state). For 5,000 concurrent instances, budget ~7.5 MB.
|
||||
|
||||
6. **Using isNew Incorrectly**: When processing live ticks within the same bar, use `Update(value, isNew: false)`. When a new bar opens, use `isNew: true` (default). Incorrect usage corrupts buffer snapshots and state.
|
||||
|
||||
7. **Comparison with JMA**: JVOLTY is a component of JMA, not a replacement. JMA outputs smoothed price; JVOLTY outputs the volatility regime measure that JMA uses internally for adaptation. Use JVOLTY when you need the volatility signal without the price smoothing.
|
||||
|
||||
## Use Cases
|
||||
|
||||
1. **Volatility Regime Detection**: JVOLTY > 2.0 often indicates a volatility regime shift. Use for strategy switching (trend-following in low volatility, mean-reversion in high volatility).
|
||||
|
||||
2. **Position Sizing**: Inverse volatility weighting: `size = base_size / JVOLTY`. Lower volatility → larger position; higher volatility → smaller position.
|
||||
|
||||
3. **Stop Loss Adjustment**: ATR-based stops can be scaled by JVOLTY. During elevated volatility (JVOLTY > 1.5), widen stops proportionally.
|
||||
|
||||
4. **Filter Adaptation**: Use JVOLTY to adjust other indicator parameters dynamically. Shorter periods during high JVOLTY, longer periods during low JVOLTY.
|
||||
|
||||
5. **Regime Change Alerts**: Track JVOLTY crossovers (e.g., crossing above 1.5 or below 1.2) to signal potential market condition changes.
|
||||
|
||||
## References
|
||||
|
||||
- Jurik Research. (1998-2005). "JMA White Papers." *jurikres.com* (archived).
|
||||
- Kositsin, Nikolay. (2007). "Digital Indicators for MetaTrader 4." *Alpari Forum Archives*.
|
||||
- Wilcox, R. R. (2012). "Introduction to Robust Estimation and Hypothesis Testing." *Academic Press*. (Trimmed mean statistics)
|
||||
@@ -0,0 +1,142 @@
|
||||
// The MIT License (MIT)
|
||||
// © mihakralj
|
||||
//@version=6
|
||||
indicator("Jurik Volatility", "Jvolty", overlay=false)
|
||||
|
||||
//@function Jurik Volatility - extracted volatility component from JMA
|
||||
//@doc Uses 10-bar local deviation + 128-sample trimmed mean distribution
|
||||
//@param source Series to calculate Jvolty from
|
||||
//@param period Number of bars used in the calculation (>= 1)
|
||||
//@returns Normalized volatility measure (1 = low volatility, logParam = high volatility)
|
||||
jvolty(series float source, simple int period) =>
|
||||
// ---- Precomputed length parameters (constant per series) ----
|
||||
simple float _LEN0 = period < 1.0000000002 ? 1e-10 : (period - 1.0) / 2.0
|
||||
simple float _LOG_PARAM = math.max(math.log(math.sqrt(_LEN0)) / math.log(2.0) + 2.0, 0.0)
|
||||
simple float _SQRT_PARAM = math.sqrt(_LEN0) * _LOG_PARAM
|
||||
simple float _SQRT_DIV = _SQRT_PARAM / (_SQRT_PARAM + 1.0)
|
||||
simple float _P_EXP = math.max(_LOG_PARAM - 2.0, 0.5)
|
||||
|
||||
// ---- Internal state (persists across bars) ----
|
||||
var float upperBand = na
|
||||
var float lowerBand = na
|
||||
var int bars = 0
|
||||
|
||||
// 10-bar local deviation window
|
||||
var float cycleDelta = 0.0
|
||||
var int volIndex = 0
|
||||
var int volCount = 0
|
||||
var array<float> volWindow = array.new_float(10, 0.0)
|
||||
|
||||
// 128-bar volatility distribution
|
||||
var int distIndex = 0
|
||||
var int distCount = 0
|
||||
var array<float> distWindow = array.new_float(128, 0.0)
|
||||
var array<float> sorted = array.new_float(0)
|
||||
|
||||
float current_volty = na
|
||||
|
||||
if not na(source)
|
||||
bars += 1
|
||||
|
||||
// ---- First bar: initialize anchors ----
|
||||
if bars == 1
|
||||
upperBand := source
|
||||
lowerBand := source
|
||||
current_volty := 1.0
|
||||
else
|
||||
// 1) Local deviation vs. upperBand / lowerBand
|
||||
float diffA = source - upperBand
|
||||
float diffB = source - lowerBand
|
||||
float absA = math.abs(diffA)
|
||||
float absB = math.abs(diffB)
|
||||
float absValue = absA > absB ? absA : absB
|
||||
float dLocal = absValue + 1e-10
|
||||
|
||||
// 2) 10-bar SMA of local deviation -> highD
|
||||
float oldVol = array.get(volWindow, volIndex)
|
||||
cycleDelta += dLocal - oldVol
|
||||
array.set(volWindow, volIndex, dLocal)
|
||||
volIndex += 1
|
||||
if volIndex >= 10
|
||||
volIndex := 0
|
||||
if volCount < 10
|
||||
volCount += 1
|
||||
float highD = volCount > 0 ? cycleDelta / (volCount < 10 ? volCount : 10) : dLocal
|
||||
|
||||
// 3) 128-bar volatility distribution + trimmed mean
|
||||
array.set(distWindow, distIndex, highD)
|
||||
distIndex += 1
|
||||
if distIndex >= 128
|
||||
distIndex := 0
|
||||
if distCount < 128
|
||||
distCount += 1
|
||||
|
||||
float dRef = highD
|
||||
if distCount >= 16
|
||||
int count = distCount
|
||||
array.clear(sorted)
|
||||
for i = 0 to count - 1
|
||||
int idx = distIndex - 1 - i
|
||||
if idx < 0
|
||||
idx += 128
|
||||
array.push(sorted, array.get(distWindow, idx))
|
||||
array.sort(sorted)
|
||||
|
||||
int idxLo = 0
|
||||
int idxHi = 0
|
||||
if count >= 128
|
||||
idxLo := 32
|
||||
idxHi := 96
|
||||
else
|
||||
int slice = int(math.max(5.0, math.round(count * 0.5)))
|
||||
int drop = (count - slice) / 2
|
||||
idxLo := drop
|
||||
idxHi := drop + slice - 1
|
||||
|
||||
if idxLo < 0
|
||||
idxLo := 0
|
||||
if idxHi >= count
|
||||
idxHi := count - 1
|
||||
|
||||
float sum = 0.0
|
||||
for i = idxLo to idxHi
|
||||
sum += array.get(sorted, i)
|
||||
dRef := sum / float(idxHi - idxLo + 1)
|
||||
|
||||
if dRef <= 0.0
|
||||
dRef := dLocal
|
||||
|
||||
// 4) Jurik dynamic exponent
|
||||
float ratio = absValue / dRef
|
||||
if ratio < 0.0
|
||||
ratio := 0.0
|
||||
float d = math.pow(ratio, _P_EXP)
|
||||
d := math.min(math.max(d, 1.0), _LOG_PARAM)
|
||||
|
||||
// 5) Update upperBand / lowerBand via sqrtDivider ^ sqrt(d)
|
||||
float adapt = math.pow(_SQRT_DIV, math.sqrt(d))
|
||||
if source > upperBand
|
||||
upperBand := source
|
||||
else
|
||||
upperBand := source - (source - upperBand) * adapt
|
||||
if source < lowerBand
|
||||
lowerBand := source
|
||||
else
|
||||
lowerBand := source - (source - lowerBand) * adapt
|
||||
|
||||
current_volty := d
|
||||
|
||||
current_volty
|
||||
|
||||
// ---------- Main loop ----------
|
||||
|
||||
// Inputs
|
||||
i_period = input.int(10, "Period", minval=1, tooltip="Number of bars used in the calculation")
|
||||
i_source = input.source(close, "Source")
|
||||
|
||||
// Calculation
|
||||
jvolty_value = jvolty(i_source, i_period)
|
||||
|
||||
// Plot
|
||||
plot(jvolty_value, "Jvolty", color=color.orange, linewidth=2)
|
||||
hline(1.0, "Min Volatility", color=color.gray, linestyle=hline.style_dotted)
|
||||
Reference in New Issue
Block a user