D08-F01-A02 / Complete engineering topic

ZigZag Segmentation

A production-minded guide to ZigZag Segmentation.

D08 · GEOMETRIC CHART PATTERNS
D08-F01-A02Canonical / Tested / Open
D08 / D08-F01

Which price swings are large enough to become confirmed segments rather than provisional noise? That is the practical question behind this tutorial. ZigZag replaces hundreds of closes with alternating confirmed swings plus one provisional endpoint. The compression is useful only when the provisional state and its confirmation clock remain visible.

The goal is not to make a persuasive chart. It is to build an object whose definition, clocks, parameters, invalid states, and diagnostics survive code review and point-in-time testing.

The useful output before the implementation

Use ZigZag for swing segmentation, labeling, compression, or as an input to pattern candidates. Do not use the final provisional endpoint as if it had always existed at its latest location.

The selected package variant is Close-only percentage reversal, minimum bar separation, strict extreme updates, and a separate provisional endpoint. A different price field, threshold unit, equality rule, seed, window, or repainting policy defines a different algorithm. Record the convention with every result.

Knowledge-time map for ZigZag Segmentation

The visual separates where geometry occurred from when the required evidence arrived. That distinction is the family’s central protection against hindsight.

Inputs, ownership, and the rejection boundary

FieldTypeMeaningRequired policy
closefinite positive numberfinalized closestrictly positive because reversals divide by an extreme
observation_indexintegerarrival orderstrictly increasing sequence position
reversal_fractionfinite fractionopposite move requiredstrictly between 0 and 1
min_barsintegerminimum event-to-confirmation distanceat least 1

Synthetic fixtures are clearly labeled and use one instrument basis. Production data must additionally carry source ownership, session, time zone, adjustment basis, and finality. Missing, malformed, mixed-basis, future-dated, or revised inputs are not silently repaired.

Build intuition before notation

ZigZag compresses a price path into alternating swings, but its newest endpoint is provisional until an opposite move crosses the reversal threshold. The endpoint drawn at the newest extreme is a mutable candidate, not a confirmed historical fact. Strict extreme updates preserve the earliest equal high or low; threshold equality confirms because the reversal comparison is inclusive.

The nearby methods below can all be reasonable, but their outputs are not interchangeable.

VariantDefinitionBest useLimitation
This packageclose-only percentage reversal with minimum barscausal swing compressionlatest endpoint is provisional
High/low ZigZaguses bar extremeswick-sensitive geometrycan differ materially from close-only
Absolute-move ZigZagfixed price-unit reversalstable tick-value contextnot scale invariant
Volatility-scaled ZigZagthreshold tied to ATR or volatilitychanging volatility regimesinherits estimator warm-up and revision behavior

The complete mathematical contract

confirm high h at t:1PtPhτ,thm\text{confirm high }h\text{ at }t:\quad 1-\frac{P_t}{P_h}\ge\tau,\quad t-h\ge m confirm low  at t:PtP1τ,tm\text{confirm low }\ell\text{ at }t:\quad \frac{P_t}{P_\ell}-1\ge\tau,\quad t-\ell\ge m up leg update: Pt>Ph;down leg update: Pt<P\text{up leg update: }P_t>P_h;\qquad \text{down leg update: }P_t<P_\ell
SymbolMeaningUnit
P_tcurrent finalized closeprice units
P_h, P_lprovisional high or low priceprice units
taureversal fractionfraction
mminimum bar separationbars
tcurrent confirmation candidatebar index

Plain-language interpretation: every output is the consequence of a declared comparison. Strict and inclusive boundaries, threshold transforms, clock cutoffs, and null results are part of the method, not cleanup details.

Reproduce the canonical trace

These are author-derived calculations over synthetic input:

Index or objectValueStateResult
0100searchingrunning high and low both 100
1103searchingrunning high becomes 103
2106searchingrunning high becomes 106
3101searchingdecline 4.71698%; below 5%
4100downdecline 5.66038%; confirm high event 2 at index 4

Start at 100 with a 5% threshold. Prices rise through 103 to 106, so the running high is 106. A fall to 101 is only 4.72% and does not confirm the high. A fall to 100 is 5.66%; the high at 106 becomes confirmed on the 100 observation. Its event time and confirmation time are different, while the new low endpoint remains provisional.

Work through the trace without calling the implementation. Independent arithmetic is the only way to catch a program that is consistently wrong in both languages.

From data to a publishable object

Rendering system map…

The diagram has one deliberate shape: validate, calculate only from available evidence, publish diagnostics, and preserve the rejection branch.

The algorithm proceeds as follows:

  1. Validate strictly positive closes; percentage reversals are undefined at nonpositive bases.
  2. While searching, track the running high and low until one side reverses by the threshold.
  3. During an up leg, replace the provisional high only with a strictly higher close.
  4. Confirm that high after an inclusive threshold reversal and minimum separation.
  5. Switch direction, start a provisional low, and repeat symmetrically.

The pseudocode and implementations preserve this ordering so a reconciliation log can identify the first divergent step.

What must remain true

  • Confirmed pivots alternate kind.
  • Confirmed count never decreases during an unrevised run.
  • A provisional endpoint may move only farther in the active direction.
  • The first anchor is not confirmed merely because it is first.
  • Reset must clear direction, running extremes, candidates, and confirmed pivots.

These properties are more useful than a screenshot test. They describe behavior across canonical, boundary, failure, and revised inputs.

See the decision boundary move

Parameter decision boundary for ZigZag Segmentation

Compare 4.99%, 5.00%, and 5.01% reversals, then increase minimum bars. The threshold decides whether the price move is large enough; minimum bars decides whether it lasted long enough. A changed answer after a parameter change is not automatically a bug; an undocumented parameter change is.

Eight experiments instead of one perfect chart

ExperimentQuestion to answer
Clean alternating swingsA clean control for confirmation latency and alternating structure.
Plateau and equality boundariesEqual extrema expose the selected earliest-plateau and strict-update policies.
Noisy micro-swingsNoise reveals sensitivity to span, reversal, density, and residual thresholds.
Repeated level retestsRepeated highs and lows create inspectable support and resistance density.
Trend with one gross outlierOne extreme observation distinguishes ordinary fitting from consensus fitting.
Level and slope regime shiftA structural shift exposes stale clusters, provisional pivots, and window dependence.
Exact reversal-threshold touchA path that lands on the inclusive 5% boundary distinguishes equality from near misses.
Moving provisional extremeThe newest endpoint keeps moving until an opposite reversal locks the swing.

Each scenario contains 300 observations and eight checkpoints. The lab starts at an informative state, hides future observations, exposes the current formula trace, and lets you change the parameters without deleting the full fixture.

Open the ZigZag Segmentation guided lab.

Use the lab in four passes:

  1. Predict the next state before pressing Step.
  2. Compare the event index with the knowledge index.
  3. Move one parameter across its boundary and explain the changed branch.
  4. Switch to the failure scenario and identify why “no result” is correct.

Where implementations fail

Failure and rejection atlas for ZigZag Segmentation

  • The first anchor is not automatically a confirmed pivot.
  • Threshold equality confirms because the comparison is inclusive.
  • Equal extremes retain the earliest event index.
  • The current endpoint is not historical truth and may move before confirmation.
  • Different high/low, close-only, absolute-move, and volatility-scaled variants are materially different.

The principal misuse is Treating the latest ZigZag endpoint as locked or using a retrospectively perfect line in a causal backtest. Detection correctness is separate from historical association, and historical association is separate from value after costs, bias controls, and out-of-sample testing.

Python, TypeScript, and reconciliation

The Python and TypeScript references consume the same declared semantics. Tests cover the canonical trace, exact boundaries, invalid states, temporal ordering, deterministic choices, and the enriched fixture shape. Numeric results use 1e-9; kinds, indexes, memberships, nulls, and rejection behavior are exact.

When production implementations disagree, compare source identity and adjustment basis first, parameters second, and the earliest intermediate state third. Comparing only final chart lines hides the actual defect.

How the family fits together

Family handoff for ZigZag Segmentation

Causal pivots provide honest extrema. ZigZag provides alternating swing compression. Support/resistance clustering provides horizontal density. Robust trendline fitting provides sloped consensus. Each stage answers a different question and must preserve upstream confirmation time.

The next tutorial is Support/Resistance Clustering.

Evidence and historical boundary

The historical-example decision is not useful. The synthetic paths isolate the mechanism more cleanly than a selected security chart. A named case would require licensed point-in-time OHLC, verified identity, session and adjustment basis, frozen parameters, finality, and a separate empirical design.

This tutorial establishes implementation agreement and intended detection. It does not establish prediction, causation, profitability, or investment suitability.

Primary sources

The canonical README, reference record, and claim ledger contain the complete definition and evidence classifications.

ZigZag Segmentation calculation flow

Purpose: preserve validation, causal availability, decision branches, and diagnostics in the order used by the implementation.

Rendering system map…

Takeaway: a rejected or unavailable result is part of the output contract; it must not be filled with retrospective geometry.

ReferencesPrimary sources and evidence notes

Expand the source trail, evidence role, and limitations behind the engineering choices.

LEAN_ZIGZAG_DOC — Zig Zag indicator

  • Organization or authors: QuantConnect
  • Source type: Maintained official platform documentation
  • Publication or effective date: Maintained documentation
  • Version: QuantConnect LEAN documentation current on 2026-07-27
  • URL: https://www.quantconnect.com/docs/v2/writing-algorithms/indicators/supported-indicators/zig-zag
  • Accessed: 2026-07-27
  • Jurisdiction: technical; not jurisdiction-specific
  • Supports: Sensitivity threshold, minimum trend length, alternating turning points, readiness, and reset behavior.
  • Limitations: This package uses a close-only percentage-reversal teaching variant and documents the difference.
  • Evidence role: sourced algorithm or maintained-library behavior; the financial application remains a package choice.
  • Publication boundary: no provider observation, historical return, causal outcome, or profitability claim.

LEAN_ZIGZAG_CODE — LEAN ZigZag source

  • Organization or authors: QuantConnect
  • Source type: Maintained Apache-2.0 reference implementation
  • Publication or effective date: Maintained source
  • Version: LEAN source snapshot accessed 2026-07-27
  • URL: https://www.lean.io/docs/v2/lean-engine/class-reference/cs/ZigZag_8cs_source.html
  • Accessed: 2026-07-27
  • Jurisdiction: technical; not jurisdiction-specific
  • Supports: Explicit current trend, candidate extreme, sensitivity, and minimum-length state.
  • Limitations: Source behavior is evidence for a maintained implementation, not a universal market convention.
  • Evidence role: sourced algorithm or maintained-library behavior; the financial application remains a package choice.
  • Publication boundary: no provider observation, historical return, causal outcome, or profitability claim.

TV_REPAINT — Pine Script Concepts: Repainting

  • Organization or authors: TradingView
  • Source type: Maintained official platform documentation
  • Publication or effective date: Maintained documentation
  • Version: Pine Script v6 concepts page current on 2026-07-27
  • URL: https://www.tradingview.com/pine-script-docs/concepts/repainting/
  • Accessed: 2026-07-27
  • Jurisdiction: technical; not jurisdiction-specific
  • Supports: A pivot plotted back on its event bar is only discovered after right-side bars have elapsed and can mislead if knowledge time is hidden.
  • Limitations: The page explains platform display behavior, not the financial usefulness of pivots.
  • Evidence role: sourced algorithm or maintained-library behavior; the financial application remains a package choice.
  • Publication boundary: no provider observation, historical return, causal outcome, or profitability claim.
zigzag.ts
export type ZigZagPivot = { kind: "high" | "low"; event_index: number; confirmation_index: number; price: number };
export type ZigZagState = { state: "searching" | "up" | "down"; confirmed_event: ZigZagPivot | null; confirmed_count: number; provisional_kind: "high" | "low" | "unknown"; provisional_index: number | null; provisional_price: number | null };
export function zigzagSegment(closes: number[], threshold = 0.05, minBars = 3): {pivots: ZigZagPivot[]; states: ZigZagState[]} {
  if (!Array.isArray(closes) || closes.length === 0 || closes.some(x => !Number.isFinite(x) || x <= 0)) throw new Error("closes must be finite and positive");
  if (!(threshold > 0 && threshold < 1) || !Number.isInteger(minBars) || minBars < 1) throw new Error("invalid parameters");
  let state: "searching" | "up" | "down" = "searching", hi = 0, lo = 0, candidate = 0;
  const pivots: ZigZagPivot[] = [], states: ZigZagState[] = [];
  closes.forEach((price, index) => {
    let event: ZigZagPivot | null = null;
    if (state === "searching") {
      if (price > closes[hi]) hi = index; if (price < closes[lo]) lo = index;
      if (price / closes[lo] - 1 + 1e-12 >= threshold && index - lo >= minBars) { event={kind:"low",event_index:lo,confirmation_index:index,price:closes[lo]}; pivots.push(event); state="up"; candidate=index; }
      else if (1 - price / closes[hi] + 1e-12 >= threshold && index - hi >= minBars) { event={kind:"high",event_index:hi,confirmation_index:index,price:closes[hi]}; pivots.push(event); state="down"; candidate=index; }
    } else if (state === "up") {
      if (price > closes[candidate]) candidate=index;
      if (1 - price / closes[candidate] + 1e-12 >= threshold && index-candidate >= minBars) { event={kind:"high",event_index:candidate,confirmation_index:index,price:closes[candidate]}; pivots.push(event); state="down"; candidate=index; }
    } else {
      if (price < closes[candidate]) candidate=index;
      if (price / closes[candidate] - 1 + 1e-12 >= threshold && index-candidate >= minBars) { event={kind:"low",event_index:candidate,confirmation_index:index,price:closes[candidate]}; pivots.push(event); state="up"; candidate=index; }
    }
    states.push({state,confirmed_event:event,confirmed_count:pivots.length,provisional_kind:state==="up"?"high":state==="down"?"low":"unknown",provisional_index:state==="searching"?null:candidate,provisional_price:state==="searching"?null:closes[candidate]});
  });
  return {pivots,states};
}
Full-height labzigzag segmentation labOpen full screen