ACE444

BB - MTF

Mar 7th, 2026
825
0
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
text 24.33 KB | None | 0 0
  1. //@version=6
  2. indicator("BB MTF", overlay=true, max_boxes_count=500, max_lines_count=500)
  3.  
  4. // ─────────────────────────────────────────────────────────
  5. // INPUTS
  6. // ─────────────────────────────────────────────────────────
  7. lookback = input.int(100, "Lookback (bars)", minval=10, maxval=500, group="General")
  8. ob_min_overlap = input.float(50, "Min FVG overlap % of OB", minval=1, maxval=100, group="General")
  9. fvg_strict_pct = input.float(25, "x2+ block mitigation threshold %", minval=1, maxval=49, step=1, group="General")
  10. bb_col = input.color(color.new(#0097a7, 75), "BB colour (x1)", group="Visual")
  11. bb_col_x2 = input.color(color.new(#0097a7, 75), "BB colour (x2)", group="Visual")
  12. bb_col_x3 = input.color(color.new(#0097a7, 75), "BB colour (x3)", group="Visual")
  13. bb_col_x4 = input.color(color.new(#0097a7, 75), "BB colour (x4+)",group="Visual")
  14. mid_col = input.color(color.new(#7a808d, 20), "Midline colour", group="Visual")
  15. show_mid = input.bool(false, "Show midline", group="Visual")
  16. show_labels = input.bool(true, "Show labels", group="Visual")
  17. border_col = input.color(color.new(#000000, 100), "Border colour", group="Visual")
  18. border_width = input.int(1, "Border width", minval=1, maxval=5, group="Visual")
  19. show_current_only = input.bool(false, "Show current TF only", group="Timeframes")
  20. show_2h = input.bool(false, "2H", group="Timeframes", inline="tfs1")
  21. show_4h = input.bool(true, "4H", group="Timeframes", inline="tfs1")
  22. show_6h = input.bool(false, "6H", group="Timeframes", inline="tfs1")
  23. show_8h = input.bool(false, "8H", group="Timeframes", inline="tfs1")
  24. show_12h = input.bool(false, "12H", group="Timeframes", inline="tfs2")
  25.  
  26. // ─────────────────────────────────────────────────────────
  27. // HELPERS
  28. // ─────────────────────────────────────────────────────────
  29. body_high(i) => math.max(open[i], close[i])
  30. body_low(i) => math.min(open[i], close[i])
  31. is_bull(i) => close[i] > open[i]
  32. is_bear(i) => close[i] < open[i]
  33.  
  34. // ─────────────────────────────────────────────────────────
  35. // CORE LOGIC
  36. // Returns event string: "B|top|bot|mid|left_t|is_bear|is_partial|fvg_cnt"
  37. // or "D|top|bot|mid|left_t|right_t|is_bear|is_partial|fvg_cnt"
  38. // Multiple events separated by ";"
  39. // ─────────────────────────────────────────────────────────
  40. bb_logic(simple int lb, simple float min_overlap, simple float strict_pct) =>
  41. var float[] a_top = array.new<float>()
  42. var float[] a_bot = array.new<float>()
  43. var float[] a_mid = array.new<float>()
  44. var int[] a_left = array.new<int>()
  45. var bool[] a_is_bear = array.new<bool>()
  46. var bool[] a_is_partial = array.new<bool>()
  47. var int[] a_fvg_cnt = array.new<int>()
  48.  
  49. var float[] p_top = array.new<float>()
  50. var float[] p_bot = array.new<float>()
  51. var float[] p_mid = array.new<float>()
  52. var int[] p_left = array.new<int>()
  53. var bool[] p_is_bear = array.new<bool>()
  54. var bool[] p_is_partial = array.new<bool>()
  55. var int[] p_fvg_cnt = array.new<int>()
  56.  
  57. events = ""
  58.  
  59. // ── Resolve pending
  60. if array.size(p_top) > 0
  61. j = 0
  62. while j < array.size(p_top)
  63. mid_level = array.get(p_mid, j)
  64. fvg_cnt_p = array.get(p_fvg_cnt, j)
  65. p_top_v = array.get(p_top, j)
  66. p_bot_v = array.get(p_bot, j)
  67. p_bear_v = array.get(p_is_bear, j)
  68. eff_p_lvl = fvg_cnt_p >= 2 ? (p_bear_v ? p_top_v - (p_top_v - p_bot_v) * (strict_pct / 100) : p_bot_v + (p_top_v - p_bot_v) * (strict_pct / 100)) : mid_level
  69. bear_fvg = low[2] > high[0]
  70. bull_fvg = high[2] < low[0]
  71. c2_top = math.max(open[1], close[1])
  72. c2_bot = math.min(open[1], close[1])
  73. c2_crosses = c2_top >= eff_p_lvl and c2_bot <= eff_p_lvl
  74. fvg_exempt = (bear_fvg or bull_fvg) and c2_crosses
  75. if fvg_exempt
  76. new_cnt = array.get(p_fvg_cnt, j) + 1
  77. array.push(a_top, array.get(p_top, j))
  78. array.push(a_bot, array.get(p_bot, j))
  79. array.push(a_mid, array.get(p_mid, j))
  80. array.push(a_left, array.get(p_left, j))
  81. array.push(a_is_bear, array.get(p_is_bear, j))
  82. array.push(a_is_partial, array.get(p_is_partial, j))
  83. array.push(a_fvg_cnt, new_cnt)
  84. // Fire update event so chart side can refresh the label
  85. u = "U|" + str.tostring(array.get(p_left, j)) + "|" + str.tostring(new_cnt) + "|" + (array.get(p_is_partial, j) ? "1" : "0")
  86. events := events == "" ? u : events + ";" + u
  87. else
  88. e = "D|" + str.tostring(array.get(p_top, j)) + "|" + str.tostring(array.get(p_bot, j)) + "|" + str.tostring(array.get(p_mid, j)) + "|" + str.tostring(array.get(p_left, j)) + "|" + str.tostring(time[1]) + "|" + (array.get(p_is_bear, j) ? "1" : "0") + "|" + (array.get(p_is_partial, j) ? "1" : "0") + "|" + str.tostring(array.get(p_fvg_cnt, j))
  89. events := events == "" ? e : events + ";" + e
  90. array.remove(p_top, j)
  91. array.remove(p_bot, j)
  92. array.remove(p_mid, j)
  93. array.remove(p_left, j)
  94. array.remove(p_is_bear, j)
  95. array.remove(p_is_partial, j)
  96. array.remove(p_fvg_cnt, j)
  97.  
  98. // ── Check active
  99. if array.size(a_top) > 0
  100. i = 0
  101. while i < array.size(a_top)
  102. mid_level = array.get(a_mid, i)
  103. fvg_cnt_a = array.get(a_fvg_cnt, i)
  104. a_top_v = array.get(a_top, i)
  105. a_bot_v = array.get(a_bot, i)
  106. a_bear_v = array.get(a_is_bear, i)
  107. // For x2+ blocks use stricter threshold — % from near edge instead of midpoint
  108. // Bearish BB: price approaches from above, strict level is just below top
  109. // Bullish BB: price approaches from below, strict level is just above bottom
  110. eff_level = fvg_cnt_a >= 2 ? (a_bear_v ? a_top_v - (a_top_v - a_bot_v) * (strict_pct / 100) : a_bot_v + (a_top_v - a_bot_v) * (strict_pct / 100)) : mid_level
  111. body_top_now = math.max(open, close)
  112. body_bot_now = math.min(open, close)
  113. body_crosses = body_top_now >= eff_level and body_bot_now <= eff_level
  114. wick_crosses = (high >= eff_level and low <= eff_level) and not body_crosses
  115. if wick_crosses
  116. e = "D|" + str.tostring(array.get(a_top, i)) + "|" + str.tostring(array.get(a_bot, i)) + "|" + str.tostring(array.get(a_mid, i)) + "|" + str.tostring(array.get(a_left, i)) + "|" + str.tostring(time) + "|" + (array.get(a_is_bear, i) ? "1" : "0") + "|" + (array.get(a_is_partial, i) ? "1" : "0") + "|" + str.tostring(array.get(a_fvg_cnt, i))
  117. events := events == "" ? e : events + ";" + e
  118. array.remove(a_top, i)
  119. array.remove(a_bot, i)
  120. array.remove(a_mid, i)
  121. array.remove(a_left, i)
  122. array.remove(a_is_bear, i)
  123. array.remove(a_is_partial, i)
  124. array.remove(a_fvg_cnt, i)
  125. else if body_crosses
  126. array.push(p_top, array.get(a_top, i))
  127. array.push(p_bot, array.get(a_bot, i))
  128. array.push(p_mid, array.get(a_mid, i))
  129. array.push(p_left, array.get(a_left, i))
  130. array.push(p_is_bear, array.get(a_is_bear, i))
  131. array.push(p_is_partial, array.get(a_is_partial, i))
  132. array.push(p_fvg_cnt, array.get(a_fvg_cnt, i))
  133. array.remove(a_top, i)
  134. array.remove(a_bot, i)
  135. array.remove(a_mid, i)
  136. array.remove(a_left, i)
  137. array.remove(a_is_bear, i)
  138. array.remove(a_is_partial, i)
  139. array.remove(a_fvg_cnt, i)
  140. else
  141. i += 1
  142.  
  143. // ── Detect new BBs
  144. if bar_index >= lb + 3
  145. fvg_c1 = 2
  146. fvg_c2 = 1
  147. fvg_c3 = 0
  148. fvg_bear_top = low[fvg_c1]
  149. fvg_bear_bot = high[fvg_c3]
  150. valid_bear_fvg = is_bear(fvg_c2) and fvg_bear_top > fvg_bear_bot
  151. fvg_bull_bot = high[fvg_c1]
  152. fvg_bull_top = low[fvg_c3]
  153. valid_bull_fvg = is_bull(fvg_c2) and fvg_bull_top > fvg_bull_bot
  154.  
  155. if valid_bear_fvg or valid_bull_fvg
  156. fvg_top = valid_bear_fvg ? fvg_bear_top : fvg_bull_top
  157. fvg_bot = valid_bear_fvg ? fvg_bear_bot : fvg_bull_bot
  158. looking_for_bear_ob = valid_bear_fvg
  159.  
  160. // Precompute prefix min/max arrays for the lookback window once,
  161. // so the inner mitigation check (was O(n)) becomes O(1) per ob_i.
  162. // prefix_low[k] = lowest low from bar fvg_c1+1 to bar fvg_c1+1+k
  163. // prefix_high[k] = highest high from bar fvg_c1+1 to bar fvg_c1+1+k
  164. prefix_low = array.new<float>()
  165. prefix_high = array.new<float>()
  166. running_low = high[fvg_c1 + 1] // initialise to opposite extreme
  167. running_high = low[fvg_c1 + 1]
  168. for k = fvg_c1 + 1 to lb - 1
  169. running_low := math.min(running_low, low[k])
  170. running_high := math.max(running_high, high[k])
  171. array.push(prefix_low, running_low)
  172. array.push(prefix_high, running_high)
  173.  
  174. for ob_i = fvg_c1 + 1 to lb - 1
  175. ob_engulf = ob_i - 1
  176. ob_colour_match = (looking_for_bear_ob and is_bear(ob_i)) or (not looking_for_bear_ob and is_bull(ob_i))
  177. if ob_colour_match
  178. ob_bh = body_high(ob_i)
  179. ob_bl = body_low(ob_i)
  180. ob_mid = (ob_bh + ob_bl) / 2
  181. b_is_opposite = looking_for_bear_ob ? is_bull(ob_engulf) : is_bear(ob_engulf)
  182. b_closes_beyond = looking_for_bear_ob ? close[ob_engulf] > ob_bh : close[ob_engulf] < ob_bl
  183. if b_is_opposite and b_closes_beyond
  184. mitigated = false
  185. deepest_high = ob_bl
  186. deepest_low = ob_bh
  187. // Use prefix arrays for O(1) range min/max instead of inner loop
  188. // Range to check: bars fvg_c1+1 to ob_engulf-1
  189. // prefix arrays are 0-indexed from fvg_c1+1, so:
  190. // index of ob_engulf-1 in prefix = (ob_engulf-1) - (fvg_c1+1) = ob_engulf - fvg_c1 - 2
  191. range_end_idx = ob_engulf - fvg_c1 - 2
  192. if ob_engulf >= fvg_c1 + 2 and range_end_idx >= 0
  193. range_low = array.get(prefix_low, range_end_idx)
  194. range_high = array.get(prefix_high, range_end_idx)
  195. // Check if any bar in range entered the OB body
  196. if range_high > ob_bl and range_low < ob_bh
  197. if looking_for_bear_ob
  198. if range_low <= ob_mid
  199. mitigated := true
  200. else
  201. deepest_low := range_low
  202. else
  203. if range_high >= ob_mid
  204. mitigated := true
  205. else
  206. deepest_high := range_high
  207. if not mitigated
  208. overlap_top = math.min(ob_bh, fvg_top)
  209. overlap_bot = math.max(ob_bl, fvg_bot)
  210. overlap_range = overlap_top - overlap_bot
  211. ob_range = ob_bh - ob_bl
  212. overlap_pct = ob_range > 0 ? (overlap_range / ob_range) * 100 : 0.0
  213. if overlap_pct >= min_overlap
  214. draw_top = overlap_top
  215. draw_bot = overlap_bot
  216. if looking_for_bear_ob
  217. draw_top := math.min(draw_top, deepest_low)
  218. else
  219. draw_bot := math.max(draw_bot, deepest_high)
  220. draw_mid = (draw_top + draw_bot) / 2
  221. ob_touched = looking_for_bear_ob ? deepest_low < ob_bh : deepest_high > ob_bl
  222. is_partial = ob_touched or overlap_pct < 100
  223. array.push(a_top, draw_top)
  224. array.push(a_bot, draw_bot)
  225. array.push(a_mid, draw_mid)
  226. array.push(a_left, time[ob_i])
  227. array.push(a_is_bear, looking_for_bear_ob)
  228. array.push(a_is_partial, is_partial)
  229. array.push(a_fvg_cnt, 1)
  230. e = "B|" + str.tostring(draw_top) + "|" + str.tostring(draw_bot) + "|" + str.tostring(draw_mid) + "|" + str.tostring(time[ob_i]) + "|" + (looking_for_bear_ob ? "1" : "0") + "|" + (is_partial ? "1" : "0") + "|1"
  231. events := events == "" ? e : events + ";" + e
  232. events
  233.  
  234. // ─────────────────────────────────────────────────────────
  235. // SECURITY CALLS
  236. // ─────────────────────────────────────────────────────────
  237. chart_secs = timeframe.in_seconds()
  238.  
  239. safe_tf(simple string tf) =>
  240. timeframe.in_seconds(tf) <= chart_secs ? tf : timeframe.period
  241.  
  242. use_ltf_2h = chart_secs > timeframe.in_seconds("120")
  243. use_ltf_4h = chart_secs > timeframe.in_seconds("240")
  244. use_ltf_6h = chart_secs > timeframe.in_seconds("360")
  245. use_ltf_8h = chart_secs > timeframe.in_seconds("480")
  246. use_ltf_12h = chart_secs > timeframe.in_seconds("720")
  247.  
  248. events_2h_std = request.security(syminfo.tickerid, "120", bb_logic(lookback, ob_min_overlap, fvg_strict_pct), lookahead=barmerge.lookahead_on)
  249. events_4h_std = request.security(syminfo.tickerid, "240", bb_logic(lookback, ob_min_overlap, fvg_strict_pct), lookahead=barmerge.lookahead_on)
  250. events_6h_std = request.security(syminfo.tickerid, "360", bb_logic(lookback, ob_min_overlap, fvg_strict_pct), lookahead=barmerge.lookahead_on)
  251. events_8h_std = request.security(syminfo.tickerid, "480", bb_logic(lookback, ob_min_overlap, fvg_strict_pct), lookahead=barmerge.lookahead_on)
  252. events_12h_std = request.security(syminfo.tickerid, "720", bb_logic(lookback, ob_min_overlap, fvg_strict_pct), lookahead=barmerge.lookahead_on)
  253.  
  254. events_2h_ltf = request.security_lower_tf(syminfo.tickerid, safe_tf("120"), bb_logic(lookback, ob_min_overlap, fvg_strict_pct))
  255. events_4h_ltf = request.security_lower_tf(syminfo.tickerid, safe_tf("240"), bb_logic(lookback, ob_min_overlap, fvg_strict_pct))
  256. events_6h_ltf = request.security_lower_tf(syminfo.tickerid, safe_tf("360"), bb_logic(lookback, ob_min_overlap, fvg_strict_pct))
  257. events_8h_ltf = request.security_lower_tf(syminfo.tickerid, safe_tf("480"), bb_logic(lookback, ob_min_overlap, fvg_strict_pct))
  258. events_12h_ltf = request.security_lower_tf(syminfo.tickerid, safe_tf("720"), bb_logic(lookback, ob_min_overlap, fvg_strict_pct))
  259.  
  260. // ─────────────────────────────────────────────────────────
  261. // CHART-SIDE STATE
  262. // ─────────────────────────────────────────────────────────
  263. var box[] active_boxes = array.new<box>()
  264. var line[] active_mids = array.new<line>()
  265. var int[] seen_born = array.new<int>()
  266. var int[] seen_died = array.new<int>()
  267.  
  268. // Trim dedup arrays to a rolling window to reduce replay desync.
  269. // Keeps the 500 most recent entries — enough to cover normal lookback
  270. // while allowing replay re-fires of older bars to be reprocessed.
  271. max_dedup = 500
  272. if array.size(seen_born) > max_dedup
  273. array.shift(seen_born)
  274. if array.size(seen_died) > max_dedup
  275. array.shift(seen_died)
  276.  
  277. if array.size(active_boxes) > 0
  278. for k = 0 to array.size(active_boxes) - 1
  279. box.set_right(array.get(active_boxes, k), timenow)
  280. if array.size(active_mids) > 0
  281. for k = 0 to array.size(active_mids) - 1
  282. line.set_x2(array.get(active_mids, k), timenow)
  283.  
  284. // ─────────────────────────────────────────────────────────
  285. // EVENT PROCESSOR
  286. // ─────────────────────────────────────────────────────────
  287. pick_col(fvg_cnt) =>
  288. fvg_cnt == 1 ? bb_col : fvg_cnt == 2 ? bb_col_x2 : fvg_cnt == 3 ? bb_col_x3 : bb_col_x4
  289.  
  290. process_events(events, lbl, show_toggle, tf_str, tf_secs) =>
  291. if events != "" and show_toggle
  292. norm_tf = switch tf_str
  293. "D" => "1D"
  294. "W" => "1W"
  295. => tf_str
  296. visible = not show_current_only or timeframe.period == norm_tf
  297. entries = str.split(events, ";")
  298. for entry in entries
  299. if entry != ""
  300. parts = str.split(entry, "|")
  301. etype = array.get(parts, 0)
  302.  
  303. if etype == "U"
  304. left_t = int(str.tonumber(array.get(parts, 1)))
  305. fvg_cnt = int(str.tonumber(array.get(parts, 2)))
  306. is_part = array.get(parts, 3) == "1"
  307. // Update label and colour on matching active box
  308. i = 0
  309. while i < array.size(active_boxes)
  310. if int(box.get_left(array.get(active_boxes, i))) == left_t
  311. prefix = is_part ? "p" : ""
  312. base_lbl = lbl + " " + prefix + "BB"
  313. disp_lbl = fvg_cnt > 1 ? base_lbl + " x" + str.tostring(fvg_cnt) : base_lbl
  314. box.set_text(array.get(active_boxes, i), show_labels ? disp_lbl : "")
  315. box.set_bgcolor(array.get(active_boxes, i), pick_col(fvg_cnt))
  316. i += 1
  317. else
  318. i += 1
  319.  
  320. if etype == "B"
  321. top = str.tonumber(array.get(parts, 1))
  322. bot = str.tonumber(array.get(parts, 2))
  323. mid_v = str.tonumber(array.get(parts, 3))
  324. left_t = int(str.tonumber(array.get(parts, 4)))
  325. is_part = array.get(parts, 6) == "1"
  326. fvg_cnt = int(str.tonumber(array.get(parts, 7)))
  327. if array.indexof(seen_born, left_t) < 0
  328. array.push(seen_born, left_t)
  329. if visible
  330. prefix = is_part ? "p" : ""
  331. base_lbl = lbl + " " + prefix + "BB"
  332. disp_lbl = fvg_cnt > 1 ? base_lbl + " x" + str.tostring(fvg_cnt) : base_lbl
  333. b = box.new(xloc=xloc.bar_time, left=left_t, right=timenow, top=top, bottom=bot, bgcolor=pick_col(fvg_cnt), border_color=border_col, border_width=border_width, extend=extend.right, text=show_labels ? disp_lbl : "", text_color=color.new(#7a808d, 20), text_size=size.small, text_halign=text.align_center, text_valign=text.align_center)
  334. l = line.new(xloc=xloc.bar_time, x1=left_t, y1=mid_v, x2=timenow, y2=mid_v, color=show_mid ? mid_col : color.new(color.white, 100), style=line.style_solid, extend=extend.right)
  335. array.push(active_boxes, b)
  336. array.push(active_mids, l)
  337.  
  338. if etype == "D"
  339. top = str.tonumber(array.get(parts, 1))
  340. bot = str.tonumber(array.get(parts, 2))
  341. mid_v = str.tonumber(array.get(parts, 3))
  342. left_t = int(str.tonumber(array.get(parts, 4)))
  343. right_t = int(str.tonumber(array.get(parts, 5)))
  344. is_part = array.get(parts, 7) == "1"
  345. fvg_cnt = int(str.tonumber(array.get(parts, 8)))
  346. already_died = array.indexof(seen_died, left_t) >= 0
  347. if not already_died
  348. array.push(seen_died, left_t)
  349. i = 0
  350. while i < array.size(active_boxes)
  351. if int(box.get_left(array.get(active_boxes, i))) == left_t
  352. box.delete(array.get(active_boxes, i))
  353. line.delete(array.get(active_mids, i))
  354. array.remove(active_boxes, i)
  355. array.remove(active_mids, i)
  356. else
  357. i += 1
  358. if not already_died and visible
  359. prefix = is_part ? "p" : ""
  360. base_lbl = lbl + " " + prefix + "BB"
  361. disp_lbl = fvg_cnt > 1 ? base_lbl + " x" + str.tostring(fvg_cnt) : base_lbl
  362. // Shift both edges left by one chart bar when source TF < chart TF
  363. shift = tf_secs < chart_secs ? timeframe.in_seconds() * 1000 : 0
  364. adj_left = left_t - shift
  365. adj_right = right_t - shift
  366. box.new(xloc=xloc.bar_time, left=adj_left, right=adj_right, top=top, bottom=bot, bgcolor=pick_col(fvg_cnt), border_color=border_col, border_width=border_width, extend=extend.none, text=show_labels ? disp_lbl : "", text_color=color.new(#7a808d, 20), text_size=size.small, text_halign=text.align_center, text_valign=text.align_center)
  367. line.new(xloc=xloc.bar_time, x1=adj_left, y1=mid_v, x2=adj_right, y2=mid_v, color=show_mid ? mid_col : color.new(color.white, 100), style=line.style_solid, extend=extend.none)
  368.  
  369. // ─────────────────────────────────────────────────────────
  370. // FIRE
  371. // ─────────────────────────────────────────────────────────
  372. fire(use_ltf, std, ltf, lbl, show_toggle, tf_str, tf_secs) =>
  373. if use_ltf
  374. if array.size(ltf) > 0
  375. for k = 0 to array.size(ltf) - 1
  376. process_events(array.get(ltf, k), lbl, show_toggle, tf_str, tf_secs)
  377. else
  378. process_events(std, lbl, show_toggle, tf_str, tf_secs)
  379.  
  380. fire(use_ltf_2h, events_2h_std, events_2h_ltf, "2H", show_2h, "120", timeframe.in_seconds("120"))
  381. fire(use_ltf_4h, events_4h_std, events_4h_ltf, "4H", show_4h, "240", timeframe.in_seconds("240"))
  382. fire(use_ltf_6h, events_6h_std, events_6h_ltf, "6H", show_6h, "360", timeframe.in_seconds("360"))
  383. fire(use_ltf_8h, events_8h_std, events_8h_ltf, "8H", show_8h, "480", timeframe.in_seconds("480"))
  384. fire(use_ltf_12h, events_12h_std, events_12h_ltf, "12H", show_12h, "720", timeframe.in_seconds("720"))
  385.  
Advertisement
Add Comment
Please, Sign In to add comment