Order blocks are one of the most discussed concepts in smart money trading, and also one of the most loosely defined. Different communities use different rules for what qualifies, which is why the same chart looks like it has order blocks everywhere to one trader and almost nowhere to another. Before writing a line of Pine Script, the definition needs to be precise enough that a computer can apply it consistently.
This tutorial starts from a concrete structural definition, explains why each rule exists, then builds a complete working indicator in Pine Script v6 using pivot detection, ATR-qualified impulse moves, and box.new() for the visual zones. Every design decision is explained so you can adjust the rules to match your own interpretation.
A precise definition of an order block
For this indicator, a bearish order block is the pivot high bar — the bar with the highest high relative to a configurable number of bars on each side — that is immediately followed by a strong bearish impulse move. The logic is that institutions placed significant sell orders at that level, which caused price to move sharply away from it. Price returning to that zone may encounter the unfilled portion of those orders.
A bullish order block is the mirror: the pivot low bar immediately followed by a strong bullish impulse. The zone is drawn from the pivot bar's high to its low as a box that extends to the right until you decide to remove it or it gets tested and invalidated.
Two criteria qualify the move as "strong enough" to confirm the block: the candle immediately after the pivot must be directional (close below open for bearish, above for bullish) and its range must exceed a minimum ATR multiple. This filters out pivots that were followed by weak consolidation rather than a genuine institutional impulse.
Why confirmed pivots are essential here
The indicator uses ta.pivothigh() and ta.pivotlow() with an equal lookback on each side. As explained in the divergence detector guide, a pivot is only confirmed once enough subsequent bars have formed to validate it. With a lookback of 3 on each side, the confirmation arrives 3 bars after the actual pivot bar. This means every order block box is drawn on a pivot that has genuinely formed, not one that might still be invalidated by the next bar. The 3-bar lag is the price of a non-repainting indicator.
The most important indexing detail: when ta.pivothigh() returns a non-na value on the current bar, the pivot bar itself is lookback bars back in history. So bar_index - lookback is the pivot bar, and high[lookback] is the high of the pivot bar, and close[lookback - 1] is the close of the impulse bar immediately after the pivot. Getting these offsets wrong is the most common bug in pivot-based indicators.
The full indicator
//@version=6
indicator("Order Block Detector", overlay=true, max_boxes_count=100)
// ── Inputs ──────────────────────────────────────────────────────────────
lookback = input.int(3, "Pivot lookback bars (each side)", minval=1)
minMoveAtr = input.float(1.5, "Min move ATR multiplier", minval=0.1, step=0.1)
boxTransp = input.int(85, "Box transparency", minval=0, maxval=95)
// ── ATR for move qualification ───────────────────────────────────────────
atrValue = ta.atr(14)
// ── Confirmed pivot detection ────────────────────────────────────────────
// Pivots are confirmed 'lookback' bars after the actual pivot bar.
// We check conditions at bar_index - lookback (the pivot bar itself).
ph = ta.pivothigh(high, lookback, lookback)
pl = ta.pivotlow(low, lookback, lookback)
// ── Bearish order block: pivot high followed by a strong down move ───────
// The pivot high bar is the potential order block. We confirm it is an OB
// only if the candle immediately after the pivot moved down by at least
// minMoveAtr * ATR — indicating an institutional sell order was present.
if not na(ph)
// bar_index - lookback is the pivot bar; bar_index - lookback + 1 is the
// bar immediately after (the "impulse" bar that confirms the order block).
pivotBarIdx = bar_index - lookback
impulseBarIdx = pivotBarIdx + 1
// Impulse bar must be a strong bearish candle.
impulseMove = high[lookback - 1] - low[lookback - 1]
strongMove = impulseMove >= atrValue[lookback - 1] * minMoveAtr
bearishBar = close[lookback - 1] < open[lookback - 1]
if strongMove and bearishBar
// The order block box spans the pivot bar's high to low.
boxLeft = pivotBarIdx
boxRight = bar_index + 20 // extend to the right for visibility
box.new(
left = boxLeft,
top = ph,
right = boxRight,
bottom = high[lookback], // open of the pivot bar as alternative bottom
border_color = color.new(color.red, 30),
bgcolor = color.new(color.red, boxTransp),
extend = extend.right
)
// ── Bullish order block: pivot low followed by a strong up move ──────────
if not na(pl)
impulseMove = high[lookback - 1] - low[lookback - 1]
strongMove = impulseMove >= atrValue[lookback - 1] * minMoveAtr
bullishBar = close[lookback - 1] > open[lookback - 1]
if strongMove and bullishBar
box.new(
left = bar_index - lookback,
top = low[lookback], // close of the pivot bar as alternative top
right = bar_index + 20,
bottom = pl,
border_color = color.new(color.green, 30),
bgcolor = color.new(color.green, boxTransp),
extend = extend.right
)Walking through the key decisions
The max_boxes_count=100 parameter in the indicator() declaration is required. Pine Script limits drawing objects per script type, and without setting this parameter explicitly the default is 50. For an indicator that draws a box at every qualifying pivot, the default fills up quickly on longer timeframes. The maximum allowed is 500. Setting it to 100 is a balance between coverage and memory. The full limits are explained in the Pine Script drawing limits guide.
The extend.right argument on each box makes the zone extend rightward on the chart indefinitely until a new bar pushes the box's right edge further right via the boxRight = bar_index + 20 offset. This is a common pattern for support and resistance zones: the level remains marked until you actively invalidate it. A production-quality version would detect when price closes inside the box and either delete it or change its color to indicate the zone has been tested.
The ATR multiplier threshold gives the indicator adaptability across different instruments and timeframes. A 1.5× ATR move is significant on most charts but can be tuned lower for volatile instruments or higher for smooth ones. Using ATR rather than a fixed pip or point value means the threshold scales with the instrument's typical volatility automatically.
What to add next
The version above draws boxes and leaves them permanently. A more complete indicator would invalidate a bullish order block when price closes below its bottom, and invalidate a bearish one when price closes above its top. This requires storing each box in an array and checking on every bar whether any active boxes have been violated. That pattern uses array.push(), array.get(), and box.delete(), which are covered in the arrays, matrices, and maps guide.
The order block concept itself — what a level means structurally — is covered in the companion post order blocks in Pine Script if you want a deeper treatment of the theory before adapting the code.
Building complex drawing indicators faster
The indicator above is roughly 50 lines, but it requires precise knowledge of pivot indexing, box drawing syntax, ATR scaling, and the max_boxes_count parameter. Getting any one of these wrong produces a script that either crashes, draws boxes at the wrong bars, or silently loses older boxes without warning. This is exactly the class of indicator where PineScripter earns its keep: describe the structural definition in plain English and the generated code handles the indexing arithmetic and drawing boilerplate correctly, with the error loop catching any compile issues before you paste.
Disclaimer: PineScripter is a coding tool for Pine Script development. It does not provide financial advice and does not guarantee trading profits. Always backtest strategies thoroughly and understand the risks before live trading.