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.
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
| Field | Type | Meaning | Required policy |
|---|---|---|---|
| close | finite positive number | finalized close | strictly positive because reversals divide by an extreme |
| observation_index | integer | arrival order | strictly increasing sequence position |
| reversal_fraction | finite fraction | opposite move required | strictly between 0 and 1 |
| min_bars | integer | minimum event-to-confirmation distance | at 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.
| Variant | Definition | Best use | Limitation |
|---|---|---|---|
| This package | close-only percentage reversal with minimum bars | causal swing compression | latest endpoint is provisional |
| High/low ZigZag | uses bar extremes | wick-sensitive geometry | can differ materially from close-only |
| Absolute-move ZigZag | fixed price-unit reversal | stable tick-value context | not scale invariant |
| Volatility-scaled ZigZag | threshold tied to ATR or volatility | changing volatility regimes | inherits estimator warm-up and revision behavior |
The complete mathematical contract
| Symbol | Meaning | Unit |
|---|---|---|
| P_t | current finalized close | price units |
| P_h, P_l | provisional high or low price | price units |
| tau | reversal fraction | fraction |
| m | minimum bar separation | bars |
| t | current confirmation candidate | bar 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 object | Value | State | Result |
|---|---|---|---|
| 0 | 100 | searching | running high and low both 100 |
| 1 | 103 | searching | running high becomes 103 |
| 2 | 106 | searching | running high becomes 106 |
| 3 | 101 | searching | decline 4.71698%; below 5% |
| 4 | 100 | down | decline 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
The diagram has one deliberate shape: validate, calculate only from available evidence, publish diagnostics, and preserve the rejection branch.
The algorithm proceeds as follows:
- Validate strictly positive closes; percentage reversals are undefined at nonpositive bases.
- While searching, track the running high and low until one side reverses by the threshold.
- During an up leg, replace the provisional high only with a strictly higher close.
- Confirm that high after an inclusive threshold reversal and minimum separation.
- 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
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
| Experiment | Question to answer |
|---|---|
| Clean alternating swings | A clean control for confirmation latency and alternating structure. |
| Plateau and equality boundaries | Equal extrema expose the selected earliest-plateau and strict-update policies. |
| Noisy micro-swings | Noise reveals sensitivity to span, reversal, density, and residual thresholds. |
| Repeated level retests | Repeated highs and lows create inspectable support and resistance density. |
| Trend with one gross outlier | One extreme observation distinguishes ordinary fitting from consensus fitting. |
| Level and slope regime shift | A structural shift exposes stale clusters, provisional pivots, and window dependence. |
| Exact reversal-threshold touch | A path that lands on the inclusive 5% boundary distinguishes equality from near misses. |
| Moving provisional extreme | The 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:
- Predict the next state before pressing Step.
- Compare the event index with the knowledge index.
- Move one parameter across its boundary and explain the changed branch.
- Switch to the failure scenario and identify why “no result” is correct.
Where implementations fail
- 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
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
- QuantConnect Zig Zag — sensitivity, minimum trend length, and alternating pivots.
- LEAN ZigZag source — maintained state-machine comparison.
- TradingView repainting — provisional and back-plotted state risk.
The canonical README, reference record, and claim ledger contain the complete definition and evidence classifications.
Rendered from the canonical Mermaid sources linked by this article.
ZigZag Segmentation calculation flow
Purpose: preserve validation, causal availability, decision branches, and diagnostics in the order used by the implementation.
Takeaway: a rejected or unavailable result is part of the output contract; it must not be filled with retrospective geometry.
ReferencesPrimary sources and evidence notesExpand the source trail, evidence role, and limitations behind the engineering choices.
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.
Full dependency-light reference implementations in both supported languages.
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};
}
The embedded lab now expands to its full document height, keeping the article as the only scroll surface.