D08-F01-A01 / Complete engineering topic

Causal Pivot Detection

A production-minded guide to Causal Pivot Detection.

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

Which past high or low has become confirmable using only information available now? That is the practical question behind this tutorial. A causal pivot stream gives downstream pattern code an honest set of extrema. It prevents a chart marker placed on an old bar from pretending that the marker was known on that old bar.

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 the stream as a causal geometry input, an audit anchor, or a labeling primitive. Do not interpret a pivot as a trade, reversal forecast, or support/resistance level without a separate tested method.

The selected package variant is Asymmetric left/right OHLC extrema with earliest-plateau equality and a price-unit separation floor. 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 Causal Pivot Detection

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
timestampISO-8601 stringbar close and availability timestrictly increasing; finalized bar only
highfinite numberbar high in price unitssame instrument, session, and adjustment basis
lowfinite numberbar low in price unitsmust not exceed high
closefinite numberfinalized close in price unitsmust lie inside high-low
openoptional finite numberfixture and display fieldnot consumed by the detector

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

A turning point is not knowable when it first appears. Right-side observations must arrive before the earlier bar can be classified. The strict-left and inclusive-right rule gives the earliest member of an equal plateau ownership. Reversing those inequalities gives a different valid convention. The inequality direction is part of the data contract.

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

VariantDefinitionBest useLimitation
This packageOHLC high/low; asymmetric legs; earliest plateau; causallive-safe extrema handoffrequires R future bars before publication
Symmetric platform pivotsame left/right legs; platform equality ruleschart scriptingplateau ownership may differ
SciPy local peakretrospective sample maxima with optional prominence, distance, widthoffline signal analysisnot a causal OHLC pivot by itself
Close-only pivotuses closes instead of intrabar extremesclose-based systemscan miss wick extrema

The complete mathematical contract

high pivot at i:Hi>maxj=iLi1Hj,Himaxj=i+1i+RHj\text{high pivot at }i:\quad H_i>\max_{j=i-L}^{i-1}H_j,\quad H_i\ge\max_{j=i+1}^{i+R}H_j low pivot at i:Li<minj=iLi1Lj,Liminj=i+1i+RLj\text{low pivot at }i:\quad L_i<\min_{j=i-L}^{i-1}L_j,\quad L_i\le\min_{j=i+1}^{i+R}L_j sihigh=Himax(Hleft,Hright),silow=min(Lleft,Lright)Lis_i^{high}=H_i-\max(H_{left},H_{right}),\quad s_i^{low}=\min(L_{left},L_{right})-L_i ki=i+R,publish only when sismink_i=i+R,\quad \text{publish only when }s_i\ge s_{min}
SymbolMeaningUnit
icandidate event indexbar index
L, Rleft and right spansbars
H_i, L_icandidate high and lowprice units
s_minminimum separationprice units
k_iconfirmation or knowledge indexbar 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
010left warm-upnot eligible
112left warm-upnot eligible
215candidate; right bars missingnot publishable
313one right bar observednot publishable
41115 beats left 10,12 and right 13,11publish high event 2, confirmation 4

For highs [10, 12, 15, 13, 11] with L=2, R=2, and zero separation, index 2 is above both left highs (10, 12) and both right highs (13, 11). The event belongs to index 2, but it becomes knowable only at index 4. Publishing it at index 2 without carrying confirmation time leaks two future observations.

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 finalized, strictly ordered OHLC bars on one adjustment basis.
  2. At knowledge index t, evaluate only candidate i = t - right_span.
  3. Compare the candidate high and low with their complete left/right neighborhoods.
  4. Apply earliest-plateau equality: strict on the left, inclusive on the right.
  5. Emit event index and confirmation index together; never overwrite their distinction.

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

What must remain true

  • Every emitted confirmation index equals event index plus right span.
  • No event can be emitted before its complete right neighborhood exists.
  • The first left-span and final right-span bars cannot be confirmed candidates.
  • An outside bar may validly emit both a high and a low pivot.
  • Changing a bar requires replaying every candidate whose window touches it.

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 Causal Pivot Detection

Vary left span, right span, and separation independently. Observe that right span changes latency as well as geometry, while separation can reject an otherwise valid plateau pivot. 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.
Right-edge confirmation gapA late extreme remains unconfirmed because its required right-side bars do not exist yet.
Dual high-and-low outside barsA single wide bar can satisfy both pivot branches; downstream policy must preserve or resolve both explicitly.

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 Causal Pivot Detection 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 Causal Pivot Detection

  • The first left_span bars cannot be candidates.
  • The last right_span bars remain unconfirmed in a finite dataset.
  • An outside bar may satisfy both high- and low-pivot rules; preserve both events for downstream policy.
  • A later equal high cannot replace the earlier plateau member because it fails the strict-left rule.
  • Missing or revised bars require recomputation of every candidate whose neighborhood touches the revision.

The principal misuse is Back-plotting a confirmed marker on its event bar without showing the later confirmation time. 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 Causal Pivot Detection

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 ZigZag Segmentation.

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.

Causal Pivot Detection 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.

TV_TECHNIQUES — Pine Script Techniques: pivots

  • Organization or authors: TradingView
  • Source type: Maintained official platform documentation
  • Publication or effective date: Maintained documentation
  • Version: Pine Script v6 documentation current on 2026-07-27
  • URL: https://www.tradingview.com/pine-script-docs/faq/techniques/
  • Accessed: 2026-07-27
  • Jurisdiction: technical; not jurisdiction-specific
  • Supports: Symmetric left/right pivot legs and delayed discovery examples.
  • Limitations: The package freezes its own equality and separation rules instead of claiming a universal pivot definition.
  • 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.

SCIPY_PEAKS — scipy.signal.find_peaks

  • Organization or authors: SciPy project
  • Source type: Maintained official library documentation
  • Publication or effective date: Maintained documentation
  • Version: SciPy 1.17.0
  • URL: https://docs.scipy.org/doc/scipy/reference/generated/scipy.signal.find_peaks.html
  • Accessed: 2026-07-27
  • Jurisdiction: technical; not jurisdiction-specific
  • Supports: Local maxima by neighbor comparison, optional distance/prominence/width conditions, plateau handling, and NaN warnings.
  • Limitations: SciPy's retrospective signal function is not itself a causal financial detector.
  • 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.
causal_pivots.ts
export type Bar = { timestamp: string; high: number; low: number; close: number };
export type Pivot = { kind: "high" | "low"; event_index: number; confirmation_index: number; price: number; separation?: number; latency?: number };

function finite(value: number): boolean { return Number.isFinite(value); }
function validateBars(bars: Bar[]): void {
  if (!Array.isArray(bars) || bars.length === 0) throw new Error("bars must be a non-empty array");
  let previous = "";
  bars.forEach((bar, index) => {
    if (![bar.high, bar.low, bar.close].every(finite)) throw new Error(`bar ${index} contains a non-finite price`);
    if (bar.high < bar.low || bar.close < bar.low || bar.close > bar.high) throw new Error(`bar ${index} violates OHLC ordering`);
    if (!bar.timestamp || (previous && bar.timestamp <= previous)) throw new Error("timestamps must be strictly increasing");
    previous = bar.timestamp;
  });
}
function validatePivots(pivots: Pivot[]): void {
  if (!Array.isArray(pivots)) throw new Error("pivots must be an array");
  const identities = new Set<string>();
  pivots.forEach((pivot, index) => {
    if (pivot.kind !== "high" && pivot.kind !== "low") throw new Error(`pivot ${index} kind must be high or low`);
    if (!Number.isInteger(pivot.event_index) || pivot.event_index < 0) throw new Error(`pivot ${index} event_index must be an integer >= 0`);
    if (!Number.isInteger(pivot.confirmation_index) || pivot.confirmation_index < pivot.event_index) throw new Error(`pivot ${index} confirmation_index must be >= event_index`);
    if (!finite(pivot.price) || pivot.price <= 0) throw new Error(`pivot ${index} price must be finite and > 0`);
    const identity = `${pivot.kind}:${pivot.event_index}`;
    if (identities.has(identity)) throw new Error("same-kind pivot event indexes must be unique");
    identities.add(identity);
  });
}

export function detectCausalPivots(bars: Bar[], leftSpan = 3, rightSpan = 3, minSeparation = 0): Pivot[] {
  validateBars(bars);
  if (!Number.isInteger(leftSpan) || leftSpan < 1 || !Number.isInteger(rightSpan) || rightSpan < 1) throw new Error("spans must be integers >= 1");
  if (!finite(minSeparation) || minSeparation < 0) throw new Error("minSeparation must be finite and >= 0");
  const events: Pivot[] = [];
  for (let i = leftSpan; i < bars.length - rightSpan; i += 1) {
    const left = bars.slice(i - leftSpan, i), right = bars.slice(i + 1, i + rightSpan + 1);
    const leftHigh = Math.max(...left.map(b => b.high)), rightHigh = Math.max(...right.map(b => b.high));
    const leftLow = Math.min(...left.map(b => b.low)), rightLow = Math.min(...right.map(b => b.low));
    const highMargin = bars[i].high - Math.max(leftHigh, rightHigh);
    const lowMargin = Math.min(leftLow, rightLow) - bars[i].low;
    if (bars[i].high > leftHigh && bars[i].high >= rightHigh && highMargin + 1e-12 >= minSeparation)
      events.push({kind:"high", event_index:i, confirmation_index:i+rightSpan, price:bars[i].high, separation:highMargin, latency:rightSpan});
    if (bars[i].low < leftLow && bars[i].low <= rightLow && lowMargin + 1e-12 >= minSeparation)
      events.push({kind:"low", event_index:i, confirmation_index:i+rightSpan, price:bars[i].low, separation:lowMargin, latency:rightSpan});
  }
  return events.sort((a,b) => a.confirmation_index-b.confirmation_index || a.event_index-b.event_index || a.kind.localeCompare(b.kind));
}
Full-height labcausal pivot detection labOpen full screen