//@version=6 // Macro Rejection Blocks — 1m / 2m / 3m / 5m, only inside the xx:42 → xx+1:15 windows // // The low-timeframe companion to "Nearest Rejection Blocks". That one gives you the // POI on 15m/30m/1h; this one gives you the trigger, and only builds blocks whose // reversing candle closed inside a macro window. // // RB definition — identical to the higher timeframe tool: the block is the REVERSING // CANDLE PLUS THE ONE BEFORE IT, each edge taking the extreme of the two. // Bearish: top = higher of the two highs · bottom = LOWER of close[prev] and open[rev] // Bullish: bottom = lower of the two lows · top = HIGHER of close[prev] and open[rev] // That lower/higher body edge is what swallows the volume imbalance. // // Invalidation defaults to a WICK through the outer edge, per the course: // "the rejection block invalidation is at the point where we literally take the swing high". // // ⚠ RUN THIS ON THE 1-MINUTE CHART. Pine cannot rebuild a series from a timeframe // LOWER than the chart's, so any requested timeframe below the chart timeframe is // switched off automatically and flagged in a warning label. indicator("Macro Rejection Blocks — 1/2/3/5m", "Macro RB", overlay = true, max_boxes_count = 500, max_lines_count = 500, max_labels_count = 500) // ─────────────────────────────── inputs ─────────────────────────────── gTF = "Timeframes" use1 = input.bool(true, "TF 1", group = gTF, inline = "a") tfA = input.timeframe("1", "", group = gTF, inline = "a") use2 = input.bool(true, "TF 2", group = gTF, inline = "b") tfB = input.timeframe("2", "", group = gTF, inline = "b") use3 = input.bool(true, "TF 3", group = gTF, inline = "c") tfC = input.timeframe("3", "", group = gTF, inline = "c") use4 = input.bool(true, "TF 4", group = gTF, inline = "d") tfD = input.timeframe("5", "", group = gTF, inline = "d") gRB = "Rejection block" pivotLen = input.int(1, "Pivot lookback (K candles)", minval = 1, maxval = 50, group = gRB, tooltip = "How many candles before the pair must be lower (bearish) or higher (bullish). 1 = minimal turn test, which lets a second, nearer block form inside a bigger one.") invMode = input.string("Wick through", "Block is used up on", options = ["Wick through", "Close through"], group = gRB) holdAfter = input.bool(true, "Keep blocks after their window closes", group = gRB, tooltip = "On: a block built inside a window stays visible until price uses it up. Off: it disappears the moment its window ends.") gW = "Macro windows (xx:42 → xx+1:15)" tzM = input.string("Etc/GMT+4", "Timezone", group = gW) w02 = input.bool(true, "02:42 – 03:15", group = gW) w03 = input.bool(true, "03:42 – 04:15", group = gW) w04 = input.bool(true, "04:42 – 05:15", group = gW) w05 = input.bool(true, "05:42 – 06:15", group = gW) w06 = input.bool(true, "06:42 – 07:15", group = gW) w07 = input.bool(true, "07:42 – 08:15", group = gW) w08 = input.bool(true, "08:42 – 09:15", group = gW) w09 = input.bool(true, "09:42 – 10:15", group = gW) w10 = input.bool(true, "10:42 – 11:15", group = gW) w11 = input.bool(true, "11:42 – 12:15", group = gW) w12 = input.bool(true, "12:42 – 13:15", group = gW) w13 = input.bool(true, "13:42 – 14:15", group = gW) w14 = input.bool(true, "14:42 – 15:15", group = gW) w15 = input.bool(true, "15:42 – 16:15", group = gW) gD = "Drawing" showEQ = input.bool(true, "Show EQ line", group = gD) showLbl = input.bool(true, "Show labels", group = gD) showOrg = input.bool(true, "Dotted line back to the wick candle", group = gD) extendR = input.int(20, "Extend right (bars)", minval = 0, maxval = 500, group = gD) bearCol = input.color(color.new(#c9453f, 82), "Bearish (above price)", group = gD) bullCol = input.color(color.new(#0f8a53, 82), "Bullish (below price)", group = gD) eqCol = input.color(color.new(#52514e, 25), "EQ line", group = gD) MAXHIST = 300 MAXRB = 150 winOn = array.from(w02, w03, w04, w05, w06, w07, w08, w09, w10, w11, w12, w13, w14, w15) // ─────────────────────────────── helpers ─────────────────────────────── // is this hour the START hour of an enabled window? (index 0 = hour 2) hourOn(int h) => int idx = h - 2 idx >= 0 and idx < array.size(winOn) ? array.get(winOn, idx) : false // does this timestamp fall inside an enabled xx:42 → xx+1:15 window? inMacro(int ts) => int h = hour(ts, tzM) int m = minute(ts, tzM) bool res = false if m >= 42 res := hourOn(h) // opening half of a window if m <= 15 res := res or hourOn(h - 1) // tail of the previous hour's window res 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) // offset 0 = the reversing candle, offset 1 = the one before it 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) if c0 < o0 and c1 >= o1 float top = math.max(h0, h1) float bot = math.min(c1, o0) 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 if c0 > o0 and c1 <= o1 float bot = math.min(l0, l1) float top = math.max(c1, o0) 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 array rbOuter array rbInner array rbDir array rbTime array rbWick array rbEnd // when this block's macro window closes 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(), array.new()) var TFState sA = newState() var TFState sB = newState() var TFState sC = newState() var TFState sD = newState() // a requested timeframe below the chart's cannot be rebuilt — switch it off chartSec = timeframe.in_seconds() okA = use1 and timeframe.in_seconds(tfA) >= chartSec okB = use2 and timeframe.in_seconds(tfB) >= chartSec okC = use3 and timeframe.in_seconds(tfC) >= chartSec okD = use4 and timeframe.in_seconds(tfD) >= chartSec // request.security must run unconditionally, so it lives at global scope [oA, hA, lA, cA, tA] = request.security(syminfo.tickerid, tfA, [open[1], high[1], low[1], close[1], time[1]], lookahead = barmerge.lookahead_off) [oB, hB, lB, cB, tB] = request.security(syminfo.tickerid, tfB, [open[1], high[1], low[1], close[1], time[1]], lookahead = barmerge.lookahead_off) [oC, hC, lC, cC, tC] = request.security(syminfo.tickerid, tfC, [open[1], high[1], low[1], close[1], time[1]], lookahead = barmerge.lookahead_off) [oD, hD, lD, cD, tD] = request.security(syminfo.tickerid, tfD, [open[1], high[1], low[1], close[1], time[1]], lookahead = barmerge.lookahead_off) // ── store a newly closed candle, detect, expire ── 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) // ONLY build a block if the reversing candle closed inside a macro window if inMacro(t_) [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) pushI(s.rbTime, t_ + durMs, MAXRB) // first tradeable candle pushI(s.rbWick, atI(s.t, 1), MAXRB) // where the wick is // end of this block's window = the next xx:15 after it int hh = hour(t_, tzM) int mm = minute(t_, tzM) int endH = mm >= 42 ? hh + 1 : hh pushI(s.rbEnd, timestamp(tzM, year(t_, tzM), month(t_, tzM), dayofmonth(t_, tzM), endH, 15, 0), MAXRB) 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 = invMode == "Wick through" ? (d < 0 ? high > ou : low < ou) : (d < 0 ? close > ou : close < ou) if not holdAfter and time > array.get(s.rbEnd, idx) dead := true 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) array.remove(s.rbEnd, idx) idx -= 1 // ── draw the nearest block above and below 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 float dnO = na float dnI = na int upT = 0 int upW = 0 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 and inr - close < dUp dUp := inr - close upO := ou upI := inr upT := bt upW := bw if d > 0 and inr < close and close - inr < dDn dDn := close - inr dnO := ou dnI := inr dnT := bt dnW := bw int rightEdge = time + extendR * chartSec * 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 + "m 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 + "m 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(sA, oA, hA, lA, cA, tA, timeframe.in_seconds(tfA) * 1000, ta.change(tA) != 0) advance(sB, oB, hB, lB, cB, tB, timeframe.in_seconds(tfB) * 1000, ta.change(tB) != 0) advance(sC, oC, hC, lC, cC, tC, timeframe.in_seconds(tfC) * 1000, ta.change(tC) != 0) advance(sD, oD, hD, lD, cD, tD, timeframe.in_seconds(tfD) * 1000, ta.change(tD) != 0) var label warn = na if barstate.islast render(sA, tfA, okA) render(sB, tfB, okB) render(sC, tfC, okC) render(sD, tfD, okD) label.delete(warn) bool anyOff = (use1 and not okA) or (use2 and not okB) or (use3 and not okC) or (use4 and not okD) if anyOff warn := label.new(bar_index, high, "Switched off: timeframe below the chart's.\nRun this on the 1-minute chart.", style = label.style_label_down, color = color.new(color.orange, 15), textcolor = color.white, size = size.small)