//@version=6 // Nearest Rejection Blocks — 15m / 30m / 1h // // Shows, per higher timeframe, ONLY the closest unmitigated rejection block above // live price and the closest one below. Nothing else is drawn, so a 1-minute chart // stays readable while you execute. // // RB definition — the block is the REVERSING CANDLE PLUS THE ONE BEFORE IT, and // each edge takes the extreme of the two, not of one candle: // Bearish RB (resistance, above price) // - a candle that closes DOWN, directly after a candle that did not close down // - top = the HIGHER of the two highs (whichever candle makes the higher wick) // - bottom = the LOWER of close[previous] and open[reversal] // Those two are usually the same price, but on an imbalance they differ by a // tick or two, and the block must swallow that gap. Taking the lower of the // pair is what does it. // - the pair must mark the highest point of the preceding K candles // - EQ is the midpoint // Bullish RB (support, below price) is the exact mirror: bottom = lower of the two // lows, top = the HIGHER of close[previous] and open[reversal]. // // NO REPAINTING, and no delay either: // 1. request.security uses lookahead_off and reads [1], so only CLOSED higher // timeframe candles are ever used. // 2. Every value the box needs — both highs, the previous close, the reversal open — // is known the instant the reversing candle closes. The block appears then and // never moves again. // 3. The box is drawn from the OPEN OF THE CANDLE AFTER CONFIRMATION, not back to // the wick candle. On a low timeframe that matters: a box painted over bars from // before the block existed looks mitigated when it is untouched. Picture and // logic now say the same thing. indicator("Nearest Rejection Blocks — 15m / 30m / 1h", "Nearest RB", overlay = true, max_boxes_count = 500, max_lines_count = 500, max_labels_count = 500) // ─────────────────────────────── inputs ─────────────────────────────── grpTF = "Timeframes" useTF1 = input.bool(true, "Timeframe 1", group = grpTF, inline = "t1") tf1 = input.timeframe("15", "", group = grpTF, inline = "t1") useTF2 = input.bool(true, "Timeframe 2", group = grpTF, inline = "t2") tf2 = input.timeframe("30", "", group = grpTF, inline = "t2") useTF3 = input.bool(true, "Timeframe 3", group = grpTF, inline = "t3") tf3 = input.timeframe("60", "", group = grpTF, inline = "t3") grpRB = "Rejection block" pivotLen = input.int(1, "Pivot lookback (K candles)", minval = 1, maxval = 50, group = grpRB, tooltip = "How many candles before the pair must be lower (bearish) or higher (bullish). 1 = minimal turn test, which is what lets a SECOND, nearer block form inside a bigger one. Raise it to only keep blocks at major swings.") invMode = input.string("Wick through", "Block is used up on", options = ["Wick through", "Close through"], group = grpRB, tooltip = "Wick through is the course rule: 'the rejection block invalidation is at the point where we literally take the swing high'. A wick beyond the outer edge kills it. Close through is the looser variant.") grpDraw = "Drawing" showEQ = input.bool(true, "Show EQ line", group = grpDraw) showLbl = input.bool(true, "Show labels", group = grpDraw) showOrg = input.bool(true, "Dotted line back to the wick candle", group = grpDraw, tooltip = "The box starts where the block became tradeable. This thin dotted line runs back along the wick level to the candle that made it, so you can still see where the block came from.") extendR = input.int(20, "Extend right (bars)", minval = 0, maxval = 500, group = grpDraw) bearCol = input.color(color.new(#c9453f, 82), "Bearish (above price)", group = grpDraw) bullCol = input.color(color.new(#0f8a53, 82), "Bullish (below price)", group = grpDraw) eqCol = input.color(color.new(#52514e, 25), "EQ line", group = grpDraw) MAXHIST = 300 MAXRB = 150 // ─────────────────────────────── helpers ─────────────────────────────── // value at an offset back from the newest element (0 = newest) at(array a, int off) => int n = array.size(a) n > off and off >= 0 ? array.get(a, n - 1 - off) : na atI(array a, int off) => int n = array.size(a) n > off and off >= 0 ? array.get(a, n - 1 - off) : na push(array a, float v, int cap) => array.push(a, v) if array.size(a) > cap array.shift(a) pushI(array a, int v, int cap) => array.push(a, v) if array.size(a) > cap array.shift(a) // Detect a rejection block on the two most recently stored candles. // offset 0 = the reversing candle, offset 1 = the candle before it. // Everything needed is already closed, so there is nothing to defer. // Returns [dir, outer, inner] — dir -1 bearish, +1 bullish, 0 none. detectRB(array aO, array aH, array aL, array aC, int K) => float outer = na float inner = na int dir = 0 if array.size(aO) >= K + 3 float o0 = at(aO, 0) float h0 = at(aH, 0) float l0 = at(aL, 0) float c0 = at(aC, 0) float o1 = at(aO, 1) float h1 = at(aH, 1) float l1 = at(aL, 1) float c1 = at(aC, 1) // ── bearish: a down close straight after a candle that did not close down if c0 < o0 and c1 >= o1 float top = math.max(h0, h1) float bot = math.min(c1, o0) // lower body edge of the pair = swallows the imbalance float prevMax = 0.0 bool ok = true for j = 2 to 1 + K float hv = at(aH, j) if na(hv) ok := false else if hv > prevMax prevMax := hv if ok and top >= prevMax and bot < top outer := top inner := bot dir := -1 // ── bullish: mirror if c0 > o0 and c1 <= o1 float bot = math.min(l0, l1) float top = math.max(c1, o0) // upper body edge of the pair float prevMin = 1e18 bool ok = true for j = 2 to 1 + K float lv = at(aL, j) if na(lv) ok := false else if lv < prevMin prevMin := lv if ok and bot <= prevMin and top > bot outer := bot inner := top dir := 1 [dir, outer, inner] // ─────────────────────── per-timeframe state ─────────────────────── type TFState array o array h array l array c array t // open time of each stored HTF candle array rbOuter array rbInner array rbDir array rbTime array rbWick // open time of the candle that made the wick box boxUp = na box boxDn = na line eqUp = na line eqDn = na line orgUp = na line orgDn = na label lblUp = na label lblDn = na newState() => TFState.new(array.new(), array.new(), array.new(), array.new(), array.new(), array.new(), array.new(), array.new(), array.new(), array.new()) var TFState s1 = newState() var TFState s2 = newState() var TFState s3 = newState() // request.security MUST run unconditionally on every bar, so it lives at global scope. // Reading [1] returns the last CLOSED higher timeframe candle. [o1, h1, l1, c1, t1x] = request.security(syminfo.tickerid, tf1, [open[1], high[1], low[1], close[1], time[1]], lookahead = barmerge.lookahead_off) [o2, h2, l2, c2, t2x] = request.security(syminfo.tickerid, tf2, [open[1], high[1], low[1], close[1], time[1]], lookahead = barmerge.lookahead_off) [o3, h3, l3, c3, t3x] = request.security(syminfo.tickerid, tf3, [open[1], high[1], low[1], close[1], time[1]], lookahead = barmerge.lookahead_off) // ── store a newly closed HTF candle, run detection, expire used-up blocks ── advance(TFState s, float o_, float h_, float l_, float c_, int t_, int durMs, bool fresh) => if fresh and not na(o_) push(s.o, o_, MAXHIST) push(s.h, h_, MAXHIST) push(s.l, l_, MAXHIST) push(s.c, c_, MAXHIST) pushI(s.t, t_, MAXHIST) [dir, ou, inr] = detectRB(s.o, s.h, s.l, s.c, pivotLen) if dir != 0 push(s.rbOuter, ou, MAXRB) push(s.rbInner, inr, MAXRB) push(s.rbDir, float(dir), MAXRB) // Anchor on the OPEN OF THE CANDLE AFTER CONFIRMATION — the first moment // the block could actually be traded. // Drawing back to the wick candle instead would paint the box over bars // where the block did not exist yet, which on a 1m chart reads as "already // mitigated" for a block that is untouched. Expiry is only evaluated from // confirmation onwards, so this keeps the picture and the logic identical: // anything you see happen inside the box really did happen after the block // became real. pushI(s.rbTime, t_ + durMs, MAXRB) // and remember where the wick actually is, for the origin line pushI(s.rbWick, atI(s.t, 1), MAXRB) // expire — walk backwards so removal does not disturb the index int idx = array.size(s.rbDir) - 1 while idx >= 0 float d = array.get(s.rbDir, idx) float ou = array.get(s.rbOuter, idx) bool dead = false if invMode == "Wick through" dead := d < 0 ? high > ou : low < ou else dead := d < 0 ? close > ou : close < ou if dead array.remove(s.rbDir, idx) array.remove(s.rbOuter, idx) array.remove(s.rbInner, idx) array.remove(s.rbTime, idx) array.remove(s.rbWick, idx) idx -= 1 // ── draw the nearest block above and below live price ── render(TFState s, string tfName, bool enabled) => box.delete(s.boxUp) box.delete(s.boxDn) line.delete(s.eqUp) line.delete(s.eqDn) line.delete(s.orgUp) line.delete(s.orgDn) label.delete(s.lblUp) label.delete(s.lblDn) if enabled float upO = na float upI = na int upT = 0 int upW = 0 float dnO = na float dnI = na int dnT = 0 int dnW = 0 float dUp = 1e18 float dDn = 1e18 for i = 0 to array.size(s.rbDir) - 1 float d = array.get(s.rbDir, i) float ou = array.get(s.rbOuter, i) float inr = array.get(s.rbInner, i) int bt = array.get(s.rbTime, i) int bw = array.get(s.rbWick, i) if d < 0 and inr > close if inr - close < dUp dUp := inr - close upO := ou upI := inr upT := bt upW := bw if d > 0 and inr < close if close - inr < dDn dDn := close - inr dnO := ou dnI := inr dnT := bt dnW := bw // draw on TIME, not bar index: bar-index coordinates are capped by Pine's // historical buffer (~300 bars) and an unmitigated 1h block outlives that. int rightEdge = time + extendR * timeframe.in_seconds() * 1000 if not na(upO) float eq = (upO + upI) / 2 s.boxUp := box.new(upT, upO, rightEdge, upI, xloc = xloc.bar_time, border_color = color.new(color.red, 45), bgcolor = bearCol) if showEQ s.eqUp := line.new(upT, eq, rightEdge, eq, xloc = xloc.bar_time, color = eqCol, style = line.style_dotted) if showOrg and upW < upT s.orgUp := line.new(upW, upO, upT, upO, xloc = xloc.bar_time, color = color.new(color.red, 55), style = line.style_dotted, width = 1) if showLbl s.lblUp := label.new(rightEdge, upI, tfName + " RB EQ " + str.tostring(eq, format.mintick), xloc = xloc.bar_time, style = label.style_label_left, textcolor = color.new(color.red, 0), color = color.new(color.white, 100), size = size.small) if not na(dnO) float eq = (dnO + dnI) / 2 s.boxDn := box.new(dnT, dnI, rightEdge, dnO, xloc = xloc.bar_time, border_color = color.new(color.green, 45), bgcolor = bullCol) if showEQ s.eqDn := line.new(dnT, eq, rightEdge, eq, xloc = xloc.bar_time, color = eqCol, style = line.style_dotted) if showOrg and dnW < dnT s.orgDn := line.new(dnW, dnO, dnT, dnO, xloc = xloc.bar_time, color = color.new(color.green, 55), style = line.style_dotted, width = 1) if showLbl s.lblDn := label.new(rightEdge, dnI, tfName + " RB EQ " + str.tostring(eq, format.mintick), xloc = xloc.bar_time, style = label.style_label_left, textcolor = color.new(color.green, 0), color = color.new(color.white, 100), size = size.small) advance(s1, o1, h1, l1, c1, t1x, timeframe.in_seconds(tf1) * 1000, ta.change(t1x) != 0) advance(s2, o2, h2, l2, c2, t2x, timeframe.in_seconds(tf2) * 1000, ta.change(t2x) != 0) advance(s3, o3, h3, l3, c3, t3x, timeframe.in_seconds(tf3) * 1000, ta.change(t3x) != 0) if barstate.islast render(s1, tf1, useTF1) render(s2, tf2, useTF2) render(s3, tf3, useTF3)