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.
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 |
|---|---|---|---|
| timestamp | ISO-8601 string | bar close and availability time | strictly increasing; finalized bar only |
| high | finite number | bar high in price units | same instrument, session, and adjustment basis |
| low | finite number | bar low in price units | must not exceed high |
| close | finite number | finalized close in price units | must lie inside high-low |
| open | optional finite number | fixture and display field | not 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.
| Variant | Definition | Best use | Limitation |
|---|---|---|---|
| This package | OHLC high/low; asymmetric legs; earliest plateau; causal | live-safe extrema handoff | requires R future bars before publication |
| Symmetric platform pivot | same left/right legs; platform equality rules | chart scripting | plateau ownership may differ |
| SciPy local peak | retrospective sample maxima with optional prominence, distance, width | offline signal analysis | not a causal OHLC pivot by itself |
| Close-only pivot | uses closes instead of intrabar extremes | close-based systems | can miss wick extrema |
The complete mathematical contract
| Symbol | Meaning | Unit |
|---|---|---|
| i | candidate event index | bar index |
| L, R | left and right spans | bars |
| H_i, L_i | candidate high and low | price units |
| s_min | minimum separation | price units |
| k_i | confirmation or knowledge index | 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 | 10 | left warm-up | not eligible |
| 1 | 12 | left warm-up | not eligible |
| 2 | 15 | candidate; right bars missing | not publishable |
| 3 | 13 | one right bar observed | not publishable |
| 4 | 11 | 15 beats left 10,12 and right 13,11 | publish 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
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 finalized, strictly ordered OHLC bars on one adjustment basis.
- At knowledge index t, evaluate only candidate i = t - right_span.
- Compare the candidate high and low with their complete left/right neighborhoods.
- Apply earliest-plateau equality: strict on the left, inclusive on the right.
- 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
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
| 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. |
| Right-edge confirmation gap | A late extreme remains unconfirmed because its required right-side bars do not exist yet. |
| Dual high-and-low outside bars | A 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:
- 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 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
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
- TradingView pivot visuals — delayed confirmation and plotting offset.
- TradingView repainting — knowledge-time and back-plotting risk.
- SciPy find_peaks 1.17.0 — nearby retrospective peak conventions.
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.
Causal Pivot Detection 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.
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.
Full dependency-light reference implementations in both supported languages.
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));
}
The embedded lab now expands to its full document height, keeping the article as the only scroll surface.