Not a member of Pastebin yet?
Sign Up,
it unlocks many cool features!
- //@version=6
- indicator("BB MTF", overlay=true, max_boxes_count=500, max_lines_count=500)
- // ─────────────────────────────────────────────────────────
- // INPUTS
- // ─────────────────────────────────────────────────────────
- lookback = input.int(100, "Lookback (bars)", minval=10, maxval=500, group="General")
- ob_min_overlap = input.float(50, "Min FVG overlap % of OB", minval=1, maxval=100, group="General")
- fvg_strict_pct = input.float(25, "x2+ block mitigation threshold %", minval=1, maxval=49, step=1, group="General")
- bb_col = input.color(color.new(#0097a7, 75), "BB colour (x1)", group="Visual")
- bb_col_x2 = input.color(color.new(#0097a7, 75), "BB colour (x2)", group="Visual")
- bb_col_x3 = input.color(color.new(#0097a7, 75), "BB colour (x3)", group="Visual")
- bb_col_x4 = input.color(color.new(#0097a7, 75), "BB colour (x4+)",group="Visual")
- mid_col = input.color(color.new(#7a808d, 20), "Midline colour", group="Visual")
- show_mid = input.bool(false, "Show midline", group="Visual")
- show_labels = input.bool(true, "Show labels", group="Visual")
- border_col = input.color(color.new(#000000, 100), "Border colour", group="Visual")
- border_width = input.int(1, "Border width", minval=1, maxval=5, group="Visual")
- show_current_only = input.bool(false, "Show current TF only", group="Timeframes")
- show_2h = input.bool(false, "2H", group="Timeframes", inline="tfs1")
- show_4h = input.bool(true, "4H", group="Timeframes", inline="tfs1")
- show_6h = input.bool(false, "6H", group="Timeframes", inline="tfs1")
- show_8h = input.bool(false, "8H", group="Timeframes", inline="tfs1")
- show_12h = input.bool(false, "12H", group="Timeframes", inline="tfs2")
- // ─────────────────────────────────────────────────────────
- // HELPERS
- // ─────────────────────────────────────────────────────────
- body_high(i) => math.max(open[i], close[i])
- body_low(i) => math.min(open[i], close[i])
- is_bull(i) => close[i] > open[i]
- is_bear(i) => close[i] < open[i]
- // ─────────────────────────────────────────────────────────
- // CORE LOGIC
- // Returns event string: "B|top|bot|mid|left_t|is_bear|is_partial|fvg_cnt"
- // or "D|top|bot|mid|left_t|right_t|is_bear|is_partial|fvg_cnt"
- // Multiple events separated by ";"
- // ─────────────────────────────────────────────────────────
- bb_logic(simple int lb, simple float min_overlap, simple float strict_pct) =>
- var float[] a_top = array.new<float>()
- var float[] a_bot = array.new<float>()
- var float[] a_mid = array.new<float>()
- var int[] a_left = array.new<int>()
- var bool[] a_is_bear = array.new<bool>()
- var bool[] a_is_partial = array.new<bool>()
- var int[] a_fvg_cnt = array.new<int>()
- var float[] p_top = array.new<float>()
- var float[] p_bot = array.new<float>()
- var float[] p_mid = array.new<float>()
- var int[] p_left = array.new<int>()
- var bool[] p_is_bear = array.new<bool>()
- var bool[] p_is_partial = array.new<bool>()
- var int[] p_fvg_cnt = array.new<int>()
- events = ""
- // ── Resolve pending
- if array.size(p_top) > 0
- j = 0
- while j < array.size(p_top)
- mid_level = array.get(p_mid, j)
- fvg_cnt_p = array.get(p_fvg_cnt, j)
- p_top_v = array.get(p_top, j)
- p_bot_v = array.get(p_bot, j)
- p_bear_v = array.get(p_is_bear, j)
- 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
- bear_fvg = low[2] > high[0]
- bull_fvg = high[2] < low[0]
- c2_top = math.max(open[1], close[1])
- c2_bot = math.min(open[1], close[1])
- c2_crosses = c2_top >= eff_p_lvl and c2_bot <= eff_p_lvl
- fvg_exempt = (bear_fvg or bull_fvg) and c2_crosses
- if fvg_exempt
- new_cnt = array.get(p_fvg_cnt, j) + 1
- array.push(a_top, array.get(p_top, j))
- array.push(a_bot, array.get(p_bot, j))
- array.push(a_mid, array.get(p_mid, j))
- array.push(a_left, array.get(p_left, j))
- array.push(a_is_bear, array.get(p_is_bear, j))
- array.push(a_is_partial, array.get(p_is_partial, j))
- array.push(a_fvg_cnt, new_cnt)
- // Fire update event so chart side can refresh the label
- u = "U|" + str.tostring(array.get(p_left, j)) + "|" + str.tostring(new_cnt) + "|" + (array.get(p_is_partial, j) ? "1" : "0")
- events := events == "" ? u : events + ";" + u
- else
- 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))
- events := events == "" ? e : events + ";" + e
- array.remove(p_top, j)
- array.remove(p_bot, j)
- array.remove(p_mid, j)
- array.remove(p_left, j)
- array.remove(p_is_bear, j)
- array.remove(p_is_partial, j)
- array.remove(p_fvg_cnt, j)
- // ── Check active
- if array.size(a_top) > 0
- i = 0
- while i < array.size(a_top)
- mid_level = array.get(a_mid, i)
- fvg_cnt_a = array.get(a_fvg_cnt, i)
- a_top_v = array.get(a_top, i)
- a_bot_v = array.get(a_bot, i)
- a_bear_v = array.get(a_is_bear, i)
- // For x2+ blocks use stricter threshold — % from near edge instead of midpoint
- // Bearish BB: price approaches from above, strict level is just below top
- // Bullish BB: price approaches from below, strict level is just above bottom
- 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
- body_top_now = math.max(open, close)
- body_bot_now = math.min(open, close)
- body_crosses = body_top_now >= eff_level and body_bot_now <= eff_level
- wick_crosses = (high >= eff_level and low <= eff_level) and not body_crosses
- if wick_crosses
- 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))
- events := events == "" ? e : events + ";" + e
- array.remove(a_top, i)
- array.remove(a_bot, i)
- array.remove(a_mid, i)
- array.remove(a_left, i)
- array.remove(a_is_bear, i)
- array.remove(a_is_partial, i)
- array.remove(a_fvg_cnt, i)
- else if body_crosses
- array.push(p_top, array.get(a_top, i))
- array.push(p_bot, array.get(a_bot, i))
- array.push(p_mid, array.get(a_mid, i))
- array.push(p_left, array.get(a_left, i))
- array.push(p_is_bear, array.get(a_is_bear, i))
- array.push(p_is_partial, array.get(a_is_partial, i))
- array.push(p_fvg_cnt, array.get(a_fvg_cnt, i))
- array.remove(a_top, i)
- array.remove(a_bot, i)
- array.remove(a_mid, i)
- array.remove(a_left, i)
- array.remove(a_is_bear, i)
- array.remove(a_is_partial, i)
- array.remove(a_fvg_cnt, i)
- else
- i += 1
- // ── Detect new BBs
- if bar_index >= lb + 3
- fvg_c1 = 2
- fvg_c2 = 1
- fvg_c3 = 0
- fvg_bear_top = low[fvg_c1]
- fvg_bear_bot = high[fvg_c3]
- valid_bear_fvg = is_bear(fvg_c2) and fvg_bear_top > fvg_bear_bot
- fvg_bull_bot = high[fvg_c1]
- fvg_bull_top = low[fvg_c3]
- valid_bull_fvg = is_bull(fvg_c2) and fvg_bull_top > fvg_bull_bot
- if valid_bear_fvg or valid_bull_fvg
- fvg_top = valid_bear_fvg ? fvg_bear_top : fvg_bull_top
- fvg_bot = valid_bear_fvg ? fvg_bear_bot : fvg_bull_bot
- looking_for_bear_ob = valid_bear_fvg
- // Precompute prefix min/max arrays for the lookback window once,
- // so the inner mitigation check (was O(n)) becomes O(1) per ob_i.
- // prefix_low[k] = lowest low from bar fvg_c1+1 to bar fvg_c1+1+k
- // prefix_high[k] = highest high from bar fvg_c1+1 to bar fvg_c1+1+k
- prefix_low = array.new<float>()
- prefix_high = array.new<float>()
- running_low = high[fvg_c1 + 1] // initialise to opposite extreme
- running_high = low[fvg_c1 + 1]
- for k = fvg_c1 + 1 to lb - 1
- running_low := math.min(running_low, low[k])
- running_high := math.max(running_high, high[k])
- array.push(prefix_low, running_low)
- array.push(prefix_high, running_high)
- for ob_i = fvg_c1 + 1 to lb - 1
- ob_engulf = ob_i - 1
- ob_colour_match = (looking_for_bear_ob and is_bear(ob_i)) or (not looking_for_bear_ob and is_bull(ob_i))
- if ob_colour_match
- ob_bh = body_high(ob_i)
- ob_bl = body_low(ob_i)
- ob_mid = (ob_bh + ob_bl) / 2
- b_is_opposite = looking_for_bear_ob ? is_bull(ob_engulf) : is_bear(ob_engulf)
- b_closes_beyond = looking_for_bear_ob ? close[ob_engulf] > ob_bh : close[ob_engulf] < ob_bl
- if b_is_opposite and b_closes_beyond
- mitigated = false
- deepest_high = ob_bl
- deepest_low = ob_bh
- // Use prefix arrays for O(1) range min/max instead of inner loop
- // Range to check: bars fvg_c1+1 to ob_engulf-1
- // prefix arrays are 0-indexed from fvg_c1+1, so:
- // index of ob_engulf-1 in prefix = (ob_engulf-1) - (fvg_c1+1) = ob_engulf - fvg_c1 - 2
- range_end_idx = ob_engulf - fvg_c1 - 2
- if ob_engulf >= fvg_c1 + 2 and range_end_idx >= 0
- range_low = array.get(prefix_low, range_end_idx)
- range_high = array.get(prefix_high, range_end_idx)
- // Check if any bar in range entered the OB body
- if range_high > ob_bl and range_low < ob_bh
- if looking_for_bear_ob
- if range_low <= ob_mid
- mitigated := true
- else
- deepest_low := range_low
- else
- if range_high >= ob_mid
- mitigated := true
- else
- deepest_high := range_high
- if not mitigated
- overlap_top = math.min(ob_bh, fvg_top)
- overlap_bot = math.max(ob_bl, fvg_bot)
- overlap_range = overlap_top - overlap_bot
- ob_range = ob_bh - ob_bl
- overlap_pct = ob_range > 0 ? (overlap_range / ob_range) * 100 : 0.0
- if overlap_pct >= min_overlap
- draw_top = overlap_top
- draw_bot = overlap_bot
- if looking_for_bear_ob
- draw_top := math.min(draw_top, deepest_low)
- else
- draw_bot := math.max(draw_bot, deepest_high)
- draw_mid = (draw_top + draw_bot) / 2
- ob_touched = looking_for_bear_ob ? deepest_low < ob_bh : deepest_high > ob_bl
- is_partial = ob_touched or overlap_pct < 100
- array.push(a_top, draw_top)
- array.push(a_bot, draw_bot)
- array.push(a_mid, draw_mid)
- array.push(a_left, time[ob_i])
- array.push(a_is_bear, looking_for_bear_ob)
- array.push(a_is_partial, is_partial)
- array.push(a_fvg_cnt, 1)
- 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"
- events := events == "" ? e : events + ";" + e
- events
- // ─────────────────────────────────────────────────────────
- // SECURITY CALLS
- // ─────────────────────────────────────────────────────────
- chart_secs = timeframe.in_seconds()
- safe_tf(simple string tf) =>
- timeframe.in_seconds(tf) <= chart_secs ? tf : timeframe.period
- use_ltf_2h = chart_secs > timeframe.in_seconds("120")
- use_ltf_4h = chart_secs > timeframe.in_seconds("240")
- use_ltf_6h = chart_secs > timeframe.in_seconds("360")
- use_ltf_8h = chart_secs > timeframe.in_seconds("480")
- use_ltf_12h = chart_secs > timeframe.in_seconds("720")
- events_2h_std = request.security(syminfo.tickerid, "120", bb_logic(lookback, ob_min_overlap, fvg_strict_pct), lookahead=barmerge.lookahead_on)
- events_4h_std = request.security(syminfo.tickerid, "240", bb_logic(lookback, ob_min_overlap, fvg_strict_pct), lookahead=barmerge.lookahead_on)
- events_6h_std = request.security(syminfo.tickerid, "360", bb_logic(lookback, ob_min_overlap, fvg_strict_pct), lookahead=barmerge.lookahead_on)
- events_8h_std = request.security(syminfo.tickerid, "480", bb_logic(lookback, ob_min_overlap, fvg_strict_pct), lookahead=barmerge.lookahead_on)
- events_12h_std = request.security(syminfo.tickerid, "720", bb_logic(lookback, ob_min_overlap, fvg_strict_pct), lookahead=barmerge.lookahead_on)
- events_2h_ltf = request.security_lower_tf(syminfo.tickerid, safe_tf("120"), bb_logic(lookback, ob_min_overlap, fvg_strict_pct))
- events_4h_ltf = request.security_lower_tf(syminfo.tickerid, safe_tf("240"), bb_logic(lookback, ob_min_overlap, fvg_strict_pct))
- events_6h_ltf = request.security_lower_tf(syminfo.tickerid, safe_tf("360"), bb_logic(lookback, ob_min_overlap, fvg_strict_pct))
- events_8h_ltf = request.security_lower_tf(syminfo.tickerid, safe_tf("480"), bb_logic(lookback, ob_min_overlap, fvg_strict_pct))
- events_12h_ltf = request.security_lower_tf(syminfo.tickerid, safe_tf("720"), bb_logic(lookback, ob_min_overlap, fvg_strict_pct))
- // ─────────────────────────────────────────────────────────
- // CHART-SIDE STATE
- // ─────────────────────────────────────────────────────────
- var box[] active_boxes = array.new<box>()
- var line[] active_mids = array.new<line>()
- var int[] seen_born = array.new<int>()
- var int[] seen_died = array.new<int>()
- // Trim dedup arrays to a rolling window to reduce replay desync.
- // Keeps the 500 most recent entries — enough to cover normal lookback
- // while allowing replay re-fires of older bars to be reprocessed.
- max_dedup = 500
- if array.size(seen_born) > max_dedup
- array.shift(seen_born)
- if array.size(seen_died) > max_dedup
- array.shift(seen_died)
- if array.size(active_boxes) > 0
- for k = 0 to array.size(active_boxes) - 1
- box.set_right(array.get(active_boxes, k), timenow)
- if array.size(active_mids) > 0
- for k = 0 to array.size(active_mids) - 1
- line.set_x2(array.get(active_mids, k), timenow)
- // ─────────────────────────────────────────────────────────
- // EVENT PROCESSOR
- // ─────────────────────────────────────────────────────────
- pick_col(fvg_cnt) =>
- fvg_cnt == 1 ? bb_col : fvg_cnt == 2 ? bb_col_x2 : fvg_cnt == 3 ? bb_col_x3 : bb_col_x4
- process_events(events, lbl, show_toggle, tf_str, tf_secs) =>
- if events != "" and show_toggle
- norm_tf = switch tf_str
- "D" => "1D"
- "W" => "1W"
- => tf_str
- visible = not show_current_only or timeframe.period == norm_tf
- entries = str.split(events, ";")
- for entry in entries
- if entry != ""
- parts = str.split(entry, "|")
- etype = array.get(parts, 0)
- if etype == "U"
- left_t = int(str.tonumber(array.get(parts, 1)))
- fvg_cnt = int(str.tonumber(array.get(parts, 2)))
- is_part = array.get(parts, 3) == "1"
- // Update label and colour on matching active box
- i = 0
- while i < array.size(active_boxes)
- if int(box.get_left(array.get(active_boxes, i))) == left_t
- prefix = is_part ? "p" : ""
- base_lbl = lbl + " " + prefix + "BB"
- disp_lbl = fvg_cnt > 1 ? base_lbl + " x" + str.tostring(fvg_cnt) : base_lbl
- box.set_text(array.get(active_boxes, i), show_labels ? disp_lbl : "")
- box.set_bgcolor(array.get(active_boxes, i), pick_col(fvg_cnt))
- i += 1
- else
- i += 1
- if etype == "B"
- top = str.tonumber(array.get(parts, 1))
- bot = str.tonumber(array.get(parts, 2))
- mid_v = str.tonumber(array.get(parts, 3))
- left_t = int(str.tonumber(array.get(parts, 4)))
- is_part = array.get(parts, 6) == "1"
- fvg_cnt = int(str.tonumber(array.get(parts, 7)))
- if array.indexof(seen_born, left_t) < 0
- array.push(seen_born, left_t)
- if visible
- prefix = is_part ? "p" : ""
- base_lbl = lbl + " " + prefix + "BB"
- disp_lbl = fvg_cnt > 1 ? base_lbl + " x" + str.tostring(fvg_cnt) : base_lbl
- 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)
- 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)
- array.push(active_boxes, b)
- array.push(active_mids, l)
- if etype == "D"
- top = str.tonumber(array.get(parts, 1))
- bot = str.tonumber(array.get(parts, 2))
- mid_v = str.tonumber(array.get(parts, 3))
- left_t = int(str.tonumber(array.get(parts, 4)))
- right_t = int(str.tonumber(array.get(parts, 5)))
- is_part = array.get(parts, 7) == "1"
- fvg_cnt = int(str.tonumber(array.get(parts, 8)))
- already_died = array.indexof(seen_died, left_t) >= 0
- if not already_died
- array.push(seen_died, left_t)
- i = 0
- while i < array.size(active_boxes)
- if int(box.get_left(array.get(active_boxes, i))) == left_t
- box.delete(array.get(active_boxes, i))
- line.delete(array.get(active_mids, i))
- array.remove(active_boxes, i)
- array.remove(active_mids, i)
- else
- i += 1
- if not already_died and visible
- prefix = is_part ? "p" : ""
- base_lbl = lbl + " " + prefix + "BB"
- disp_lbl = fvg_cnt > 1 ? base_lbl + " x" + str.tostring(fvg_cnt) : base_lbl
- // Shift both edges left by one chart bar when source TF < chart TF
- shift = tf_secs < chart_secs ? timeframe.in_seconds() * 1000 : 0
- adj_left = left_t - shift
- adj_right = right_t - shift
- 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)
- 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)
- // ─────────────────────────────────────────────────────────
- // FIRE
- // ─────────────────────────────────────────────────────────
- fire(use_ltf, std, ltf, lbl, show_toggle, tf_str, tf_secs) =>
- if use_ltf
- if array.size(ltf) > 0
- for k = 0 to array.size(ltf) - 1
- process_events(array.get(ltf, k), lbl, show_toggle, tf_str, tf_secs)
- else
- process_events(std, lbl, show_toggle, tf_str, tf_secs)
- fire(use_ltf_2h, events_2h_std, events_2h_ltf, "2H", show_2h, "120", timeframe.in_seconds("120"))
- fire(use_ltf_4h, events_4h_std, events_4h_ltf, "4H", show_4h, "240", timeframe.in_seconds("240"))
- fire(use_ltf_6h, events_6h_std, events_6h_ltf, "6H", show_6h, "360", timeframe.in_seconds("360"))
- fire(use_ltf_8h, events_8h_std, events_8h_ltf, "8H", show_8h, "480", timeframe.in_seconds("480"))
- fire(use_ltf_12h, events_12h_std, events_12h_ltf, "12H", show_12h, "720", timeframe.in_seconds("720"))
Advertisement
Add Comment
Please, Sign In to add comment