// Licensed under the Apache License, Version 2.0 // © mihakralj //@version=6 indicator("JVOLTYN - Normalized Jurik Volatility", shorttitle="JVOLTYN", overlay=false) //@function Calculates Normalized Jurik Volatility (0-100 scale) //@param period Number of bars used in the calculation (>= 2) //@param src Source series for volatility measurement //@returns Normalized volatility value (0 = low, 100 = high) //@optimized Uses adaptive bands with distribution-based normalization // Inputs period = input.int(14, "Period", minval=2) src = input.source(close, "Source") // 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.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))