From ce473f8f7f7ba2be8c493c48b1f5e39889d3a9c9 Mon Sep 17 00:00:00 2001 From: Miha Kralj Date: Mon, 14 Oct 2024 08:45:32 -0700 Subject: [PATCH] Jvolty --- .github/workflows/Publish.yml | 3 +- Tests/test_iTBar.cs | 3 +- Tests/test_quantower.cs | 2 + Tests/test_updates_volatility.cs | 104 +++++++++++++++++ lib/volatility/Jvolty.cs | 141 ++++++++++++++++++++++++ lib/volatility/todo.md | 2 +- quantower/Volatility/JvoltyIndicator.cs | 41 +++++++ 7 files changed, 292 insertions(+), 4 deletions(-) create mode 100644 Tests/test_updates_volatility.cs create mode 100644 lib/volatility/Jvolty.cs create mode 100644 quantower/Volatility/JvoltyIndicator.cs diff --git a/.github/workflows/Publish.yml b/.github/workflows/Publish.yml index 34c762b..fae466e 100644 --- a/.github/workflows/Publish.yml +++ b/.github/workflows/Publish.yml @@ -36,7 +36,7 @@ jobs: - name: Install dotnet-sonarscanner run: | - dotnet tool install --global dotnet-sonarscanner + dotnet tool install --global dotnet-sonarscanner --version 5.14.0 dotnet tool install JetBrains.dotCover.GlobalTool --global dotnet tool install dotnet-coverage --global dotnet restore @@ -64,6 +64,7 @@ jobs: /d:sonar.cs.dotcover.reportsPaths=dotcover* \ /d:sonar.scanner.scanAll=false \ /d:sonar.scm.provider=git \ + /d:sonar.issue.ignore.multicriteria=s1944,s2053,s2222,s2259,s2583,s2589,s3329,s3655,s3900,s3949,s3966,s4158,s4347,s5773,s6781 \ $PR_PARAMS $BRANCH_PARAMS - name: Build diff --git a/Tests/test_iTBar.cs b/Tests/test_iTBar.cs index 83aac9b..1cf5f78 100644 --- a/Tests/test_iTBar.cs +++ b/Tests/test_iTBar.cs @@ -3,8 +3,6 @@ using System.Diagnostics.CodeAnalysis; using System.Security.Cryptography; -#pragma warning disable S1944, S2053, S2222, S2259, S2583, S2589, S3329, S3655, S3900, S3949, S3966, S4158, S4347, S5773, S6781 - namespace QuanTAlib; /// @@ -27,6 +25,7 @@ public BarIndicatorTests() private static readonly ITValue[] indicators = new ITValue[] { new Atr(period: 14), + new Jvolty(period: 14) // Add other TBar-based indicators here }; diff --git a/Tests/test_quantower.cs b/Tests/test_quantower.cs index 440040f..844f173 100644 --- a/Tests/test_quantower.cs +++ b/Tests/test_quantower.cs @@ -95,6 +95,8 @@ public class QuantowerTests // Volatility Indicators [Fact] public void Atr() => TestIndicator("atr"); + [Fact] public void Jvolty() => TestIndicator("jvolty"); + [Fact] public void Historical() => TestIndicator("historical"); [Fact] public void Realized() => TestIndicator("realized"); [Fact] public void Rvi() => TestIndicator("rvi"); diff --git a/Tests/test_updates_volatility.cs b/Tests/test_updates_volatility.cs new file mode 100644 index 0000000..901006c --- /dev/null +++ b/Tests/test_updates_volatility.cs @@ -0,0 +1,104 @@ +using Xunit; +using System.Security.Cryptography; + +namespace QuanTAlib.Tests; + +public class VolatilityUpdateTests +{ + private readonly RandomNumberGenerator rng = RandomNumberGenerator.Create(); + private const int RandomUpdates = 100; + private const double ReferenceValue = 100.0; + private const int precision = 8; + + private double GetRandomDouble() + { + byte[] bytes = new byte[8]; + rng.GetBytes(bytes); + return (double)BitConverter.ToUInt64(bytes, 0) / ulong.MaxValue * 200 - 100; // Range: -100 to 100 + } + + private TBar GetRandomBar(bool IsNew) + { + double open = GetRandomDouble(); + double high = open + Math.Abs(GetRandomDouble()); + double low = open - Math.Abs(GetRandomDouble()); + double close = low + (high - low) * GetRandomDouble(); + return new TBar(DateTime.Now, open, high, low, close, 1000, IsNew); + } + + [Fact] + public void Atr_Update() + { + var indicator = new Atr(period: 14); + TBar r = GetRandomBar(true); + double initialValue = indicator.Calc(r); + + for (int i = 0; i < RandomUpdates; i++) + { + indicator.Calc(GetRandomBar(IsNew: false)); + } + double finalValue = indicator.Calc(new TBar(r.Time, r.Open, r.High, r.Low, r.Close, r.Volume, IsNew: false)); + + Assert.Equal(initialValue, finalValue, precision); + } + + [Fact] + public void Historical_Update() + { + var indicator = new Historical(period: 14); + double initialValue = indicator.Calc(new TBar(DateTime.Now, ReferenceValue, ReferenceValue, ReferenceValue, ReferenceValue, 1000, IsNew: true)); + + for (int i = 0; i < RandomUpdates; i++) + { + indicator.Calc(GetRandomBar(false)); + } + double finalValue = indicator.Calc(new TBar(DateTime.Now, ReferenceValue, ReferenceValue, ReferenceValue, ReferenceValue, 1000, IsNew: false)); + + Assert.Equal(initialValue, finalValue, precision); + } + + [Fact] + public void Jvolty_Update() + { + var indicator = new Jvolty(period: 14); + double initialValue = indicator.Calc(new TBar(DateTime.Now, ReferenceValue, ReferenceValue, ReferenceValue, ReferenceValue, 1000, IsNew: true)); + + for (int i = 0; i < RandomUpdates; i++) + { + indicator.Calc(GetRandomBar(false)); + } + double finalValue = indicator.Calc(new TBar(DateTime.Now, ReferenceValue, ReferenceValue, ReferenceValue, ReferenceValue, 1000, IsNew: false)); + + Assert.Equal(initialValue, finalValue, precision); + } + + [Fact] + public void Realized_Update() + { + var indicator = new Realized(period: 14); + double initialValue = indicator.Calc(new TBar(DateTime.Now, ReferenceValue, ReferenceValue, ReferenceValue, ReferenceValue, 1000, IsNew: true)); + + for (int i = 0; i < RandomUpdates; i++) + { + indicator.Calc(GetRandomBar(false)); + } + double finalValue = indicator.Calc(new TBar(DateTime.Now, ReferenceValue, ReferenceValue, ReferenceValue, ReferenceValue, 1000, IsNew: false)); + + Assert.Equal(initialValue, finalValue, precision); + } + + [Fact] + public void Rvi_Update() + { + var indicator = new Rvi(period: 14); + double initialValue = indicator.Calc(new TBar(DateTime.Now, ReferenceValue, ReferenceValue, ReferenceValue, ReferenceValue, 1000, IsNew: true)); + + for (int i = 0; i < RandomUpdates; i++) + { + indicator.Calc(GetRandomBar(false)); + } + double finalValue = indicator.Calc(new TBar(DateTime.Now, ReferenceValue, ReferenceValue, ReferenceValue, ReferenceValue, 1000, IsNew: false)); + + Assert.Equal(initialValue, finalValue, precision); + } +} diff --git a/lib/volatility/Jvolty.cs b/lib/volatility/Jvolty.cs new file mode 100644 index 0000000..dba3208 --- /dev/null +++ b/lib/volatility/Jvolty.cs @@ -0,0 +1,141 @@ +/// +/// Represents a Jurik Volatility (Jvolty) calculator, a measure of market volatility based on Jurik Moving Average (JMA) concepts. +/// + +namespace QuanTAlib; + +public class Jvolty : AbstractBase +{ + private readonly int _period; + private readonly CircularBuffer _values; + private readonly CircularBuffer _voltyShort; + private readonly CircularBuffer _vsumBuff; + private readonly CircularBuffer _avoltyBuff; + + private double _len1; + private double _pow1; + private double _upperBand; + private double _lowerBand; + private double _p_upperBand; + private double _p_lowerBand; + + /// + /// Initializes a new instance of the Jvolty class with the specified parameters. + /// + /// The period over which to calculate the Jvolty. + /// The phase parameter for the JMA-style calculation. + /// The short-term volatility period. + /// + /// Thrown when period is less than 1. + /// + public Jvolty(int period, int vshort = 10) + { + if (period < 1) + { + throw new ArgumentOutOfRangeException(nameof(period), "Period must be greater than or equal to 1."); + } + _period = period; + int _vlong = 65; + + _values = new CircularBuffer(period); + _voltyShort = new CircularBuffer(vshort); + _vsumBuff = new CircularBuffer(_vlong); + _avoltyBuff = new CircularBuffer(2); + + WarmupPeriod = period * 2; + Name = $"JVOLTY({period},{vshort})"; + } + + /// + /// Initializes a new instance of the Jvolty class with the specified source and parameters. + /// + /// The source object to subscribe to for bar updates. + /// The period over which to calculate the Jvolty. + /// The phase parameter for the JMA-style calculation. + /// The short-term volatility period. + public Jvolty(object source, int period, int vshort = 10) : this(period, vshort) + { + var pubEvent = source.GetType().GetEvent("Pub"); + pubEvent?.AddEventHandler(source, new BarSignal(Sub)); + } + + /// + /// Initializes the Jvolty instance by setting up the initial state. + /// + public override void Init() + { + base.Init(); + _upperBand = _lowerBand = 0.0; + _p_upperBand = _p_lowerBand = 0.0; + _len1 = Math.Max((Math.Log(Math.Sqrt(_period - 1)) / Math.Log(2.0)) + 2.0, 0); + _pow1 = Math.Max(_len1 - 2.0, 0.5); + _avoltyBuff.Clear(); + _avoltyBuff.Add(0, true); + _avoltyBuff.Add(0, true); + } + + /// + /// Manages the state of the Jvolty instance based on whether a new bar is being processed. + /// + /// Indicates whether the current input is a new bar. + protected override void ManageState(bool isNew) + { + if (isNew) + { + _index++; + _p_upperBand = _upperBand; + _p_lowerBand = _lowerBand; + } + else + { + _upperBand = _p_upperBand; + _lowerBand = _p_lowerBand; + } + } + + /// + /// Performs the Jvolty calculation for the current bar. + /// + /// + /// The calculated Jvolty value for the current bar. + /// + protected override double Calculation() + { + ManageState(BarInput.IsNew); + + _values.Add(BarInput.Close, BarInput.IsNew); + + if (_index == 1) + { + return 0; + } + + double hprice = _values.Max(); + double lprice = _values.Min(); + + double del1 = hprice - _upperBand; + double del2 = lprice - _lowerBand; + double volty = Math.Max(Math.Abs(del1), Math.Abs(del2)); + + _voltyShort.Add(volty, BarInput.IsNew); + double vsum = _vsumBuff.Newest() + 0.1 * (volty - _voltyShort.Oldest()); + _vsumBuff.Add(vsum, BarInput.IsNew); + + double prevAvolty = _avoltyBuff.Newest(); + double avolty = prevAvolty + 2.0 / (Math.Max(4.0 * _period, 30) + 1.0) * (vsum - prevAvolty); + _avoltyBuff.Add(avolty, BarInput.IsNew); + + double dVolty = (avolty > 0) ? volty / avolty : 0; + dVolty = Math.Min(Math.Max(dVolty, 1.0), Math.Pow(_len1, 1.0 / _pow1)); + + double pow2 = Math.Pow(dVolty, _pow1); + double len2 = Math.Sqrt(0.5 * (_period - 1)) * _len1; + double Kv = Math.Pow(len2 / (len2 + 1), Math.Sqrt(pow2)); + + _upperBand = (del1 > 0) ? hprice : hprice - (Kv * del1); + _lowerBand = (del2 < 0) ? lprice : lprice - (Kv * del2); + + IsHot = _index >= WarmupPeriod; + return volty; + } +} diff --git a/lib/volatility/todo.md b/lib/volatility/todo.md index 2ce815c..5a4aa17 100644 --- a/lib/volatility/todo.md +++ b/lib/volatility/todo.md @@ -2,7 +2,7 @@ ## Single Value Input (Typically Closing Prices) -- Jurik Volatility (Volty) +- **Jurik Volatility (Volty)** - **Standard Deviation** - **Relative Volatility Index (RVI)** - Ulcer Index diff --git a/quantower/Volatility/JvoltyIndicator.cs b/quantower/Volatility/JvoltyIndicator.cs new file mode 100644 index 0000000..9db2334 --- /dev/null +++ b/quantower/Volatility/JvoltyIndicator.cs @@ -0,0 +1,41 @@ +using System.Drawing; +using TradingPlatform.BusinessLayer; + +namespace QuanTAlib; + +public class JvoltyIndicator : Indicator, IWatchlistIndicator +{ + [InputParameter("Periods", sortIndex: 1, 1, 2000, 1, 0)] + public int Periods { get; set; } = 20; + + private Jvolty? jvolty; + protected LineSeries? JvoltySeries; + public static int MinHistoryDepths => 2; + int IWatchlistIndicator.MinHistoryDepths => MinHistoryDepths; + + public JvoltyIndicator() + { + Name = "JVOLTY - Mark Jurik's Volatility"; + Description = "Measures market volatility according to Mark Jurik."; + SeparateWindow = true; + + JvoltySeries = new("JVOLTY", Color.Blue, 2, LineStyle.Solid); + AddLineSeries(JvoltySeries); + } + + protected override void OnInit() + { + jvolty = new (Periods); + base.OnInit(); + } + + protected override void OnUpdate(UpdateArgs args) + { + TBar input = IndicatorExtensions.GetInputBar(this, args); + TValue result = jvolty!.Calc(input); + + JvoltySeries!.SetValue(result.Value); + } + + public override string ShortName => $"JVOLTY ({Periods})"; +}