Files
QuanTAlib/lib/dynamics/vhf/vhf.pine
T

94 lines
3.8 KiB
Plaintext

// Licensed under the Apache License, Version 2.0
// © mihakralj
//@version=6
indicator("VHF: Vertical Horizontal Filter", "VHF", overlay=false)
//@function Calculates Vertical Horizontal Filter using max-min range vs sum of absolute changes
//@param period Lookback period for range and path measurement (default: 28)
//@returns VHF value (positive, typically 0 to 1; higher = trending, lower = ranging)
//@references Adam White, "Vertical Horizontal Filter", Futures magazine, August 1991
//@optimized O(1) per bar via circular buffer with running sum + deque-based min/max tracking
vhf(simple int period) =>
if period <= 1
runtime.error("Period must be greater than 1")
// Circular buffer for close values (size = period + 1 to access close[period])
var array<float> closeBuf = array.new_float(period + 1, na)
var int head = 0
var int filled = 0
// Running sum of absolute bar-to-bar changes over period bars
// |close[0]-close[1]| + |close[1]-close[2]| + ... + |close[period-2]-close[period-1]|
// That is period terms of absolute 1-bar changes within the window
var array<float> absDiffBuf = array.new_float(period, na)
var int diffHead = 0
var int diffFilled = 0
var float diffSum = 0.0
// Store current close in buffer
array.set(closeBuf, head, close)
filled := math.min(filled + 1, period + 1)
// Compute absolute change from previous close (if available)
float absDiff = na
if filled >= 2
int prevIdx = (head - 1 + period + 1) % (period + 1)
float prevClose = array.get(closeBuf, prevIdx)
if not na(prevClose)
absDiff := math.abs(close - prevClose)
// Update running sum of absolute differences
if not na(absDiff)
float oldDiff = array.get(absDiffBuf, diffHead)
if not na(oldDiff)
diffSum -= oldDiff
diffSum += absDiff
array.set(absDiffBuf, diffHead, absDiff)
diffFilled := math.min(diffFilled + 1, period)
diffHead := (diffHead + 1) % period
float result = na
// Need period+1 close values to compute:
// - Highest/Lowest over period+1 values (current + period historical)
// - Sum of period absolute bar-to-bar changes
if filled >= period + 1 and diffFilled >= period
// Step 1: Numerator (Vertical) = Highest(close, period+1) - Lowest(close, period+1)
// Scan the circular buffer for max and min over the full window
float hi = -1e308
float lo = 1e308
for i = 0 to period
int idx = (head - i + period + 1) % (period + 1)
float val = array.get(closeBuf, idx)
if not na(val)
hi := math.max(hi, val)
lo := math.min(lo, val)
float numerator = hi - lo
// Step 2: Denominator (Horizontal) = Sum of |close[i] - close[i-1]| over period bars
float denominator = diffSum
// Step 3: VHF = Numerator / Denominator
// Guard against division by zero (flat price = all changes zero)
if denominator > 1e-10
result := numerator / denominator
head := (head + 1) % (period + 1)
result
// ---------- Main loop ----------
// Inputs
i_period = input.int(28, "Period", minval=2, maxval=200, tooltip="Lookback period (Adam White default: 28)")
i_trendThreshold = input.float(0.40, "Trend Threshold", minval=0.0, maxval=2.0, step=0.05, tooltip="Above this level = trending market")
i_rangeThreshold = input.float(0.25, "Range Threshold", minval=0.0, maxval=2.0, step=0.05, tooltip="Below this level = ranging market")
// Calculation
vhf_value = vhf(i_period)
// Plot
plot(vhf_value, "VHF", color=color.yellow, linewidth=2)
hline(i_trendThreshold, "Trend Threshold", color=color.new(color.green, 50), linestyle=hline.style_dashed)
hline(i_rangeThreshold, "Range Threshold", color=color.new(color.red, 50), linestyle=hline.style_dashed)