TradingView indicator guides

Vertical Horizontal Filter: trend or congestion?

The Vertical Horizontal Filter (VHF) compares the highest-to-lowest closing-price range with the total absolute close-to-close movement. Higher readings suggest a more direct path through the range; lower readings suggest more backtracking. VHF has no directional sign, so the same value can describe an advance or a decline.

Adam White created VHF. The Trading Technologies formula uses closing prices. Below, the window convention, numeric examples and copyable code make that calculation explicit.

The VHF formula and its extra observation

Rₜ = max(Cₜ, …, Cₜ₋ₙ₊₁) − min(Cₜ, …, Cₜ₋ₙ₊₁)

Dₜ = Σᵢ₌₀ⁿ⁻¹ |Cₜ₋ᵢ − Cₜ₋ᵢ₋₁|

VHFₜ = Rₜ / Dₜ, when Dₜ > 0

C is the close and N is the selected length. The numerator covers N closes. The denominator covers N changes, so it also needs the close immediately before that range. At N = 28, read 29 closes but use only the latest 28 for the extrema. The code applies this same convention to a user-selected source.

This indexing matches the calculation loop in MotiveWave documentation: each of N range observations contributes a change from its predecessor.

The numerator is a maximum-minus-minimum range, not the absolute change from the first close to the last. Do not substitute candle highs and lows into a close-based numerator. That produces a different measure and can invalidate the zero-to-one bound.

Summing N−1 changes instead would use only the movements inside the N-point range. That convention gives different results. On equal-sized monotonic steps, this page's N-change ratio is (N−1)/N, not necessarily one.

Recalculate five four-period examples

Each row lists five closes from oldest to newest. The first is used only to calculate the first change. The final four supply the highest and lowest closes. These are synthetic arithmetic examples, not backtest results.

Four-period VHF: five closes, four absolute changes and the range of the latest four closes
PathCloses, oldest firstRange RTravel DVHF
Steady rise100 → 101 → 102 → 103 → 104104 − 101 = 31 + 1 + 1 + 1 = 40.75
Steady decline104 → 103 → 102 → 101 → 100103 − 100 = 31 + 1 + 1 + 1 = 40.75
Back and forth100 → 102 → 100 → 102 → 100102 − 100 = 22 + 2 + 2 + 2 = 80.25
Entire window flat100 → 100 → 100 → 100 → 100100 − 100 = 00 + 0 + 0 + 0 = 0na
Flat after a gap90 → 100 → 100 → 100 → 100100 − 100 = 010 + 0 + 0 + 0 = 100.00

The rise and decline both return 0.75. The alternating path returns 0.25 even though its last close equals its first close. Flat prices return an undefined 0/0; a flat range after a gap returns zero because the denominator still includes that boundary change. One bar later, that gap leaves the denominator and the result becomes undefined if prices remain flat.

Pine Script v6 with strict window checks

Paste this complete indicator into Pine Editor. Length 28 and thresholds 0.25/0.35 are starting inputs for investigation. The Data Window exposes both parts of the ratio. This is a local calculation, not a built-in function named p_ta_vhf.

Pine Script v6 · indicator
//@version=6
indicator("Vertical Horizontal Filter: window check", overlay=false, precision=4)
source = input.source(close, "Source")
length = input.int(28, "Range bars / number of changes", minval=2, maxval=500)
upper = input.float(0.35, "Upper research threshold", minval=0, maxval=1, step=0.01)
lower = input.float(0.25, "Lower research threshold", minval=0, maxval=1, step=0.01)
confirmedOnly = input.bool(true, "Show only closed chart bars")
if lower >= upper
    runtime.error("Lower threshold must be smaller than upper threshold.")

// Range uses source[0] through source[length - 1].
windowRange = ta.highest(source, length) - ta.lowest(source, length)
// N changes need N + 1 observations, including source[length].
travel = math.sum(math.abs(source - source[1]), length)
// Require the exact contiguous window; never skip or replace missing values.
missing = math.sum(na(source) ? 1.0 : 0.0, length + 1)
ready = bar_index >= length and missing == 0
vhf = ready and travel > 0 ? windowRange / travel : na
showValue = not confirmedOnly or barstate.isconfirmed

plot(showValue ? vhf : na, "VHF", color=color.blue, linewidth=2, style=plot.style_linebr)
hline(upper, "Upper research threshold", color=color.gray, linestyle=hline.style_dashed)
hline(lower, "Lower research threshold", color=color.gray, linestyle=hline.style_dashed)
plot(showValue and ready ? windowRange : na, "N-bar source range", display=display.data_window)
plot(showValue and ready ? travel : na, "N absolute changes", display=display.data_window)

crossedUpper = ta.crossover(vhf, upper)
crossedLower = ta.crossunder(vhf, lower)
upperEvent = not na(vhf) and crossedUpper and barstate.isconfirmed
lowerEvent = not na(vhf) and crossedLower and barstate.isconfirmed
alertcondition(upperEvent, "VHF above upper at close", "VHF crossed above its upper research threshold on a closed chart bar.")
alertcondition(lowerEvent, "VHF below lower at close", "VHF crossed below its lower research threshold on a closed chart bar.")
  • Warm-up requires N+1 observations. Any missing source within that exact window suppresses the value; missing values are neither skipped nor replaced with zero.
  • A zero denominator returns na. A zero numerator with positive movement returns zero. The plot leaves gaps visible.
  • Show only closed chart bars is enabled initially. Turning it off exposes an intrabar value, while alerts still wait for confirmation. Create an alert in TradingView separately.
  • Only the current chart timeframe is used. An external indicator selected as Source may have its own repainting or missing-data behavior.

The bar-close behavior follows TradingView's bar-state documentation. Syntax validation does not prove trading performance or data quality.

Compare VHF, ADX and Choppiness on the same chart

MeasureCalculation basisInterpretationDirection
VHFClose range / absolute close changesHigher: more range per unit of travelUnsigned
ADXSmoothed directional-movement imbalanceHigher: stronger directional imbalanceADX unsigned; +DI/−DI add direction
CHOPLog-scaled true-range sum / high-low rangeHigher: more choppinessUnsigned

Use the same symbol, session, timeframe, evaluation dates and closed bars. For an initial comparison, set VHF and CHOP lengths to 28 and record both ADX inputs, such as DI length 28 and smoothing 28. Equal length labels do not create identical memory: ADX smoothing carries earlier observations, whereas VHF uses a rolling window.

A bar can have wide wicks while its close barely changes. VHF's close path misses that movement; CHOP includes true range, and ADX uses highs and lows for directional movement. Disagreement can therefore reflect different inputs rather than an implementation error. The mechanisms are documented by TradingView for ADX and CHOP.

Test a filter before switching trading rules

VHF above 0.35 is a testable condition, not proof that a trend strategy will work. Below 0.25 does not establish a safe range trade. A threshold crossing can come from an old extreme or a large change leaving the rolling window even when the newest move is small.

  1. Specify one entry, exit and position-sizing rule before adding VHF. Keep costs and execution timing fixed for the comparison.
  2. Compare the baseline with a trend-filter variant requiring closed-bar VHF above an upper level. Specify direction separately. Test a range-filter variant as a separate strategy, rather than assuming the opposite gate must be profitable.
  3. For a stateful switch, define what happens between the two levels: retain the prior state or disallow new entries. Also define whether a regime change closes existing positions. These choices change results.
  4. Pick candidate lengths and levels on a development period, then freeze them for a later evaluation period. Report trade count, exposure, drawdown and net results after costs, including filtered-out trades.

Changing timeframe changes what a bar means. Gaps, session settings and price adjustments also change the path. VHF cannot establish the next direction or guarantee that a regime persists. This is educational analysis, not investment advice.

VHF questions

Turn the window definition into a strategy specification

Use Pineify's Pine Script AI Coding Agent to draft or revise code from an explicit specification. Include the N-close/N-change convention, missing-value rule, bar confirmation and how the VHF gate affects entries and exits. Review the output, run syntax checks and compare its values in TradingView before backtesting. This page does not execute trades or run that comparison for you.

"Add the VHF calculation above to my existing strategy. Use it only to gate new entries on closed bars. Keep the current exit and sizing rules. Return na for incomplete or zero-movement windows, expose the thresholds as inputs, and let me disable the filter for a baseline comparison."
Explore Pine Script AI Coding Agent

Sources and calculation scope

Reviewed October 17, 2026. Platform documentation supports the indicator definitions; the exact window policy, synthetic examples and testing workflow are specified on this page.

Browse TradingView indicator guides