@pond-ts/financial API Reference
    Preparing search index...

    Function verticalHorizontalFilter

    • Vertical Horizontal Filter (Adam White, 1991) — net movement over total movement: how much of the distance the price walked actually took it somewhere.

      ${output} = (highest − lowest of column over period) / Σ |column[i] − column[i−1]| over period
      

      Appends one column, in 0..1. High is trending, low is choppy — the opposite polarity to choppinessIndex, which measures the reciprocal idea on a log scale. A market that went one way in a straight line has a net range equal to its path length and reads near 1; one that shuffled back and forth reads near period's reciprocal. White's use of it was as a regime filter: run a trend-following rule when it is high and an oscillator when it is low, rather than reading it directionally.

      TA-Lib has no VHF, so the oracle is a pandas replication with the analytic first-valid bar asserted and a measured separation from the plausible wrong turn — the same ratio over a period − 1-long sum of changes, which is the off-by-one this study's window alignment invites (see below).

      • Highest and lowest of the same column the changes are taken over. White's statement is in terms of closes and this study generalises it to any column, one option rather than three, because both halves must read the same field or the ratio is not a ratio of anything. (Some vendors use the bar's high and low for the range and the close for the changes; that mixes two fields and is a different, larger number. The single column shape is what makes this study composable over another study's output, which is the zScore / rollingStdev shape.)
      • A fraction, not a percent. 0.34, not 34 — the historicalVolatility convention: only percentChange and the studies whose names say percent multiply by 100.
      • 28 is White's own default. Both halves take the one period.

      The two halves do not have the same reach. period bars give period − 1 changes, so a period-long sum of changes needs period + 1 closes: it first exists on bar period, one row later than the range's period − 1. The column therefore first lands on bar period — bar 28 at the default — and the range on that bar covers rows [1 … period] while the changes cover the transitions [0 → 1] … [period − 1 → period], i.e. one transition into the range's window. That asymmetry is inherent to the definition (a net range is over bars, a path length is over moves) and is why the oracle separates this study from the version whose sum is one term shorter.

      • Scale-invariant, and shift-invariant. Numerator and denominator are both homogeneous of degree one in price and both are built from differences. Pinned by property tests.
      • Bounded 0 < vhf ≤ 1, and the upper bound is the interesting one. The period − 1 transitions inside the range's window already trace a path from the window's low to its high, so their absolute values sum to at least the range; the sum has one further term on top of them, so the ratio can never exceed 1, and 1 means "no retracement at all in the window". There is no useful lower bound: the extra term is the move into the window and is not bounded by the window's own range, so a single large gap on that bar can drive the reading arbitrarily close to zero. Pinned as (0, 1] rather than as the 1/period floor a symmetric-looking argument would suggest.
      • A window with no movement is 0/0, and needs no guard. If the changes sum to zero then every close in the window is the same one, which forces the range to zero too — so the ratio is JavaScript's own NaN and the study reports undefined on a halted instrument without a branch. There is no input that puts a non-zero numerator over a zero denominator, because both halves read the same column: an explicit guard here would be dead code that no test could kill (the commodityChannelIndex finding, applied rather than copied). Contrast choppinessIndex, whose two halves read different columns and which therefore does need one.
      • A leading gap shifts the start; an interior gap blanks the change on that bar and the next, then every sum over those, after which the study recovers. As in choppinessIndex, the two halves treat the gap differently — core's max/min skip a missing cell, rollingMeanValues does not — so the path-length half is what sets the mask.
      • A misnamed column throws rather than reading empty: max / min fall through to core's sweep, which rejects the name. As in ulcerIndex, that is a reducer-level split — the same rollingValues door with stdev reads all-missing instead — and both behaviours are pinned by tests rather than reconciled here.

      Type Parameters

      • S extends SeriesSchema
      • const Output extends string = "vhf"

      Parameters

      Returns TimeSeries<
          readonly [S[0], ValueColumnsForSchema<S>, OptionalNumberColumn<Output>],
      >