Files

129 lines
4.1 KiB
Plaintext

// 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))