Swing-pivot market structure from a price series: the HH / HL / LH / LL
read, in dependency-free Python.
Feed it highs and lows from any source and ask the question traders actually ask about trend: is this market making higher highs and higher lows?
from pivotstructure import detect_pivots, structure_state
pivots = detect_pivots(highs, lows, lookback=10)
pivots[-1].type # 'HL'
pivots[-1].confirmed # False, the newest pivot can still move
state = structure_state(pivots)
state.sequence # 'LH-HL-HH-HL'
state.family # 'momentum_long'
state.reading # 'a higher low after a higher high: the advance is extending, …'Stdlib only. No numpy, no pandas, no I/O, no configuration. Python 3.12+.
A moving average is a lagging function of price: it whipsaws in a directionless market and confirms turns late. A pivot read degrades more honestly, since chop produces mixed labels, which is information rather than a false signal.
The two are complementary rather than competing. A market trading above its 200-day average while printing lower highs is a specific, nameable state that neither read expresses alone.
Three stages:
- Mark. Bar i is a pivot high when its high is the maximum of the trailing
lookbackbars including itself. The window looks only backwards, so a mark is knowable the moment the bar closes. - Zigzag. Consecutive marks of the same kind collapse to the most extreme one, leaving a strictly alternating high/low sequence.
- Classify. Each pivot is labelled against the previous pivot of its own kind:
higher high
HH, lower highLH, higher lowHL, lower lowLL.
lookback sets how significant a swing has to be: larger windows give fewer,
bigger pivots. Ten daily bars is a reasonable starting point, not a calibrated
number.
The zigzag keeps replacing the newest pivot while price extends, so its index, price and label are all provisional until a pivot of the opposite kind forms. Because the sequence strictly alternates, that means:
every pivot except the last is final; the last one can still move.
Scanning a whole series and treating every pivot as though it had been knowable when it printed is look-ahead bias. It discards the false starts a live reader would have acted on, each having been overwritten by the better pivot that followed. Results built that way are optimistic in a way no amount of careful cross-validation downstream can repair.
from pivotstructure import confirmed_pivots
safe = confirmed_pivots(pivots) # everything that can no longer changeStructureState.confirmed_through gives the bar index the state was actually
settled through, which is the bar a forward-return study should date it from.
detect_pivots(highs, lows, lookback=10) -> list[Pivot] |
Detect and classify. |
confirmed_pivots(pivots) -> list[Pivot] |
Drop the provisional one. |
structure_state(pivots, length=5) -> StructureState | None |
Name the current shape. None when there are fewer than two pivots. |
classify_pattern_family(pattern) -> str |
'HL-HH-HL' → 'reversal_long'. Coarse: each family spans two opposite shapes. |
reading_for(pattern) -> str |
What price actually did, from the last two pivots. Use this for one sentence. |
sequence_string(pivots, length=5) -> str |
The last n labels, hyphenated. |
Pivot(index, price, is_high, type, confirmed). index is a position in the
input series, not a date; this package never sees a calendar.
StructureState(sequence, family, last_pivot, confirmed_through, reading).
Decided by two things: the kind of pivot the sequence ends on, and whether the last two legs travelled the same way.
| ends on a low (turning up) | ends on a high (turning down) | |
|---|---|---|
| legs agree | momentum_long |
momentum_short |
| legs disagree | reversal_long |
reversal_short |
A two-pivot sequence is always momentum: one leg has no prior direction to have reversed from.
long and short name the side of the terminal pivot, not the trend. LH-LL is
a long family and a market making lower lows: price is at a low and the next move
off it is up. Each family therefore spans a rising and a falling shape, which is
why reading_for keys on the last two pivots instead.
The family names a shape, not a recommendation. momentum_long says the last
two legs both went up, nothing about what happens next. Whether any of these
states carries a tradeable edge is a question about forward returns that this
package does not answer, and the honest way to ask it is with confirmed pivots
only.
There are no entries, exits, targets, stops, or models here, and there never will be.
pip install pivotstructureoptionstruct, a position: legs, payoff, exact risk,Decimalthroughout.strikemetrics, a chain's positioning: gamma exposure, max pain, walls, put/call ratios.
MIT