16 agents onlineXAUUSD 4 552,4Session: London KillzoneNext analysis: 04:12
Indicators · Pine Script v6 · Gratis
● Free · TradingView

Tetris ICT [Bolívar]: your ICT indicator for TradingView

The market keeps memory. Every Fair Value Gap, every unmitigated Order Block is a structural debt the price will eventually settle. Tetris ICT [Bolívar] turns those debts into Tetris pieces on screen so you can learn to read the market's hidden order.

Built by Xavier Mas, founder of Bolívar Bolsa, for over 42,000 ICT/SMC students across Latin America. Free, unrestricted, written in Pine Script v6.

Add on TradingViewView the codeDownload .pine

What does it do?

  • Fair Value Gap → I piece. Detects 3-candle imbalances and marks them as unfilled zones.
  • Order Block → O piece. Identifies the last opposite candle before an expansive move: the institutional origin of the impulse.
  • Balanced Price Range (BPR) → S piece. Maps the overlap of two opposing FVGs — the zone of maximum price efficiency.
  • Breaker Block → L piece. Detects Order Blocks that have failed and turned into reverse structure.
  • Liquidity zones → T piece. Marks stop accumulations (equal highs/lows) where price tends to sweep before reversing.
  • Arcade-style side HUD. Groups active (unmitigated) pieces with a real-time structural debt counter.

How to read the score

The HUD calculates how many unmitigated zones remain active above price and shows one of three states:

ScoreStateEducational reading
≥ 65BLOCKEDHigh structural debt density. Price faces pending zones on multiple levels.
30 – 64CAUTIONModerate density. Some unmitigated zones may influence the path.
< 30GREENClean board. Few active debts; price moves in open space.

The next-piece preview anticipates which type of zone appears closest to price. Each piece disappears from the board when price mitigates it, just like a complete row in Tetris.

How to install on TradingView

  1. Open the Pine Editor. In TradingView, click the Pine Editor tab (bottom panel).
  2. Paste the code. Copy the full code below and replace the editor contents.
  3. Add to chart. Click "Add to chart" (top right of the editor).
  4. Save it. Press Ctrl/Cmd + S and give it a name. It will be stored in your TradingView library.

Pine Script v6 code

Tetris-ICT [Bolívar] · Pine v6 · 1137 lines · Free.

Download .pine
// ============================================================
// Tetris-ICT — bolivarbolsa.com — by Xavier Mas
// Indicador TradingView Pine Script v6
//
// Concepto: Las deudas estructurales del mercado ICT (FVG, OB,
// Breaker, BPR, Liquidez) se visualizan como piezas de Tetris
// en un tablero lateral con score HUD, next-piece preview y
// paleta de colores neón arcade. Cada zona no mitigada es una
// pieza activa que "bloquea" el precio hasta ser rellenada.
//
// Estructuras detectadas:
//   FVG  → pieza I (cyan / rojo)
//   OB   → pieza O (verde / naranja)
//   BPR  → pieza S (oro)
//   Breaker → pieza L (azul / violeta)
//   Liq  → pieza T (magenta / violeta oscuro)
//
// Scoring:
//   Debt Long  = presión bajista encima del precio (FVG bear, OB bear, etc.)
//   Debt Short = presión alcista debajo del precio
//   BLOCKED (≥65) / CAUTION (≥30) / GREEN (<30)
//
// Créditos: Xavier Mas — Fundador bolivarbolsa.com
//   42k estudiantes Forex/ICT/SMC LATAM
// ============================================================
//@version=6
indicator("Tetris-ICT [Bolívar]", shorttitle="TETRIS-ICT", overlay=true,
          max_boxes_count=500, max_lines_count=100, max_labels_count=150, max_bars_back=500)

// ============================================================
// PALETA DE COLORES NEÓN SYNTHWAVE
// ============================================================
var color C_FVG_BULL      = #00F5FF
var color C_FVG_BEAR      = #FF3131
var color C_OB_BULL       = #39FF14
var color C_OB_BEAR       = #FF7F00
var color C_BPR           = #FFD700
var color C_BREAKER_BULL  = #4D79FF
var color C_BREAKER_BEAR  = #CC44FF
var color C_LIQ_HIGH      = #FF00FF
var color C_LIQ_LOW       = #9D00FF
var color C_BG            = #0D0D0D
var color C_GRID          = #1A1A2E
var color C_TEXT          = #00FF41
var color C_BLOCKED       = #FF0044
var color C_GREEN         = #00FF41
var color C_CAUTION       = #FFFF00

// ============================================================
// INPUTS — GRUPO "Detection"
// ============================================================
string GRP_DET = "Detection"
i_fvg_atr_mult    = input.float(0.15, "FVG min gap (ATR × factor)", minval=0.0, step=0.05, group=GRP_DET,
                     tooltip="0 = sin filtro de tamaño mínimo")
i_ob_lookback     = input.int(10, "OB lookback (barras)", minval=3, maxval=10, group=GRP_DET,
                     tooltip="Lookback limité à 10 para rendimiento")
i_swing_len       = input.int(5, "Swing length (pivot left/right)", minval=2, maxval=20, group=GRP_DET)
i_bpr_enabled     = input.bool(true, "Detectar BPR (Balanced Price Range)", group=GRP_DET)
i_mitigation_mode = input.string("Wick", "Modo mitigación", options=["Wick", "50%", "Close"], group=GRP_DET)
i_max_per_type    = input.int(10, "Máx. estructuras activas por tipo", minval=3, maxval=20, group=GRP_DET)

// ============================================================
// INPUTS — GRUPO "Visuals"
// ============================================================
string GRP_VIS = "Visuals"
i_show_onchart = input.bool(true,  "Mostrar bloques sobre el precio", group=GRP_VIS)
i_show_board   = input.bool(true,  "Mostrar Game Board lateral", group=GRP_VIS)
i_board_pos    = input.string("Bottom Right", "Posición del Game Board", options=["Top Right", "Middle Right", "Bottom Right", "Top Left", "Middle Left", "Bottom Left"], group=GRP_VIS)
i_board_size   = input.string("Compact", "Tamaño Game Board", options=["Compact", "Normal", "Large"], group=GRP_VIS)
i_show_hud     = input.bool(true,  "Mostrar HUD (top-right)", group=GRP_VIS)
i_hud_pos      = input.string("Top Right", "Posición del HUD", options=["Top Right", "Top Center", "Top Left", "Bottom Right", "Bottom Center", "Bottom Left"], group=GRP_VIS)
i_show_next    = input.bool(true,  "Mostrar Next-Piece (top-left)", group=GRP_VIS)
i_next_pos     = input.string("Top Left", "Posición del Next-Piece", options=["Top Left", "Top Center", "Top Right", "Bottom Left", "Bottom Center", "Bottom Right"], group=GRP_VIS)
i_show_grid    = input.bool(true,  "Mostrar grilla horizontal ATR", group=GRP_VIS)
i_opacity      = input.int(70, "Opacidad bloques", minval=10, maxval=95, group=GRP_VIS)
i_beginner     = input.bool(false, "Modo principiante (labels extendidos)", group=GRP_VIS)

// ============================================================
// INPUTS — GRUPO "Scoring"
// ============================================================
string GRP_SCO = "Scoring"
i_w_fvg          = input.float(15.0, "Peso FVG",     minval=0.0, group=GRP_SCO)
i_w_ob           = input.float(20.0, "Peso OB",      minval=0.0, group=GRP_SCO)
i_w_breaker      = input.float(25.0, "Peso Breaker", minval=0.0, group=GRP_SCO)
i_w_bpr          = input.float(20.0, "Peso BPR",     minval=0.0, group=GRP_SCO)
i_w_liq          = input.float(22.0, "Peso Liq",     minval=0.0, group=GRP_SCO)
i_thresh_blocked = input.int(65, "Umbral BLOCKED", minval=1, maxval=100, group=GRP_SCO)
i_thresh_caution = input.int(30, "Umbral CAUTION", minval=1, maxval=100, group=GRP_SCO)
i_dist_cap_atr   = input.float(6.0, "Distancia máx. (ATR ×)", minval=1.0, group=GRP_SCO)
i_age_halflife   = input.int(500, "Semi-vida edad (barras)", minval=50, group=GRP_SCO)

// ============================================================
// INPUTS — GRUPO "Alerts"
// ============================================================
string GRP_ALT = "Alerts"
i_alert_blocked    = input.bool(true, "Alerta BLOCKED", group=GRP_ALT)
i_alert_green      = input.bool(true, "Alerta GREEN",   group=GRP_ALT)
i_alert_mitigation = input.bool(true, "Alerta Mitigación", group=GRP_ALT)

// ============================================================
// ARRAYS DE DATOS — FVG
// ============================================================
var array<float> fvg_top   = array.new<float>()
var array<float> fvg_bot   = array.new<float>()
var array<int>   fvg_dir   = array.new<int>()
var array<int>   fvg_bar   = array.new<int>()
var array<bool>  fvg_mitig = array.new<bool>()
var array<box>   fvg_box   = array.new<box>()

// ============================================================
// ARRAYS DE DATOS — OB (Order Block)
// ============================================================
var array<float> ob_top   = array.new<float>()
var array<float> ob_bot   = array.new<float>()
var array<int>   ob_dir   = array.new<int>()
var array<int>   ob_bar   = array.new<int>()
var array<bool>  ob_mitig = array.new<bool>()
var array<box>   ob_box   = array.new<box>()

// ============================================================
// ARRAYS DE DATOS — BREAKER
// ============================================================
var array<float> brk_top   = array.new<float>()
var array<float> brk_bot   = array.new<float>()
var array<int>   brk_dir   = array.new<int>()
var array<int>   brk_bar   = array.new<int>()
var array<bool>  brk_mitig = array.new<bool>()
var array<box>   brk_box   = array.new<box>()
var int          brk_max   = 8

// ============================================================
// ARRAYS DE DATOS — BPR (Balanced Price Range)
// ============================================================
var array<float> bpr_top   = array.new<float>()
var array<float> bpr_bot   = array.new<float>()
var array<int>   bpr_dir   = array.new<int>()
var array<int>   bpr_bar   = array.new<int>()
var array<bool>  bpr_mitig = array.new<bool>()
var array<box>   bpr_box   = array.new<box>()
var int          bpr_max   = 6

// ============================================================
// ARRAYS DE DATOS — LIQUIDEZ (BSL / SSL)
// ============================================================
var array<float> liq_top   = array.new<float>()
var array<float> liq_bot   = array.new<float>()
var array<int>   liq_dir   = array.new<int>()
var array<int>   liq_bar   = array.new<int>()
var array<bool>  liq_mitig = array.new<bool>()
var array<box>   liq_box   = array.new<box>()

// ============================================================
// VARIABLES DE ESTADO GLOBAL
// ============================================================
var int   lines_cleared    = 0
var int   blink_until      = 0
var bool  mitig_event      = false
var float mitig_price      = na
var string mitig_type      = ""

// Señales previas para alertas de transición
var string prev_signal_long  = "GREEN"
var string prev_signal_short = "GREEN"
var int    prev_lines        = 0

// ============================================================
// GRILLA HORIZONTAL (creada una sola vez)
// ============================================================
var array<line> grid_lines = array.new<line>()
if i_show_grid and array.size(grid_lines) == 0
    for _i = 0 to 19
        array.push(grid_lines, line.new(bar_index, close, bar_index + 1, close,
                   color=color.new(C_GRID, 80), style=line.style_dotted, width=1))

// ============================================================
// TABLAS (creadas una sola vez)
// ============================================================
var table tbl_hud  = na
var table tbl_next = na
var table tbl_board = na

// Helper: convertir string del input → position constant
pos_of(s) =>
    switch s
        "Top Right"     => position.top_right
        "Top Center"    => position.top_center
        "Top Left"      => position.top_left
        "Middle Right"  => position.middle_right
        "Middle Center" => position.middle_center
        "Middle Left"   => position.middle_left
        "Bottom Right"  => position.bottom_right
        "Bottom Center" => position.bottom_center
        "Bottom Left"   => position.bottom_left
        => position.bottom_right

// Dimensiones de cada celda del Game Board (% del chart)
board_cell_w = i_board_size == "Compact" ? 1.4 : i_board_size == "Normal" ? 2.2 : 3.0
board_cell_h = i_board_size == "Compact" ? 0.9 : i_board_size == "Normal" ? 1.4 : 1.9

if i_show_hud and na(tbl_hud)
    tbl_hud := table.new(pos_of(i_hud_pos), 2, 6,
                         bgcolor=C_BG, frame_color=C_TEXT, frame_width=2,
                         border_color=color.new(C_TEXT, 60), border_width=1)

if i_show_next and na(tbl_next)
    tbl_next := table.new(pos_of(i_next_pos), 6, 8,
                          bgcolor=C_BG, frame_color=C_LIQ_HIGH, frame_width=2,
                          border_color=color.new(C_LIQ_HIGH, 60), border_width=1)

if i_show_board and na(tbl_board)
    tbl_board := table.new(pos_of(i_board_pos), 12, 22,
                           bgcolor=C_BG, frame_color=C_TEXT, frame_width=3,
                           border_color=color.new(C_TEXT, 50), border_width=1)

// ============================================================
// ATR Y PARÁMETROS BASE
// ============================================================
atr14   = ta.atr(14)
fvg_min = atr14 * i_fvg_atr_mult

// ============================================================
// FUNCIÓN: color segun señal
// ============================================================
sig_color(sig) =>
    sig == "BLOCKED" ? C_BLOCKED : sig == "CAUTION" ? C_CAUTION : C_GREEN

// ============================================================
// FUNCIÓN: barra de score ASCII
// ============================================================
score_bar(v) =>
    filled = int(math.round(math.max(0.0, math.min(100.0, v)) / 10.0))
    str.repeat("▓", filled) + str.repeat("░", 10 - filled)

// ============================================================
// FUNCIÓN: verificar mitigación de una zona
// ============================================================
is_mitigated(top_val, bot_val) =>
    mid = (top_val + bot_val) / 2.0
    result = false
    if i_mitigation_mode == "Wick"
        result := low <= top_val and high >= bot_val
    else if i_mitigation_mode == "50%"
        result := low <= mid or high >= mid
    else
        result := close >= bot_val and close <= top_val
    result

// ============================================================
// FUNCIÓN: pruning de un array de estructuras
// ============================================================
prune_struct(tops, bots, dirs, bars, mitigs, boxes, max_count) =>
    // Primero eliminar mitigadas
    i = 0
    while i < array.size(mitigs)
        if array.get(mitigs, i)
            b = array.get(boxes, i)
            if not na(b)
                box.delete(b)
            array.remove(tops,   i)
            array.remove(bots,   i)
            array.remove(dirs,   i)
            array.remove(bars,   i)
            array.remove(mitigs, i)
            array.remove(boxes,  i)
        else
            i += 1
    // Luego FIFO si sigue sobre el límite
    while array.size(tops) >= max_count
        b = array.get(boxes, 0)
        if not na(b)
            box.delete(b)
        array.remove(tops,   0)
        array.remove(bots,   0)
        array.remove(dirs,   0)
        array.remove(bars,   0)
        array.remove(mitigs, 0)
        array.remove(boxes,  0)

// ============================================================
// DETECCIÓN FVG — solo en barras confirmadas
// ============================================================
if barstate.isconfirmed
    // FVG alcista: vela[0].low > vela[2].high → gap entre vela 2 y vela 0
    bull_fvg_size = low[1] - high[3]
    if low[1] > high[3] and bull_fvg_size >= fvg_min
        prune_struct(fvg_top, fvg_bot, fvg_dir, fvg_bar, fvg_mitig, fvg_box, i_max_per_type)
        new_box = box.new(bar_index[2], high[3], bar_index + 5, low[1],
                          border_color=C_FVG_BULL, border_width=2,
                          bgcolor=color.new(C_FVG_BULL, i_opacity))
        array.push(fvg_top,   low[1])
        array.push(fvg_bot,   high[3])
        array.push(fvg_dir,   1)
        array.push(fvg_bar,   bar_index[2])
        array.push(fvg_mitig, false)
        array.push(fvg_box,   new_box)

    // FVG bajista: vela[0].high < vela[2].low → gap inverso
    bear_fvg_size = low[3] - high[1]
    if high[1] < low[3] and bear_fvg_size >= fvg_min
        prune_struct(fvg_top, fvg_bot, fvg_dir, fvg_bar, fvg_mitig, fvg_box, i_max_per_type)
        new_box = box.new(bar_index[2], high[1], bar_index + 5, low[3],
                          border_color=C_FVG_BEAR, border_width=2,
                          bgcolor=color.new(C_FVG_BEAR, i_opacity))
        array.push(fvg_top,   low[3])
        array.push(fvg_bot,   high[1])
        array.push(fvg_dir,   -1)
        array.push(fvg_bar,   bar_index[2])
        array.push(fvg_mitig, false)
        array.push(fvg_box,   new_box)

// ============================================================
// DETECCIÓN OB (Order Block)
// ============================================================
if barstate.isconfirmed
    // OB alcista: última vela bajista antes de un desplazamiento alcista fuerte
    // Condición: close actual > high[3], y hay una vela bajista reciente
    displacement_bull = close > high[3] and (close - low[3]) >= atr14 * 0.5
    if displacement_bull
        best_bear_idx = 0
        best_bear_body = 0.0
        for k = 1 to math.min(i_ob_lookback, 10)
            body_k = open[k] - close[k]
            if body_k > best_bear_body and body_k >= atr14 * 0.5
                best_bear_body := body_k
                best_bear_idx  := k
        if best_bear_idx > 0 and best_bear_body > 0.0
            ob_t = high[best_bear_idx]
            ob_b = low[best_bear_idx]
            // Verificar que no existe ya un OB muy cercano
            already_exists = false
            if array.size(ob_top) > 0
                for m = 0 to array.size(ob_top) - 1
                    if math.abs(array.get(ob_top, m) - ob_t) < atr14 * 0.3
                        already_exists := true
                        break
            if not already_exists
                prune_struct(ob_top, ob_bot, ob_dir, ob_bar, ob_mitig, ob_box, i_max_per_type)
                new_box = box.new(bar_index[best_bear_idx], ob_b, bar_index + 5, ob_t,
                                  border_color=C_OB_BULL, border_width=2,
                                  bgcolor=color.new(C_OB_BULL, i_opacity))
                array.push(ob_top,   ob_t)
                array.push(ob_bot,   ob_b)
                array.push(ob_dir,   1)
                array.push(ob_bar,   bar_index[best_bear_idx])
                array.push(ob_mitig, false)
                array.push(ob_box,   new_box)

    // OB bajista: última vela alcista antes de un desplazamiento bajista fuerte
    displacement_bear = close < low[3] and (high[3] - close) >= atr14 * 0.5
    if displacement_bear
        best_bull_idx  = 0
        best_bull_body = 0.0
        for k = 1 to math.min(i_ob_lookback, 10)
            body_k = close[k] - open[k]
            if body_k > best_bull_body and body_k >= atr14 * 0.5
                best_bull_body := body_k
                best_bull_idx  := k
        if best_bull_idx > 0 and best_bull_body > 0.0
            ob_t = high[best_bull_idx]
            ob_b = low[best_bull_idx]
            already_exists = false
            if array.size(ob_top) > 0
                for m = 0 to array.size(ob_top) - 1
                    if math.abs(array.get(ob_top, m) - ob_t) < atr14 * 0.3
                        already_exists := true
                        break
            if not already_exists
                prune_struct(ob_top, ob_bot, ob_dir, ob_bar, ob_mitig, ob_box, i_max_per_type)
                new_box = box.new(bar_index[best_bull_idx], ob_b, bar_index + 5, ob_t,
                                  border_color=C_OB_BEAR, border_width=2,
                                  bgcolor=color.new(C_OB_BEAR, i_opacity))
                array.push(ob_top,   ob_t)
                array.push(ob_bot,   ob_b)
                array.push(ob_dir,   -1)
                array.push(ob_bar,   bar_index[best_bull_idx])
                array.push(ob_mitig, false)
                array.push(ob_box,   new_box)

// ============================================================
// DETECCIÓN BREAKER — OB invalidado (precio cierra más allá)
// ============================================================
if barstate.isconfirmed
    i = 0
    while i < array.size(ob_top)
        ob_t   = array.get(ob_top,  i)
        ob_b   = array.get(ob_bot,  i)
        ob_d   = array.get(ob_dir,  i)
        ob_br  = array.get(ob_bar,  i)
        // OB alcista invalidado → precio cierra BAJO el OB → se convierte en bearish breaker
        if ob_d == 1 and close < ob_b
            prune_struct(brk_top, brk_bot, brk_dir, brk_bar, brk_mitig, brk_box, brk_max)
            new_box = box.new(ob_br, ob_b, bar_index + 5, ob_t,
                              border_color=C_BREAKER_BEAR, border_width=2,
                              bgcolor=color.new(C_BREAKER_BEAR, i_opacity))
            array.push(brk_top,   ob_t)
            array.push(brk_bot,   ob_b)
            array.push(brk_dir,   -1)
            array.push(brk_bar,   ob_br)
            array.push(brk_mitig, false)
            array.push(brk_box,   new_box)
            // Eliminar del array OB
            old_box = array.get(ob_box, i)
            if not na(old_box)
                box.delete(old_box)
            array.remove(ob_top,   i)
            array.remove(ob_bot,   i)
            array.remove(ob_dir,   i)
            array.remove(ob_bar,   i)
            array.remove(ob_mitig, i)
            array.remove(ob_box,   i)
        // OB bajista invalidado → precio cierra SOBRE el OB → se convierte en bullish breaker
        else if ob_d == -1 and close > ob_t
            prune_struct(brk_top, brk_bot, brk_dir, brk_bar, brk_mitig, brk_box, brk_max)
            new_box = box.new(ob_br, ob_b, bar_index + 5, ob_t,
                              border_color=C_BREAKER_BULL, border_width=2,
                              bgcolor=color.new(C_BREAKER_BULL, i_opacity))
            array.push(brk_top,   ob_t)
            array.push(brk_bot,   ob_b)
            array.push(brk_dir,   1)
            array.push(brk_bar,   ob_br)
            array.push(brk_mitig, false)
            array.push(brk_box,   new_box)
            old_box = array.get(ob_box, i)
            if not na(old_box)
                box.delete(old_box)
            array.remove(ob_top,   i)
            array.remove(ob_bot,   i)
            array.remove(ob_dir,   i)
            array.remove(ob_bar,   i)
            array.remove(ob_mitig, i)
            array.remove(ob_box,   i)
        else
            i += 1

// ============================================================
// DETECCIÓN BPR (Balanced Price Range)
// ============================================================
if barstate.isconfirmed and i_bpr_enabled
    n_fvg = array.size(fvg_top)
    if n_fvg >= 2
        last_bull_idx = -1
        last_bear_idx = -1
        for k = n_fvg - 1 to 0
            d = array.get(fvg_dir, k)
            if d == 1 and last_bull_idx < 0
                last_bull_idx := k
            else if d == -1 and last_bear_idx < 0
                last_bear_idx := k
            if last_bull_idx >= 0 and last_bear_idx >= 0
                break
        if last_bull_idx >= 0 and last_bear_idx >= 0
            bar_bull = array.get(fvg_bar, last_bull_idx)
            bar_bear = array.get(fvg_bar, last_bear_idx)
            bar_diff = math.abs(bar_bull - bar_bear)
            if bar_diff <= 20
                // Verificar solapamiento
                bull_top = array.get(fvg_top, last_bull_idx)
                bull_bot = array.get(fvg_bot, last_bull_idx)
                bear_top = array.get(fvg_top, last_bear_idx)
                bear_bot = array.get(fvg_bot, last_bear_idx)
                overlap_top = math.min(bull_top, bear_top)
                overlap_bot = math.max(bull_bot, bear_bot)
                if overlap_top > overlap_bot
                    // Zona de solapamiento válida — verificar no duplicado
                    already_bpr = false
                    if array.size(bpr_top) > 0
                        for m = 0 to array.size(bpr_top) - 1
                            if math.abs(array.get(bpr_top, m) - overlap_top) < atr14 * 0.2
                                already_bpr := true
                                break
                    if not already_bpr
                        prune_struct(bpr_top, bpr_bot, bpr_dir, bpr_bar, bpr_mitig, bpr_box, bpr_max)
                        new_box = box.new(math.min(bar_bull, bar_bear), overlap_bot,
                                          bar_index + 5, overlap_top,
                                          border_color=C_BPR, border_width=2,
                                          bgcolor=color.new(C_BPR, i_opacity))
                        array.push(bpr_top,   overlap_top)
                        array.push(bpr_bot,   overlap_bot)
                        array.push(bpr_dir,   0)
                        array.push(bpr_bar,   bar_index)
                        array.push(bpr_mitig, false)
                        array.push(bpr_box,   new_box)

// ============================================================
// DETECCIÓN LIQUIDEZ (BSL / SSL via pivots)
// ============================================================
ph = ta.pivothigh(high, i_swing_len, i_swing_len)
pl = ta.pivotlow(low,   i_swing_len, i_swing_len)

if barstate.isconfirmed
    if not na(ph)
        already_liq = false
        if array.size(liq_top) > 0
            for m = 0 to array.size(liq_top) - 1
                if math.abs(array.get(liq_top, m) - ph) < atr14 * 0.3
                    already_liq := true
                    break
        if not already_liq
            prune_struct(liq_top, liq_bot, liq_dir, liq_bar, liq_mitig, liq_box, i_max_per_type)
            new_box = box.new(bar_index[i_swing_len], ph - atr14 * 0.05,
                              bar_index + 5, ph + atr14 * 0.05,
                              border_color=C_LIQ_HIGH, border_width=1,
                              bgcolor=color.new(C_LIQ_HIGH, 85))
            array.push(liq_top,   ph + atr14 * 0.05)
            array.push(liq_bot,   ph - atr14 * 0.05)
            array.push(liq_dir,   1)
            array.push(liq_bar,   bar_index[i_swing_len])
            array.push(liq_mitig, false)
            array.push(liq_box,   new_box)

    if not na(pl)
        already_liq = false
        if array.size(liq_top) > 0
            for m = 0 to array.size(liq_top) - 1
                if math.abs(array.get(liq_bot, m) - pl) < atr14 * 0.3
                    already_liq := true
                    break
        if not already_liq
            prune_struct(liq_top, liq_bot, liq_dir, liq_bar, liq_mitig, liq_box, i_max_per_type)
            new_box = box.new(bar_index[i_swing_len], pl - atr14 * 0.05,
                              bar_index + 5, pl + atr14 * 0.05,
                              border_color=C_LIQ_LOW, border_width=1,
                              bgcolor=color.new(C_LIQ_LOW, 85))
            array.push(liq_top,   pl + atr14 * 0.05)
            array.push(liq_bot,   pl - atr14 * 0.05)
            array.push(liq_dir,   -1)
            array.push(liq_bar,   bar_index[i_swing_len])
            array.push(liq_mitig, false)
            array.push(liq_box,   new_box)

// ============================================================
// CHECK MITIGACIÓN — actualizar estado de todas las estructuras
// ============================================================
mitig_event := false

if barstate.isconfirmed
    // FVG
    if array.size(fvg_top) > 0
        for i = 0 to array.size(fvg_top) - 1
            if not array.get(fvg_mitig, i)
                if is_mitigated(array.get(fvg_top, i), array.get(fvg_bot, i))
                    array.set(fvg_mitig, i, true)
                    b = array.get(fvg_box, i)
                    if not na(b)
                        box.set_bgcolor(b, color.new(color.gray, 90))
                        box.set_border_color(b, color.new(color.gray, 50))
                    mitig_event := true
                    mitig_type  := "FVG"
                    mitig_price := (array.get(fvg_top, i) + array.get(fvg_bot, i)) / 2.0

    // OB
    if array.size(ob_top) > 0
        for i = 0 to array.size(ob_top) - 1
            if not array.get(ob_mitig, i)
                if is_mitigated(array.get(ob_top, i), array.get(ob_bot, i))
                    array.set(ob_mitig, i, true)
                    b = array.get(ob_box, i)
                    if not na(b)
                        box.set_bgcolor(b, color.new(color.gray, 90))
                        box.set_border_color(b, color.new(color.gray, 50))
                    mitig_event := true
                    mitig_type  := "OB"
                    mitig_price := (array.get(ob_top, i) + array.get(ob_bot, i)) / 2.0

    // Breaker
    if array.size(brk_top) > 0
        for i = 0 to array.size(brk_top) - 1
            if not array.get(brk_mitig, i)
                if is_mitigated(array.get(brk_top, i), array.get(brk_bot, i))
                    array.set(brk_mitig, i, true)
                    b = array.get(brk_box, i)
                    if not na(b)
                        box.set_bgcolor(b, color.new(color.gray, 90))
                        box.set_border_color(b, color.new(color.gray, 50))
                    mitig_event := true
                    mitig_type  := "BRK"
                    mitig_price := (array.get(brk_top, i) + array.get(brk_bot, i)) / 2.0

    // BPR
    if array.size(bpr_top) > 0
        for i = 0 to array.size(bpr_top) - 1
            if not array.get(bpr_mitig, i)
                if is_mitigated(array.get(bpr_top, i), array.get(bpr_bot, i))
                    array.set(bpr_mitig, i, true)
                    b = array.get(bpr_box, i)
                    if not na(b)
                        box.set_bgcolor(b, color.new(color.gray, 90))
                        box.set_border_color(b, color.new(color.gray, 50))
                    mitig_event := true
                    mitig_type  := "BPR"
                    mitig_price := (array.get(bpr_top, i) + array.get(bpr_bot, i)) / 2.0

    // Liquidez — sweep check
    if array.size(liq_top) > 0
        for i = 0 to array.size(liq_top) - 1
            if not array.get(liq_mitig, i)
                liq_d = array.get(liq_dir, i)
                liq_t = array.get(liq_top, i)
                liq_b = array.get(liq_bot, i)
                swept = false
                if liq_d == 1 and high > liq_t + atr14 * 0.1
                    swept := true
                else if liq_d == -1 and low < liq_b - atr14 * 0.1
                    swept := true
                if swept
                    array.set(liq_mitig, i, true)
                    b = array.get(liq_box, i)
                    if not na(b)
                        box.set_bgcolor(b, color.new(color.gray, 90))
                        box.set_border_color(b, color.new(color.gray, 50))
                    mitig_event := true
                    mitig_type  := "LIQ"
                    mitig_price := (liq_t + liq_b) / 2.0

// ============================================================
// ACTUALIZAR box.set_right EN TODAS LAS ESTRUCTURAS ACTIVAS
// ============================================================
if (barstate.islast or barstate.isrealtime) and i_show_onchart
    if array.size(fvg_box) > 0
        for i = 0 to array.size(fvg_box) - 1
            b = array.get(fvg_box, i)
            if not na(b)
                box.set_right(b, bar_index + 5)
    if array.size(ob_box) > 0
        for i = 0 to array.size(ob_box) - 1
            b = array.get(ob_box, i)
            if not na(b)
                box.set_right(b, bar_index + 5)
    if array.size(brk_box) > 0
        for i = 0 to array.size(brk_box) - 1
            b = array.get(brk_box, i)
            if not na(b)
                box.set_right(b, bar_index + 5)
    if array.size(bpr_box) > 0
        for i = 0 to array.size(bpr_box) - 1
            b = array.get(bpr_box, i)
            if not na(b)
                box.set_right(b, bar_index + 5)
    if array.size(liq_box) > 0
        for i = 0 to array.size(liq_box) - 1
            b = array.get(liq_box, i)
            if not na(b)
                box.set_right(b, bar_index + 5)

// ============================================================
// CÁLCULO DEBT SCORE — fórmula ICT ponderada con distancia y edad
// ============================================================
debt_long_raw  = 0.0
debt_short_raw = 0.0

// Helper interno para acumular el score de una zona
calc_contrib(top_val, bot_val, dir_val, bar_val, weight, is_mitig) =>
    contrib_l = 0.0
    contrib_s = 0.0
    if not is_mitig
        mid      = (top_val + bot_val) / 2.0
        dist     = math.abs(close - mid) / math.max(atr14, 0.0001)
        if dist <= i_dist_cap_atr
            dist_factor = math.max(0.5, math.min(2.0, 1.0 / (1.0 + dist * 0.1)))
            age         = bar_index - bar_val
            age_factor  = math.exp(-float(age) / float(i_age_halflife))
            contrib     = weight * dist_factor * age_factor
            // dir_val: 1 = alcista (presiona deuda SHORT si está debajo, debería ser LONG driver)
            // Lógica: estructura alcista DEBAJO del precio → deuda SHORT (el precio la debe regresar)
            // Estructura bajista ENCIMA del precio → deuda LONG (el precio la debe cubrir)
            if dir_val == 1 and mid < close
                contrib_s := contrib
            else if dir_val == -1 and mid > close
                contrib_l := contrib
            else if dir_val == 0
                // BPR: cuenta en ambos lados según posición
                if mid > close
                    contrib_l := contrib * 0.5
                else
                    contrib_s := contrib * 0.5
    [contrib_l, contrib_s]

// Helper top-level para contar zonas activas en cada fila Tetris
count_cells_into(tops, bots, mitigs, target, row_min_val) =>
    if array.size(tops) > 0
        for i = 0 to array.size(tops) - 1
            if not array.get(mitigs, i)
                t   = array.get(tops, i)
                b   = array.get(bots, i)
                mid = (t + b) / 2.0
                row_idx = int(math.floor((mid - row_min_val) / (atr14 * 0.5)))
                if row_idx >= 0 and row_idx < 20
                    array.set(target, row_idx, array.get(target, row_idx) + 1)

// Acumular FVG
if array.size(fvg_top) > 0
    for i = 0 to array.size(fvg_top) - 1
        [cl, cs] = calc_contrib(array.get(fvg_top, i), array.get(fvg_bot, i),
                                array.get(fvg_dir, i), array.get(fvg_bar, i),
                                i_w_fvg, array.get(fvg_mitig, i))
        debt_long_raw  += cl
        debt_short_raw += cs

// Acumular OB
if array.size(ob_top) > 0
    for i = 0 to array.size(ob_top) - 1
        [cl, cs] = calc_contrib(array.get(ob_top, i), array.get(ob_bot, i),
                                array.get(ob_dir, i), array.get(ob_bar, i),
                                i_w_ob, array.get(ob_mitig, i))
        debt_long_raw  += cl
        debt_short_raw += cs

// Acumular Breaker
if array.size(brk_top) > 0
    for i = 0 to array.size(brk_top) - 1
        [cl, cs] = calc_contrib(array.get(brk_top, i), array.get(brk_bot, i),
                                array.get(brk_dir, i), array.get(brk_bar, i),
                                i_w_breaker, array.get(brk_mitig, i))
        debt_long_raw  += cl
        debt_short_raw += cs

// Acumular BPR
if array.size(bpr_top) > 0
    for i = 0 to array.size(bpr_top) - 1
        [cl, cs] = calc_contrib(array.get(bpr_top, i), array.get(bpr_bot, i),
                                array.get(bpr_dir, i), array.get(bpr_bar, i),
                                i_w_bpr, array.get(bpr_mitig, i))
        debt_long_raw  += cl
        debt_short_raw += cs

// Acumular Liquidez
// BSL (dir=1) encima = deuda LONG; SSL (dir=-1) debajo = deuda SHORT
if array.size(liq_top) > 0
    for i = 0 to array.size(liq_top) - 1
        liq_d   = array.get(liq_dir, i)
        liq_mid = (array.get(liq_top, i) + array.get(liq_bot, i)) / 2.0
        if not array.get(liq_mitig, i)
            dist = math.abs(close - liq_mid) / math.max(atr14, 0.0001)
            if dist <= i_dist_cap_atr
                dist_factor = math.max(0.5, math.min(2.0, 1.0 / (1.0 + dist * 0.1)))
                age_factor  = math.exp(-float(bar_index - array.get(liq_bar, i)) / float(i_age_halflife))
                contrib     = i_w_liq * dist_factor * age_factor
                // BSL encima bloquea longs (el precio lo irá a buscar pero hay riesgo de reversión)
                if liq_d == 1 and liq_mid > close
                    debt_long_raw += contrib
                // SSL debajo bloquea shorts
                else if liq_d == -1 and liq_mid < close
                    debt_short_raw += contrib

// Normalizar (cap empírico 120)
debt_long  = math.min(100.0, debt_long_raw  / 120.0 * 100.0)
debt_short = math.min(100.0, debt_short_raw / 120.0 * 100.0)

// ============================================================
// SEÑAL FINAL
// ============================================================
var string signal_long  = "GREEN"
var string signal_short = "GREEN"
signal_long  := debt_long  >= i_thresh_blocked ? "BLOCKED" : debt_long  >= i_thresh_caution ? "CAUTION" : "GREEN"
signal_short := debt_short >= i_thresh_blocked ? "BLOCKED" : debt_short >= i_thresh_caution ? "CAUTION" : "GREEN"

// ============================================================
// GRILLA HORIZONTAL — actualizar posiciones cada barra
// ============================================================
if i_show_grid and array.size(grid_lines) > 0
    for k = 0 to 19
        y_level = close + (k - 10) * atr14 * 0.5
        l = array.get(grid_lines, k)
        line.set_x1(l, bar_index - 50)
        line.set_x2(l, bar_index + 50)
        line.set_y1(l, y_level)
        line.set_y2(l, y_level)

// ============================================================
// BUSCAR ESTRUCTURA MITIGACIÓN MÁS CERCANA (para HUD + Next-piece)
// Pine v6: funciones no pueden mutar vars del scope padre vía :=,
// usamos arrays de 1 elemento (paso por referencia).
// ============================================================
var array<float>  nearest_dist_arr  = array.from(999999.0)
var array<float>  nearest_price_arr = array.from(float(na))
var array<string> nearest_name_arr  = array.from("---")
var array<color>  nearest_color_arr = array.from(C_TEXT)
var array<string> nearest_type_arr  = array.from("")

// Reset cada barra
array.set(nearest_dist_arr,  0, 999999.0)
array.set(nearest_price_arr, 0, na)
array.set(nearest_name_arr,  0, "---")
array.set(nearest_color_arr, 0, C_TEXT)
array.set(nearest_type_arr,  0, "")

check_nearest(tops, bots, mitigs, type_name, piece_color) =>
    if array.size(tops) > 0
        for i = 0 to array.size(tops) - 1
            if not array.get(mitigs, i)
                mid  = (array.get(tops, i) + array.get(bots, i)) / 2.0
                dist = math.abs(close - mid)
                if dist < array.get(nearest_dist_arr, 0)
                    array.set(nearest_dist_arr,  0, dist)
                    array.set(nearest_price_arr, 0, mid)
                    array.set(nearest_name_arr,  0, type_name)
                    array.set(nearest_color_arr, 0, piece_color)
                    array.set(nearest_type_arr,  0, type_name)

check_nearest(fvg_top, fvg_bot, fvg_mitig, "FVG",     C_FVG_BULL)
check_nearest(ob_top,  ob_bot,  ob_mitig,  "OB",      C_OB_BULL)
check_nearest(brk_top, brk_bot, brk_mitig, "BREAKER", C_BREAKER_BULL)
check_nearest(bpr_top, bpr_bot, bpr_mitig, "BPR",     C_BPR)
check_nearest(liq_top, liq_bot, liq_mitig, "LIQ",     C_LIQ_HIGH)

// Aliases locales para el resto del código
nearest_dist  = array.get(nearest_dist_arr,  0)
nearest_price = array.get(nearest_price_arr, 0)
nearest_name  = array.get(nearest_name_arr,  0)
nearest_color = array.get(nearest_color_arr, 0)
nearest_type  = array.get(nearest_type_arr,  0)

// ============================================================
// DETECCIÓN "LÍNEA COMPLETA" TETRIS — solo en barras confirmadas
// ============================================================
if barstate.isconfirmed
    row_min = close - 10.0 * atr14 * 0.5
    cells_per_row = array.new<int>(20, 0)

    count_cells_into(fvg_top, fvg_bot, fvg_mitig, cells_per_row, row_min)
    count_cells_into(ob_top,  ob_bot,  ob_mitig,  cells_per_row, row_min)
    count_cells_into(brk_top, brk_bot, brk_mitig, cells_per_row, row_min)
    count_cells_into(bpr_top, bpr_bot, bpr_mitig, cells_per_row, row_min)
    count_cells_into(liq_top, liq_bot, liq_mitig, cells_per_row, row_min)

    for row_idx = 0 to 19
        if array.get(cells_per_row, row_idx) >= 7
            lines_cleared += 1
            blink_until   := bar_index + 3

// ============================================================
// RENDER GAME BOARD LATERAL — solo en última barra (rendimiento)
// ============================================================
if barstate.islast and i_show_board and not na(tbl_board)
    row_min_board = close - 10.0 * atr14 * 0.5

    // Header
    table.cell(tbl_board, 0, 0, "TETRIS-ICT",
               text_color=C_TEXT, text_size=size.small,
               bgcolor=color.new(C_BG, 20), text_halign=text.align_center,
               width=board_cell_w, height=board_cell_h)
    for c = 1 to 11
        table.merge_cells(tbl_board, 0, 0, 11, 0)
        break

    // Filas 1-20: celdas de precio
    for row_idx = 0 to 19
        row_price = row_min_board + (19 - row_idx) * atr14 * 0.5
        price_str = str.tostring(math.round(row_price, 2))

        // Columna precio (col 0)
        table.cell(tbl_board, 0, row_idx + 1, price_str,
                   text_color=color.new(C_TEXT, 40), text_size=size.tiny,
                   bgcolor=C_BG, text_halign=text.align_right,
                   width=board_cell_w * 1.5, height=board_cell_h)

        // Columna leyenda (col 11)
        table.cell(tbl_board, 11, row_idx + 1, "",
                   bgcolor=C_BG, text_size=size.tiny,
                   width=board_cell_w * 0.3, height=board_cell_h)

        // Celdas de juego (cols 1-10)
        cell_color  = C_BG
        cell_text   = ""
        cell_tcolor = C_BG
        has_struct  = false

        // Verificar qué estructura ocupa esta fila
        if array.size(fvg_top) > 0
            for i = 0 to array.size(fvg_top) - 1
                if not array.get(fvg_mitig, i)
                    t = array.get(fvg_top, i)
                    b = array.get(fvg_bot, i)
                    if row_price <= t and row_price >= b
                        cell_color := array.get(fvg_dir, i) == 1 ? color.new(C_FVG_BULL, 50) : color.new(C_FVG_BEAR, 50)
                        cell_text  := "I"
                        cell_tcolor := color.white
                        has_struct := true
                        break

        if not has_struct and array.size(ob_top) > 0
            for i = 0 to array.size(ob_top) - 1
                if not array.get(ob_mitig, i)
                    t = array.get(ob_top, i)
                    b = array.get(ob_bot, i)
                    if row_price <= t and row_price >= b
                        cell_color := array.get(ob_dir, i) == 1 ? color.new(C_OB_BULL, 50) : color.new(C_OB_BEAR, 50)
                        cell_text  := "O"
                        cell_tcolor := color.white
                        has_struct := true
                        break

        if not has_struct and array.size(brk_top) > 0
            for i = 0 to array.size(brk_top) - 1
                if not array.get(brk_mitig, i)
                    t = array.get(brk_top, i)
                    b = array.get(brk_bot, i)
                    if row_price <= t and row_price >= b
                        cell_color := array.get(brk_dir, i) == 1 ? color.new(C_BREAKER_BULL, 50) : color.new(C_BREAKER_BEAR, 50)
                        cell_text  := "L"
                        cell_tcolor := color.white
                        has_struct := true
                        break

        if not has_struct and array.size(bpr_top) > 0
            for i = 0 to array.size(bpr_top) - 1
                if not array.get(bpr_mitig, i)
                    t = array.get(bpr_top, i)
                    b = array.get(bpr_bot, i)
                    if row_price <= t and row_price >= b
                        cell_color := color.new(C_BPR, 50)
                        cell_text  := "S"
                        cell_tcolor := color.white
                        has_struct := true
                        break

        if not has_struct and array.size(liq_top) > 0
            for i = 0 to array.size(liq_top) - 1
                if not array.get(liq_mitig, i)
                    t = array.get(liq_top, i)
                    b = array.get(liq_bot, i)
                    if row_price <= t and row_price >= b
                        cell_color := array.get(liq_dir, i) == 1 ? color.new(C_LIQ_HIGH, 50) : color.new(C_LIQ_LOW, 50)
                        cell_text  := "T"
                        cell_tcolor := color.white
                        has_struct := true
                        break

        // Efecto blink en líneas completas
        if blink_until >= bar_index and has_struct
            cell_color := bar_index % 2 == 0 ? color.white : color.new(color.white, 60)

        // Render celdas 1-10 con la misma info
        for col_idx = 1 to 10
            table.cell(tbl_board, col_idx, row_idx + 1, col_idx == 5 ? cell_text : "",
                       text_color=cell_tcolor, text_size=size.tiny,
                       bgcolor=has_struct ? cell_color : color.new(C_GRID, 80),
                       text_halign=text.align_center,
                       width=board_cell_w, height=board_cell_h)

    // Footer
    table.cell(tbl_board, 0, 21,
               "LINES: " + str.tostring(lines_cleared) + "  |  bolivarbolsa.com",
               text_color=C_TEXT, text_size=size.tiny,
               bgcolor=color.new(C_BG, 20), text_halign=text.align_center,
               width=board_cell_w, height=board_cell_h)
    for c = 1 to 11
        table.merge_cells(tbl_board, 0, 21, 11, 21)
        break

// ============================================================
// RENDER HUD — top-right
// ============================================================
if barstate.islast and i_show_hud and not na(tbl_hud)
    // Row 0: Título
    table.cell(tbl_hud, 0, 0, "TETRIS-ICT",
               text_color=C_TEXT, text_size=size.normal,
               bgcolor=color.new(C_BG, 10), text_halign=text.align_center)
    table.cell(tbl_hud, 1, 0, "bolivarbolsa.com",
               text_color=color.new(C_TEXT, 40), text_size=size.tiny,
               bgcolor=color.new(C_BG, 10), text_halign=text.align_center)

    // Row 1: Debt Long
    dl_str = str.tostring(math.round(debt_long)) + "%  " + score_bar(debt_long)
    table.cell(tbl_hud, 0, 1, "DEBT ↑ LONG",
               text_color=color.new(C_TEXT, 20), text_size=size.small,
               bgcolor=C_BG, text_halign=text.align_left)
    table.cell(tbl_hud, 1, 1, dl_str,
               text_color=sig_color(signal_long), text_size=size.small,
               bgcolor=C_BG, text_halign=text.align_right)

    // Row 2: Debt Short
    ds_str = str.tostring(math.round(debt_short)) + "%  " + score_bar(debt_short)
    table.cell(tbl_hud, 0, 2, "DEBT ↓ SHORT",
               text_color=color.new(C_TEXT, 20), text_size=size.small,
               bgcolor=C_BG, text_halign=text.align_left)
    table.cell(tbl_hud, 1, 2, ds_str,
               text_color=sig_color(signal_short), text_size=size.small,
               bgcolor=C_BG, text_halign=text.align_right)

    // Row 3: Signal Long / Short
    blink_blocked_l = signal_long  == "BLOCKED" and bar_index % 2 == 0
    blink_blocked_s = signal_short == "BLOCKED" and bar_index % 2 == 0
    sig_l_bg = blink_blocked_l ? C_BLOCKED : sig_color(signal_long)
    sig_s_bg = blink_blocked_s ? C_BLOCKED : sig_color(signal_short)
    table.cell(tbl_hud, 0, 3, "SIG ↑  " + signal_long,
               text_color=color.black, text_size=size.small,
               bgcolor=color.new(sig_l_bg, 20), text_halign=text.align_center)
    table.cell(tbl_hud, 1, 3, "SIG ↓  " + signal_short,
               text_color=color.black, text_size=size.small,
               bgcolor=color.new(sig_s_bg, 20), text_halign=text.align_center)

    // Row 4: Next mitigación
    next_str = na(nearest_price) ? "---" : nearest_name + " @ " + str.tostring(math.round(nearest_price, 2))
    table.cell(tbl_hud, 0, 4, "NEXT MITIG",
               text_color=color.new(C_TEXT, 20), text_size=size.small,
               bgcolor=C_BG, text_halign=text.align_left)
    table.cell(tbl_hud, 1, 4, next_str,
               text_color=na(nearest_price) ? color.new(C_TEXT, 50) : nearest_color,
               text_size=size.small, bgcolor=C_BG, text_halign=text.align_right)

    // Row 5: Lines cleared
    table.cell(tbl_hud, 0, 5, "LINES CLEARED",
               text_color=color.new(C_TEXT, 20), text_size=size.small,
               bgcolor=C_BG, text_halign=text.align_left)
    table.cell(tbl_hud, 1, 5, str.tostring(lines_cleared),
               text_color=C_TEXT, text_size=size.normal,
               bgcolor=C_BG, text_halign=text.align_right)

// ============================================================
// RENDER NEXT-PIECE PREVIEW — top-left
// ============================================================
// Formas 4×4 para cada tipo de estructura
get_piece_shape(type_str) =>
    // Devuelve array de 16 ints (4x4), 1=celda activa
    shape = array.new<int>(16, 0)
    if type_str == "FVG"
        // I-piece: fila completa
        array.set(shape, 0, 1)
        array.set(shape, 1, 1)
        array.set(shape, 2, 1)
        array.set(shape, 3, 1)
    else if type_str == "OB"
        // O-piece: cuadrado 2x2
        array.set(shape, 0, 1)
        array.set(shape, 1, 1)
        array.set(shape, 4, 1)
        array.set(shape, 5, 1)
    else if type_str == "BREAKER"
        // L-piece: columna + esquina
        array.set(shape, 0, 1)
        array.set(shape, 4, 1)
        array.set(shape, 8, 1)
        array.set(shape, 9, 1)
    else if type_str == "BPR"
        // S-piece
        array.set(shape, 1, 1)
        array.set(shape, 2, 1)
        array.set(shape, 4, 1)
        array.set(shape, 5, 1)
    else if type_str == "LIQ"
        // T-piece
        array.set(shape, 0, 1)
        array.set(shape, 1, 1)
        array.set(shape, 2, 1)
        array.set(shape, 5, 1)
    shape

if barstate.islast and i_show_next and not na(tbl_next)
    // Row 0: Header
    table.cell(tbl_next, 0, 0, "NEXT", text_color=C_LIQ_HIGH,
               text_size=size.small, bgcolor=color.new(C_BG, 10),
               text_halign=text.align_center)
    for c = 1 to 5
        table.merge_cells(tbl_next, 0, 0, 5, 0)
        break

    // Rows 1-4: grilla 4×4 de la pieza
    piece_shape = get_piece_shape(nearest_name)
    piece_col   = nearest_color

    for pr = 0 to 3
        for pc = 0 to 3
            cell_val = array.get(piece_shape, pr * 4 + pc)
            table.cell(tbl_next, pc + 1, pr + 1, "",
                       bgcolor=cell_val == 1 ? color.new(piece_col, 30) : color.new(C_BG, 80),
                       text_size=size.tiny)

    // Columna 0 (margen izq) y col 5 (margen der) — vacías
    for pr = 1 to 4
        table.cell(tbl_next, 0, pr, "", bgcolor=C_BG)
        table.cell(tbl_next, 5, pr, "", bgcolor=C_BG)

    // Row 5: Nombre estructura
    table.cell(tbl_next, 0, 5, nearest_name,
               text_color=nearest_color, text_size=size.small,
               bgcolor=C_BG, text_halign=text.align_center)
    for c = 1 to 5
        table.merge_cells(tbl_next, 0, 5, 5, 5)
        break

    // Row 6: Precio
    price_str_next = na(nearest_price) ? "---" : str.tostring(math.round(nearest_price, 2))
    table.cell(tbl_next, 0, 6, price_str_next,
               text_color=color.new(nearest_color, 20), text_size=size.small,
               bgcolor=C_BG, text_halign=text.align_center)
    for c = 1 to 5
        table.merge_cells(tbl_next, 0, 6, 5, 6)
        break

    // Row 7: vacía
    table.cell(tbl_next, 0, 7, "", bgcolor=C_BG)
    for c = 1 to 5
        table.merge_cells(tbl_next, 0, 7, 5, 7)
        break

// ============================================================
// PLOTS SILENCIOSOS — para alertas JSON vía {{plot()}}
// ============================================================
plot(debt_long,     "Debt Long",     display=display.none)
plot(debt_short,    "Debt Short",    display=display.none)
plot(lines_cleared, "Lines Cleared", display=display.none)

// ============================================================
// ALERTCONDITIONS — 5 condiciones de alerta
// ============================================================

// 1. Blocked Long — transición a BLOCKED
alertcondition(
     signal_long == "BLOCKED" and signal_long[1] != "BLOCKED",
     title="Blocked Long",
     message='{"event":"TETRIS_BLOCKED_LONG","pair":"{{ticker}}","tf":"{{interval}}","debt":{{plot("Debt Long")}},"source":"bolivarbolsa"}')

// 2. Green Long — transición a GREEN
alertcondition(
     signal_long == "GREEN" and signal_long[1] != "GREEN",
     title="Green Long",
     message='{"event":"TETRIS_GREEN_LONG","pair":"{{ticker}}","tf":"{{interval}}","debt":{{plot("Debt Long")}},"source":"bolivarbolsa"}')

// 3. Green Short — transición a GREEN (short)
alertcondition(
     signal_short == "GREEN" and signal_short[1] != "GREEN",
     title="Green Short",
     message='{"event":"TETRIS_GREEN_SHORT","pair":"{{ticker}}","tf":"{{interval}}","debt":{{plot("Debt Short")}},"source":"bolivarbolsa"}')

// 4. Mitigación detectada
alertcondition(
     mitig_event,
     title="Mitigación ICT",
     message='{"event":"TETRIS_MITIGATED","pair":"{{ticker}}","tf":"{{interval}}","source":"bolivarbolsa"}')

// 5. Línea completa — Level Up
alertcondition(
     lines_cleared > lines_cleared[1],
     title="Line Cleared (Level Up)",
     message='{"event":"TETRIS_LEVEL_UP","pair":"{{ticker}}","tf":"{{interval}}","lines":{{plot("Lines Cleared")}},"source":"bolivarbolsa"}')

Frequently asked questions

Is it free?

Yes, completely free. No subscription or payment needed. You only need a TradingView account (the free plan is enough).

Which timeframes work best?

Any timeframe. The Bolívar Bolsa ICT/SMC methodology uses H4 for context and M15/M5 for detailed structure.

Does it work on XAUUSD and Forex?

Yes. It detects structural imbalances on any TradingView instrument: XAUUSD, currency pairs, indices and crypto. ICT logic is universal.

Do I need a paid TradingView account?

No. The free plan lets you run Pine Script v6 indicators without restriction.

Exclusively educational tool for studying ICT/SMC concepts; does not constitute financial advice and does not guarantee results. Trading involves risk of capital loss.

Learn the full methodology. Tetris ICT is a visual entry point, not the complete system. Understand why price respects each structural debt in the free Trading School, or join the Telegram community.