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

    Function zigZag

    • ZigZag — the price path reduced to its swings: every move smaller than deviation percent is discarded, and what is left is a chain of alternating high and low pivots. Appends three columns:

      ${prefix}Pivot      the pivot price, on the pivot's OWN bar; undefined elsewhere
      ${prefix}Direction +1 on a rising leg, −1 on a falling one
      ${prefix}Line the straight line between consecutive pivots, per bar

      A leg turns when price retraces deviation percent from the leg's running extreme: from a peak the retracement is measured against the peak, and from a trough the rise is measured against the trough (so a round trip is not two equal absolute moves). Extremes come from high and low — the standard — with the close-based variant one call away and needing no option of its own, since the two column names are already knobs:

      zigZag(bars, { high: 'close', low: 'close' })     // the close-based fork
      zigZag(withSma, { high: 'sma', low: 'sma' }) // over another study

      A pivot is written at the bar its extreme occurred — but it is not known until a later bar reverses far enough to confirm it. So the value at row i can depend on bars after i, in all three columns:

      • ${prefix}Pivot[k] appears only once some bar i > k confirms it.
      • ${prefix}Line between two pivots interpolates towards a pivot that is still in the future at every bar in between.
      • ${prefix}Direction names the leg a bar belongs to, which for the bars between a pivot and its confirmation is the opposite of what the machine believed at the time.

      That is not an implementation choice — it is what ZigZag is, and it is the corpus assessment's gap G6. The columns are deliberately uniform about it rather than mixing one causal column in among two that repaint: do not feed any of them to a backtest or a feature matrix without lagging them, and treat the whole study as a description of the past. The causal reading a strategy can take is ${prefix}Direction's value at the bar the direction changes, which is the confirmation bar.

      The leg in force at the last bar has no confirmed end, so it has no pivot and no line: ${prefix}Pivot and ${prefix}Line stop at the last confirmed pivot. No ${prefix}Provisional column ships — it would be undefined on every row but one, and the one number it holds is three lines away from the data already on the series:

      // The provisional extreme: the running high (or low) since the last pivot.
      const dir = last(col(r, 'zzDirection'));
      const from = lastIndexWhere(col(r, 'zzPivot'), (v) => v !== undefined);
      const provisional = dir === 1 ? max(highs.slice(from)) : min(lows.slice(from));

      ${prefix}Direction does cover the provisional leg — its direction is the one thing about it that is known — so the tail is not blank.

      ${prefix}Line is set at a pivot exactly when that pivot starts or ends a completed leg, and there it is the pivot's own value, not an interpolation of it. The one pivot that carries no line is a run's only confirmed pivot (the last one of a run has the leg before it, so its line is set): Pivot is 100 on that bar and Line is undefined, because there is no completed leg on either side of it to draw.

      The [PND-SFOLD] rule, applied with the one addition ZigZag needs: a hole in high or low resets the machine, and the provisional leg in force at the hole is discarded — its extreme never saw the reversal that would have confirmed it, and confirming it would be inventing one. No line is drawn across the hole either: the interpolation runs between consecutive pivots of the same run, so a gap ends one chain and the next complete bar starts a fresh one, seeding from scratch.

      The alternative — ending the study at the first gap, as wilderValues does — was considered and rejected. Wilder's seed is a period-bar mean, so restarting it restates what the statistic means; ZigZag's seed is two bars and a threshold, which is exactly what a chart does when a halted instrument resumes. Resetting costs the one leg that spanned the hole and nothing else.

      Length-preserving. All three columns are undefined before the run's first confirmed pivot, which is data-dependent rather than a fixed bar count — there is no period here. The interpolation is linear in row index, not in time: ${prefix}Line on an irregular series is straight in bars, which is what a bar chart draws.

      TA-Lib has no ZigZag, and platforms disagree on the details, so the oracle is a pandas transcription of this same machine (not an independent derivation) whose value is the separations it measures. On the oracle's 80 bars at the default 5%: the close-based fork moves every pivot value (99.15 / 108.64 / 97.81 against 100.00 / 108.04 / 98.65) and at 2% it changes the pivot set as well (6 pivots against 4); the absolute-deviation fork — reading deviation as a price move rather than a percent — finds a fourth pivot at bar 69 that the percent rule does not (at 2% the two agree, so the separation is deviation-dependent and the generator asserts the one that bites).

      The two rules the fixture cannot separate are pinned by unit tests: a bar whose own range exceeds the threshold (which confirms nothing), and a run whose high and low extremes fall on the same bar (which seeds nothing).

      • Scale-equivariant, and NOT shift-invariant. Multiplying every price by k multiplies Pivot and Line by k and leaves the pivot bars and Direction identical — deviation is a percent, so the threshold scales with it. Adding a constant does not leave the study alone: it changes what a percent is worth, and both the pivot set and the columns move. That is asserted as a real property, not waived.
      • Ties keep the EARLIER bar. A later bar whose high merely equals the running extreme does not move it (the comparison is strict), so a double top's pivot sits on the first of the equal bars, and the leg's line is measured from there. This is the opposite of highest / lowest, where the newest equal bar wins; here a bar that added no price information is not allowed to move a pivot that a later reversal will confirm. Pinned by a unit test.
      • deviation is a percent, not a bar count, so it is validated as a positive finite number rather than by assertPeriod.
      • Prices are assumed positive, as everywhere a percent is taken in this package. A non-positive extreme makes the threshold meaningless (a negative base inverts the comparison) rather than throwing — the same answer percentChange gives.
      • Cost is O(N): one fold pass, then one pass over the pivots to fill the direction and the interpolated line. State is O(1) plus one entry per confirmed pivot.

      Type Parameters

      • S extends SeriesSchema
      • const Prefix extends string = "zz"

      Parameters

      Returns TimeSeries<
          readonly [
              S[0],
              ValueColumnsForSchema<
                  readonly [
                      S[0],
                      ValueColumnsForSchema<
                          readonly [
                              S[0],
                              ValueColumnsForSchema<S>,
                              OptionalNumberColumn<`${Prefix}Pivot`>,
                          ],
                      >,
                      OptionalNumberColumn<`${Prefix}Direction`>,
                  ],
              >,
              OptionalNumberColumn<`${Prefix}Line`>,
          ],
      >