16 agentes en líneaXAUUSD 4 552,4Sesión: London KillzonePróximo análisis: 04:12
Indicadores · Pine Script v6 · Gratis
● Gratis · TradingView

Tetris ICT [Bolívar]: tu indicador ICT para TradingView

El mercado guarda memoria. Cada Fair Value Gap, cada Order Block no mitigado es una deuda estructural que el precio terminará por saldar. Tetris ICT [Bolívar] convierte esas deudas en piezas de Tetris en pantalla, para que aprendas a leer el orden oculto del mercado.

Creado por Xavier Mas, fundador de Bolívar Bolsa, para más de 42.000 estudiantes de ICT/SMC en LATAM. Gratuito, sin restricciones, escrito en Pine Script v6.

Añadir en TradingViewVer el códigoDescargar .pine

¿Qué hace?

  • Fair Value Gap → pieza I. Detecta desequilibrios de 3 velas y los marca como zonas pendientes de relleno.
  • Order Block → pieza O. Identifica la última vela opuesta antes de un movimiento expansivo: el origen institucional del impulso.
  • Balanced Price Range (BPR) → pieza S. Mapea la superposición de dos FVG opuestos, zona de máxima eficiencia de precio.
  • Breaker Block → pieza L. Detecta Order Blocks que han fallado y se han convertido en estructura inversa.
  • Zonas de liquidez → pieza T. Marca acumulaciones de stops (iguales altos/bajos) donde el precio tiende a barrer antes de revertir.
  • HUD lateral tipo arcade. Agrupa las piezas activas (no mitigadas) con un contador de deuda estructural en tiempo real.

Cómo se lee el score

El HUD calcula cuántas zonas no mitigadas quedan activas sobre el precio y muestra uno de tres estados:

ScoreEstadoLectura educativa
≥ 65BLOCKEDAlta densidad de deuda estructural. El precio enfrenta zonas pendientes en múltiples planos.
30 – 64CAUTIONDensidad moderada. Algunas zonas sin mitigar pueden influir en el recorrido.
< 30GREENTablero limpio. Pocas deudas activas; el precio se mueve en espacio abierto.

La siguiente pieza (next-piece preview) anticipa qué tipo de zona aparece más cerca del precio. Cada pieza desaparece del tablero cuando el precio la mitiga, igual que una fila completa en Tetris.

Cómo instalarlo en TradingView

  1. Abre el Pine Editor. En TradingView, pestaña Pine Editor (parte inferior).
  2. Pega el código. Copia el código completo de abajo y reemplaza el contenido del editor.
  3. Agrégalo al gráfico. Botón «Add to chart» (arriba a la derecha del editor).
  4. Guárdalo. Pulsa Ctrl/Cmd + S y ponle un nombre. Quedará en tu biblioteca de TradingView.

Código Pine Script v6

Tetris-ICT [Bolívar] · Pine v6 · 1137 líneas · Gratis.

Descargar .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"}')

Preguntas frecuentes

¿Es gratis?

Sí, completamente gratis. Sin suscripción ni pago. Solo necesitas una cuenta TradingView (el plan gratuito basta).

¿En qué temporalidades funciona mejor?

En cualquier temporalidad. En la metodología ICT/SMC de Bolívar Bolsa se trabaja H4 para contexto y M15/M5 para estructura de detalle.

¿Funciona en XAUUSD y Forex?

Sí. Detecta deudas estructurales en cualquier instrumento de TradingView: XAUUSD, divisas, índices y criptomonedas. La lógica ICT es universal.

¿Necesito una cuenta de pago en TradingView?

No. El plan gratuito permite ejecutar indicadores en Pine Script v6 sin restricción de uso.

Herramienta exclusivamente educativa para el estudio de conceptos ICT/SMC; no constituye asesoramiento financiero ni garantiza resultados. El trading implica riesgo de pérdida de capital.

Aprende la metodología completa. Tetris ICT es un punto de entrada visual, no el sistema completo. Entiende por qué el precio respeta cada deuda estructural en la Escuela de Trading gratuita, o únete a la comunidad en Telegram.