SMC Liquidity Sniper — Acciones

Estrategia para TradingView. No se instala nada: se copia y se pega.

  1. Pulsa Copiar el código aquí abajo.
  2. Abre TradingView y entra en cualquier gráfica.
  3. Abajo del todo, pestaña Pine Editor.
  4. Borra lo que haya, pega el código (Cmd+V o Ctrl+V) y pulsa Save.
  5. Pulsa Add to chart. Ya está.
Descargarlo como archivo de texto
Esto opera acciones con tu propio bróker, no con Topstep. Y una acción abre con hueco: si abre por debajo de tu stop, la orden se ejecuta en la apertura y no en tu nivel, así que se puede perder más del tope. Eso no lo evita ningún stop.

Vista previa del código:

// This work is licensed under a Attribution-NonCommercial-ShareAlike 4.0 International (CC BY-NC-SA 4.0)
// https://creativecommons.org/licenses/by-nc-sa/4.0/
// Based on / derived from: "Smart Money Concepts [LuxAlgo]" and "Liquidity Swings [LuxAlgo]" © LuxAlgo
//
// =====================================================================================================================
// SMC LIQUIDITY SNIPER — VERSIÓN ACCIONES
// Motor de entradas: barrido de liquidez, BOS/CHoCH, medio cuerpo sobre la línea, vela dominante, Liquidez+FVG.
// Ese motor es el mismo que la versión de futuros y no se ha tocado: es acción del precio y no sabe qué instrumento
// está mirando. Lo que cambia por completo es la capa de dinero.
// =====================================================================================================================
//
// A1) TAMAÑO POR RIESGO FIJO — el cambio de fondo
//     En futuros el tamaño era fijo ($20/punto × contratos) y para que la pérdida cupiera en el tope había que
//     RECORTAR EL STOP. Eso metía el stop dentro del ruido y mataba operaciones que iban a ganar.
//     Aquí es al revés. El stop se queda donde el mercado dice, y lo que se adapta es cuántas acciones se compran:
//         acciones = riesgo_permitido / distancia_al_stop
//     Con $350 de riesgo: acción de $40 con stop a $0.50 -> 700 acciones. Acción de $400 con stop a $6 -> 58.
//     En las dos arriesgas $350 exactos. El tope de pérdida deja de ser un recorte y pasa a ser la regla de tamaño,
//     que es como debe funcionar.
//     Consecuencia técnica: NO existe un "dólares por punto" global. Cada operación tiene el suyo, congelado al
//     abrir en `tPP` (que en acciones es, literalmente, el número de acciones). Todo el dinero del script pasa por ahí.
//
// A2) DOS FRENOS AL TAMAÑO, imprescindibles con acciones baratas
//     Un stop de $0.04 haría que la fórmula pidiera 8.750 acciones. `maxPosUsd` y `maxSharesIn` cortan eso.
//     Cuando muerden, tu riesgo real es MENOR que el presupuestado; el dashboard lleva la cuenta para que lo sepas.
//
// A3) SALIDAS PARCIALES POR PORCENTAJE (40/35/25 por defecto)
//     Aquí sí se puede repartir la posición, porque las acciones se dividen y un contrato de NQ no. El resto de la
//     división entera va al último tramo activo, para que la posición salga entera y no queden picos colgados.
//
// A4) HUECOS DE APERTURA — la limitación que NO se puede arreglar
//     Elegiste permitir posiciones overnight, así que esto hay que decirlo claro: una acción abre con hueco.
//     Si abre por debajo de tu stop, la orden se ejecuta EN LA APERTURA, no en tu nivel, y pierdes más de los $400.
//     Ningún stop lo evita. Ni éste ni el de tu bróker. Lo único que evita el riesgo de hueco es no dormir con la
//     posición abierta (`eodFlat`, apagado por defecto porque tú pediste dejarlas correr).
//     Lo que sí hace el script: detecta el hueco en la primera vela y sale a mercado sin esperar, en vez de dejar
//     la posición corriendo a ver qué pasa. Y lleva un contador de cuántas veces te ha ocurrido.
//     TU TOPE DE $400 ES REAL INTRADÍA Y ORIENTATIVO DE UN DÍA PARA OTRO. No es un fallo del código; es cómo
//     funcionan las acciones.
//
// A5) TODO RELATIVO AL PRECIO, no en valores absolutos
//     El "pegado a liquidez" pasa de 5 puntos fijos a un múltiplo del ATR. 5 puntos son el 12% en un valor de $40
//     y el 1% en uno de $400: un número absoluto no puede servir para las dos.
//     Por el mismo motivo el modo de objetivos por defecto es "Liquidity + R:R", que es relativo al riesgo.
//     El modo "Dollar targets" sigue estando, pero sólo tiene sentido si operas siempre el mismo valor.
//
// A6) SESIÓN ENCENDIDA POR DEFECTO (09:30-16:00)
//     Al revés que en futuros. En pre-market y after-hours el volumen es una fracción del real: los pivotes crean
//     niveles de liquidez falsos y el filtro de volumen deja pasar cualquier cosa.
//
// A7) FILTROS DE CALIDAD DEL VALOR (grupo nuevo)
//     Precio mínimo, volumen mínimo en dólares y rango de ATR en % del precio. Pine no puede leer el spread, así
//     que el precio mínimo es el único proxy disponible: en un valor de $1.50 un spread de $0.02 es el 1.3% de tu
//     entrada y te comes medio TP1 sólo en entrar y salir.
//
// LO QUE SE HEREDA DE LA VERSIÓN DE FUTUROS (sin cambios)
//     Tope de pérdida con cierre forzoso · trailing que sólo mueve el stop a favor · timeout · cancelación por
//     falta de progreso · registro interno · webhook · dashboard · tabla de rendimiento por setup.
//
// COSTES: comisión 0,5 centavos por acción y 2 ticks de slippage. Realista para large caps líquidas y OPTIMISTA
// para small caps. Si operas valores de bajo precio, sube el slippage antes de creerte el backtest.
// =====================================================================================================================
//@version=6
strategy('SMC Liquidity Sniper — Acciones', 'SMC Stocks', overlay = true, max_lines_count = 500, max_labels_count = 500, max_boxes_count = 500, initial_capital = 25000, currency = currency.USD, default_qty_type = strategy.fixed, default_qty_value = 100, pyramiding = 0, calc_on_every_tick = false, calc_on_order_fills = false, process_orders_on_close = true, commission_type = strategy.commission.cash_per_contract, commission_value = 0.005, slippage = 2)

//=====================================================================================================================
// INPUTS
//=====================================================================================================================
gG = '⚙️  General'
atrLen        = input.int(200, 'ATR length', minval = 1, group = gG)
volLen        = input.int(50, 'Volume average', minval = 1, group = gG)
volBase       = input.string('Median', 'Volume reference', options = ['Median', 'Average'], group = gG, tooltip = 'Median ignores the opening spike. Average gets inflated by it for the next `Volume average` bars and blocks every signal.')
momLen        = input.int(5, 'Momentum bars', minval = 1, group = gG)
tzOpt         = input.string('America/New_York', 'Timezone for displayed clocks', options = ['Chart exchange', 'America/New_York', 'America/Chicago', 'America/Los_Angeles', 'UTC', 'Europe/London', 'Europe/Madrid'], group = gG)

gL = '💧  Liquidity'
pivLen        = input.int(5, 'Pivot lookback (liquidity)', minval = 1, maxval = 100, group = gL)
areaMode      = input.string('Wick Extremity', 'Liquidity area', options = ['Wick Extremity', 'Full Range'], group = gL)
maxLevels     = input.int(20, 'Max levels per side', minval = 2, maxval = 25, group = gL)
showLiq       = input.bool(false, 'Draw liquidity levels', group = gL)
showLiqLbl    = input.bool(false, 'Show volume · touches', group = gL)

gS = '🏗️  Structure (SMC)'
intLen        = input.int(5, 'Internal structure', minval = 2, group = gS)
swgLen        = input.int(25, 'Swing structure', minval = 5, group = gS)
showIntBos    = input.bool(false, 'Show internal BOS / CHoCH', group = gS, tooltip = 'Off by default: on 1m this fires on every micro pivot and floods the 500-line budget, which makes TradingView drop older drawings including entry marks.')
showSwgBos    = input.bool(true, 'Show swing BOS / CHoCH', group = gS)
bosWidth      = input.int(1, 'BOS line width', minval = 1, maxval = 4, group = gS)
extendBos     = input.bool(true, 'Extend BOS lines to the right', group = gS)
colorBars     = input.bool(false, 'Color bars with trend', group = gS)

gV = '🕯️  Candle Dominance'
useCandle     = input.bool(true, 'Enable candle system', group = gV)
candleFilter  = input.bool(true, 'Use it as a filter for other entries', group = gV)
candleEntry   = input.bool(false, 'Generate its own entries', group = gV, tooltip = 'Off by default: in testing this engine produced 416 trades at 66% win rate for -$8990. Keep it as a filter, not as an entry.')
candleDom     = input.float(1.15, 'Right candle must exceed left candle (x)', step = 0.05, minval = 1.0, group = gV)
candleMinBody = input.float(0.05, 'Min left-candle body (x ATR)', step = 0.01, minval = 0.0, group = gV, tooltip = 'FIX B — without this a doji (body = 0) makes every following candle \'dominant\', because `body > 0 * 1.15` is true for any candle at all.')
candleVolDom  = input.float(1.00, 'Min volume vs left candle (x)', step = 0.05, minval = 0, group = gV)
zoneMode      = input.string('Both (recommended)', 'Zone definition', options = ['Premium / Discount', 'Structure', 'Both (recommended)', 'Liquidity / OB'], group = gV)
zoneLen       = input.int(50, 'Premium/Discount range', minval = 10, group = gV)
zoneNear      = input.float(1.0, 'Liquidity proximity (x ATR)', step = 0.1, minval = 0.1, group = gV)
showCandle    = input.bool(false, 'Color the dominant candle', group = gV)
oppositeOnly  = input.bool(true, 'Left candle must be the opposite color (reversal)', group = gV)
showDots      = input.bool(false, 'Mark dominant candles with dots', group = gV)

gF = '🧩  Liquidity + FVG'
useFvgSetup   = input.bool(true, 'Liquidity + FVG entries', group = gF)
showFvg       = input.bool(true, 'Draw fair value gaps', group = gF)
fvgMinAtr     = input.float(0.15, 'Min FVG size (x ATR)', step = 0.05, minval = 0, group = gF)
mssWindow     = input.int(12, 'Max bars from sweep to MSS', minval = 1, group = gF)
fvgWindow     = input.int(12, 'Max bars from MSS to FVG', minval = 1, group = gF)
mitWindow     = input.int(20, 'Max bars waiting for mitigation', minval = 1, group = gF)
seqStrict     = input.bool(false, 'Each step of the sequence needs its own candle', group = gF, tooltip = 'FIX A — the mitigation step ALWAYS requires a later candle now (that was the bug). This extra option also forces the sweep, the MSS and the FVG onto separate candles, so one huge candle cannot complete the whole sequence by itself.')
fvgUseSwing   = input.bool(false, 'Require a swing MSS (stricter)', group = gF)
markSeq       = input.bool(true, 'Tag the sweep and the MSS on the chart', group = gF)
maxFvg        = input.int(10, 'Max FVG boxes on screen', minval = 1, maxval = 40, group = gF)

gE = '🎯  Entries'
mode          = input.string('Aggressive', 'Mode', options = ['Aggressive', 'Balanced', 'Conservative'], group = gE)
useSweep      = input.bool(true, 'Liquidity sweep entries', group = gE)
useBreak      = input.bool(true, 'CHoCH/BOS entries with displacement', group = gE)
useBosHalf    = input.bool(true, 'Half-body close beyond a BOS / CHoCH line', group = gE)
halfRatio     = input.float(0.5, 'Body fraction beyond the line', step = 0.1, minval = 0.1, maxval = 1.0, group = gE)
bosSrc        = input.string('Both', 'Lines used for that entry', options = ['Both', 'Swing only', 'Internal only'], group = gE)
minVolRatio   = input.float(0.5, 'Min volume (x reference)', step = 0.1, minval = 0, group = gE)
dispMult      = input.float(0.55, 'Min displacement (x ATR)', step = 0.05, minval = 0, group = gE)
confirmBars   = input.int(8, 'Sweep validity (bars)', minval = 1, group = gE)
cooldownBar   = input.int(2, 'Min bars between signals', minval = 0, group = gE)
useCancel     = input.bool(true, 'Cancel the order if it makes no progress', group = gE)
cancelBars    = input.int(8, 'Bars to confirm progress', minval = 1, group = gE)
cancelR       = input.float(0.25, 'Min progress in favor (R)', step = 0.05, minval = 0.05, group = gE)
cancelOffset  = input.float(1.8, 'Cancel label distance (x ATR)', step = 0.1, minval = 0.2, group = gE)
cancelWipe    = input.bool(true, 'Wipe the whole trade off the chart when the order is cancelled', group = gE)
cancelNotice  = input.bool(true, 'Leave the CANCELLED notice on the chart', group = gE)
rearmLvl      = input.bool(true, 'Re-arm the BOS level after a cancelled order', group = gE)
signalOffset  = input.float(1.4, 'Signal label distance (x ATR)', step = 0.1, minval = 0.2, group = gE)
earlySignal   = input.bool(false, 'Fire the signal before the candle closes', group = gE, tooltip = 'S4 — OFF by default in the strategy version. It repaints by design: the signal is judged on a provisional close and the order cannot be un-sent if the candle turns. With it on, the backtest and the live behaviour stop being the same thing.')
earlySecs     = input.int(15, 'Seconds before close', minval = 1, maxval = 60, group = gE)
useSession    = input.bool(true, 'Filtro de sesión', group = gE, inline = 'ses')
sessInput     = input.session('0930-1600', '', group = gE, inline = 'ses', tooltip = 'A6 — ENCENDIDO por defecto, al revés que en futuros. En pre-market y after-hours el volumen es una fracción del real, así que los pivotes crean niveles de liquidez falsos y el filtro de volumen deja pasar cualquier cosa. NQ cotiza casi 24h y no tiene ese problema; una acción sí.')

gQ = '🏦  Filtros del valor  ·  calidad del subyacente'
minPrice      = input.float(3.0, 'Precio mínimo ($)', step = 0.5, minval = 0, group = gQ, tooltip = 'A7 — por debajo de esto el spread se come la operación. En un valor de $1.50 un spread de $0.02 es el 1.3% de tu entrada: te comes medio TP1 sólo en entrar y salir. Pine no puede medir el spread, así que el precio es el único filtro disponible.')
maxPriceIn    = input.float(0, 'Precio máximo ($), 0 = sin límite', step = 10, minval = 0, group = gQ)
minDollarVol  = input.float(2000000, 'Volumen mínimo en $ por vela (media)', step = 250000, minval = 0, group = gQ, tooltip = 'Volumen medio × precio. Filtra los valores donde tu propia orden movería el precio. Bájalo si operas small caps de verdad, súbelo si sólo quieres large caps.')
minAtrPct     = input.float(0.10, 'ATR mínimo (% del precio)', step = 0.05, minval = 0, group = gQ, tooltip = 'Descarta valores dormidos: si el ATR es el 0.02% del precio no hay recorrido que capturar y los objetivos nunca se tocan.')
maxAtrPct     = input.float(0, 'ATR máximo (% del precio), 0 = sin límite', step = 0.5, minval = 0, group = gQ, tooltip = 'Para no operar el día de resultados o un valor en pleno desplome. Un ATR del 8% del precio no es una oportunidad, es un valor roto.')

gT = '💰  TP / SL'
maxLossUsd    = input.float(400, '★ PÉRDIDA MÁXIMA POR OPERACIÓN ($)', step = 25, minval = 25, group = gT, tooltip = 'R1 — techo absoluto. El stop nunca se coloca a una distancia que valga más que esto. Si el stop técnico queda más lejos se recorta (o se descarta la señal, según la opción de abajo). Comisión y slippage van aparte: deja margen si necesitas que el techo sea el importe final.')
panicLossUsd  = input.float(350, '★ CERRAR LA OPERACIÓN AL ir en contra ($)', step = 25, minval = 25, group = gT, tooltip = 'R2 — el disparador de cierre. Se convierte en el stop real que vive en el mercado, no en una comprobación al cierre de vela. Si lo pones por encima de la pérdida máxima, manda la pérdida máxima.')
gapGuard      = input.bool(true, 'Cerrar a mercado si un hueco de apertura salta el stop', group = gT, tooltip = 'A4 — LA LIMITACIÓN QUE NO SE PUEDE ARREGLAR DEL TODO. Una acción abre con hueco. Si abre por debajo de tu stop, la orden se ejecuta en la apertura, no en tu nivel, y la pérdida es mayor de lo que pediste. Esto no lo evita nadie. Lo que hace esta opción es detectarlo en la primera vela y salir a mercado sin esperar, en vez de dejar la posición corriendo.')
eodFlat       = input.bool(false, 'Cerrar todo antes del cierre de mercado', group = gT, tooltip = 'La única forma de que el tope de pérdida sea REAL es no dormir con posiciones abiertas. Elegiste dejar correr las operaciones, así que esto viene desactivado — pero está aquí si cambias de idea.')
eodSess       = input.session('1550-1600', 'Ventana de cierre forzoso', group = gT)
minProfitUsd  = input.float(150, '★ MINIMUM PROFIT PER TRADE ($)', step = 10, minval = 1, group = gT, tooltip = 'S3 — the master threshold. TP1 can never be worth less than this; a signal whose TP1 does not reach it is discarded; once TP1 is touched the stop locks at least this amount; and the trailing stop never secures less than this. It also drives the log filter and the "Trades >= $" counter.')
tpMode        = input.string('Liquidity + R:R', 'Cómo se colocan los objetivos', options = ['Dollar targets', 'Liquidity + R:R'], group = gT, tooltip = 'A5 — por defecto R:R, al revés que en futuros. Los objetivos en dólares fijos ($250/$600/$1500) NO escalan entre valores: en una acción de $40 con 700 acciones, $250 son $0.36 de recorrido; en una de $400 con 58 acciones son $4.31. Usa Dollar targets sólo si operas siempre el mismo valor.')
tp1Usd        = input.float(250, 'TP1 ($)', step = 25, minval = 1, group = gT, tooltip = 'Raised to the minimum profit above if it is set lower.')
tp2Usd        = input.float(600, 'TP2 ($)', step = 25, minval = 1, group = gT)
tp3Usd        = input.float(1500, 'TP3 ($)', step = 50, minval = 1, group = gT)
enforceMinTp  = input.bool(true, 'Reject signals whose TP1 does not reach the minimum profit', group = gT)
snapAtr       = input.float(0.25, 'Pegar el objetivo a la liquidez cercana si está a (x ATR), 0 = off', step = 0.05, minval = 0, group = gT, tooltip = 'A5 — en la versión de futuros esto eran 5 puntos fijos. En acciones no sirve: 5 puntos son un 12% en un valor de $40 y un 1% en uno de $400. Ahora es relativo al ATR, así que escala solo.')
useRFloor     = input.bool(false, 'Also require the minimum R:R below', group = gT, tooltip = 'With this off, `Dollar targets` ignores risk entirely: on a wide-ATR candle a 30-point stop can be paired with a 12-point TP1. Turn it on if you never want R:R below the values set here.')
slBuf         = input.float(0.30, 'SL buffer (x ATR)', step = 0.05, minval = 0, group = gT)
slLook        = input.int(2, 'SL lookback (bars used for the swing)', minval = 1, maxval = 20, group = gT, tooltip = 'FIX F — was hard-wired to 2. Raise it if the stop keeps landing inside the noise.')
worstCase     = input.bool(true, 'Resolve ambiguous bars as losses', group = gT, tooltip = 'FIX E — when one candle contains both TP3 and the stop there is no way to know which came first. On, the stop wins (honest). Off, TP3 wins (the old, optimistic behaviour).')
rr1           = input.float(1.0, 'Min R:R TP1', step = 0.1, minval = 0.1, group = gT)
rr2           = input.float(2.0, 'Min R:R TP2', step = 0.1, minval = 0.2, group = gT)
rr3           = input.float(3.0, 'Min R:R TP3', step = 0.1, minval = 0.3, group = gT)
tp3Ext        = input.float(1.5, 'ATR extension for TP3', step = 0.1, minval = 0, group = gT)
volBoost      = input.bool(true, 'Widen TP on high volume', group = gT)
rrMax         = input.float(8.0, 'Max allowed R:R', step = 0.5, minval = 1, group = gT)
useTimeout    = input.bool(true, 'Close the trade if it never resolves', group = gT, tooltip = 'Without this, one trade that never reaches TP3 or SL keeps `freeSl` false forever and blocks every later signal.')
maxBars       = input.int(45, 'Close the trade after (bars)', minval = 5, group = gT)
showTrade     = input.bool(true, 'Draw active trade', group = gT)
trailOn       = input.bool(true, 'Trail the stop into profit (TP1 → break-even, TP2 → TP1)', group = gT)
trailBE       = input.float(150, 'Profit locked in as soon as TP1 is touched ($)', step = 10, minval = 0, group = gT, tooltip = 'Never applied below the minimum profit above.')
trailFollow   = input.bool(true, 'Stop follows the profit from the very first move', group = gT)
trailGap      = input.float(100, 'Cushion left behind the best profit ($)', step = 25, minval = 25, group = gT)
trailStep     = input.float(50, 'The stop only moves in steps of ($), 0 = continuous', step = 25, minval = 0, group = gT)
logAtTp       = input.bool(true, 'Log the trade as a winner as soon as TP1 is touched', group = gT)
clearOnClose  = input.bool(true, 'Clear the trade from the chart when it hits TP or SL', group = gT, tooltip = 'The whole trade, signal mark included, leaves the chart the moment it resolves. Only the live trade is ever on screen.')
showPnl       = input.bool(true, 'Live profit line while the trade runs', group = gT)
pnlColor      = input.color(#ffe000, 'Live profit line color', group = gT)
pnlOffset     = input.int(5, 'Live profit label distance (bars to the right)', minval = 0, maxval = 30, group = gT)
pnlGap        = input.float(0.0, 'Live profit label height offset (x ATR)', step = 0.1, group = gT)
showCont      = input.bool(true, 'Mark continuation candles', group = gT)
contStep      = input.float(0.5, 'Progress between continuation marks (R)', step = 0.1, minval = 0.1, group = gT)
contKeep      = input.int(1, 'Continuation marks kept on screen', minval = 1, maxval = 10, group = gT)

gD = '📅  Día anterior  ·  PDH / PDL'
showPD        = input.bool(true, 'Dibujar el máximo y el mínimo del día anterior', group = gD, tooltip = 'PDH (Previous Day High) y PDL (Previous Day Low). En SMC son de los charcos de liquidez más fiables que existen: mucho más gente los está mirando que a un pivote de 5 velas.\n\nSON SÓLO DIBUJO. No generan entradas, no mueven objetivos y no filtran nada. Tus resultados no cambian ni un dólar por activar esto.')
pdSource      = input.string('Día completo del gráfico', 'De qué día', options = ['Día completo del gráfico'], group = gD, tooltip = 'El máximo y el mínimo de la vela diaria tal como la da TradingView. En NQ eso incluye la sesión de noche; en acciones es sólo el horario regular.')
pdhCss        = input.color(#f23645, 'PDH', group = gD, inline = 'pdc')
pdlCss        = input.color(#089981, 'PDL', group = gD, inline = 'pdc')
pdStyleIn     = input.string('Discontinua', 'Estilo de línea', options = ['Sólida', 'Discontinua', 'Punteada'], group = gD)
pdWidth       = input.int(1, 'Grosor', minval = 1, maxval = 4, group = gD)
pdLabels      = input.bool(true, 'Etiqueta con el precio', group = gD)
pdKeep        = input.bool(false, 'Dejar en el gráfico los niveles de días pasados', group = gD, tooltip = 'Apagado: sólo ves el PDH/PDL de hoy, y la línea se mueve cada día. Encendido: cada día deja su par de líneas congelado, útil para revisar hacia atrás dónde estaban los niveles. Ojo con el presupuesto de 500 líneas de TradingView si miras mucho histórico.')
pdBackPad     = input.int(10, 'Barras que la línea se extiende a la derecha', minval = 0, maxval = 100, group = gD)

gX = '⚠️  Liquidation'
proxAtr       = input.float(3.0, 'Detection radius (x ATR)', step = 0.5, minval = 0.5, group = gX)
liqThresh     = input.int(65, 'Warning threshold (0-100)', minval = 1, maxval = 100, group = gX)
showWarn      = input.bool(false, 'Mark warnings on chart (alerts stay active)', group = gX)

gZ = '📐  Tamaño de posición  ·  riesgo fijo en $'
riskPerTrade  = input.float(350, '★ RIESGO POR OPERACIÓN ($)', step = 25, minval = 10, group = gZ, tooltip = 'A1 — el corazón de esta versión. NO fijas un número de acciones: fijas cuánto estás dispuesto a perder. El script divide ese dinero entre la distancia al stop y de ahí salen las acciones.\n\nEjemplo: riesgo $350. En una acción de $40 con el stop a $0.50 -> 700 acciones. En una de $400 con el stop a $6 -> 58 acciones. En las dos arriesgas los mismos $350.\n\nAsí el stop se queda donde es técnicamente correcto y el que se adapta es el tamaño. Es al revés que en la versión de futuros, donde había que recortar el stop.')
maxPosUsd     = input.float(25000, 'Valor máximo de la posición ($)', step = 1000, minval = 100, group = gZ, tooltip = 'A2 — el freno imprescindible. Si el stop sale muy pegado (típico en una acción de $3), la fórmula del riesgo pide una barbaridad de acciones. Este tope corta ahí: nunca se compran más acciones de las que caben en este importe.')
maxSharesIn   = input.int(20000, 'Máximo de acciones', minval = 1, group = gZ, tooltip = 'Segundo freno, por si operas valores de muy bajo precio donde el tope en $ todavía deja un número de acciones que el libro no absorbe.')
minSharesIn   = input.int(1, 'Mínimo de acciones para abrir', minval = 1, group = gZ, tooltip = 'Si el riesgo por acción es tan grande que no sale ni este mínimo, la señal se descarta. Con acciones caras y stops anchos pasa.')
roundLot      = input.bool(false, 'Redondear a lotes de 100', group = gZ, tooltip = 'Algunos brókers dan mejor ejecución en lotes redondos. Redondea hacia abajo, así que nunca sube el riesgo.')
tp1Pct        = input.float(40, '% de la posición que sale en TP1', step = 5, minval = 0, maxval = 100, group = gZ, tooltip = 'A3 — aquí sí puedes partir la posición, porque las acciones se dividen y un contrato de NQ no. Los tres porcentajes se normalizan a 100 si no suman exacto.')
tp2Pct        = input.float(35, '% de la posición que sale en TP2', step = 5, minval = 0, maxval = 100, group = gZ)
tp3Pct        = input.float(25, '% de la posición que sale en TP3', step = 5, minval = 0, maxval = 100, group = gZ)

gR = '📒  Trade Log'
showLog       = input.bool(true, 'Show trade log table', group = gR)
showStats     = input.bool(true, 'Show setup performance table', group = gR)
statsPos      = input.string('Top left', 'Performance table position', options = ['Top left', 'Middle left', 'Bottom left', 'Middle right'], group = gR)
statsDrop     = input.int(3, 'Push the performance table down (rows)', minval = 0, maxval = 12, group = gR)
logWinMin     = input.bool(true, 'Log winners only above the minimum profit', group = gR, tooltip = 'FIX D — this used to do nothing when `Log the trade as a winner at TP1` was on, because the row already existed. The filter is now re-applied when the trade closes, and a small winner drops off the log at that point.')
logRows       = input.int(10, 'Trades to display', minval = 1, maxval = 30, group = gR)
logPos        = input.string('Bottom right', 'Log position', options = ['Bottom right', 'Bottom center', 'Bottom left'], group = gR)
logSize       = input.string('Tiny', 'Log text size', options = ['Tiny', 'Small', 'Normal'], group = gR)

gW = '🔗  Registro de entradas (webhook)'
whOn   = input.bool(false, 'Enviar cada operación a mi app', group = gW, tooltip = 'Manda un JSON al abrir y otro al cerrar. Sin esto el indicador sólo emite alertas de texto y el registro se queda vacío.')
whKey  = input.string('', 'Clave del webhook', group = gW, tooltip = 'La variable TV_KEY de la app. Viaja dentro del mensaje porque TradingView no deja poner cabeceras propias en un webhook.')
whTag  = input.string('', 'Etiqueta de esta gráfica (opcional)', group = gW)
whMute = input.bool(true, 'Silenciar las alertas de texto cuando el webhook está activo', group = gW, tooltip = 'Las de texto no son JSON: la app las descarta, pero llenan el historial de alertas de TradingView para nada.')

gP = '📊  Dashboard & Style'
showShapes    = input.bool(true, 'Leave a small arrow on past entries', group = gP, tooltip = 'A little triangle stays on every entry so you can see where they were. The full description box, the stem and the big arrow are still removed when the trade resolves or a new entry appears — only this small mark survives.')
shapeSize     = input.string('Tiny', 'Small arrow size', options = ['Tiny', 'Small', 'Normal', 'Large'], group = gP)
showClock     = input.bool(true, 'Countdown above the running candle', group = gP)
clockCss      = input.color(#ffe000, 'Countdown color', group = gP, inline = 'ck')
clockAlert    = input.color(#ff1744, 'Last seconds', group = gP, inline = 'ck')
clockSize     = input.string('Large', 'Countdown size', options = ['Small', 'Normal', 'Large', 'Huge'], group = gP)
markBull      = input.color(#00e676, 'Signal arrow bullish', group = gP, inline = 'ar')
markBear      = input.color(#ff1744, 'Signal arrow bearish', group = gP, inline = 'ar')
arrowSize     = input.string('Large', 'Signal arrow size', options = ['Small', 'Normal', 'Large', 'Huge'], group = gP)
clockOffset   = input.float(0.5, 'Countdown distance (x ATR)', step = 0.1, minval = 0.1, group = gP)
rightPad      = input.int(0, 'Bars drawn past the last candle', minval = 0, maxval = 30, group = gP, tooltip = 'Purely cosmetic: how far the drawings extend to the right of the last candle. It has no effect whatsoever on the orders.')
sigKeep       = input.int(1, 'Signal marks kept on screen', minval = 1, maxval = 60, group = gP, tooltip = '1 means a new entry always removes the previous one. Raise it only if you want to inspect a few recent signals side by side.')
keepLatest    = input.bool(true, 'Each new sweep / MSS / cancel mark removes the previous one', group = gP)
useCleanup    = input.bool(false, 'Clear chart marks after a while', group = gP)
cleanupMin    = input.int(120, 'Clear chart marks after (minutes)', minval = 1, group = gP)
cleanBos      = input.bool(true, 'Also clear old BOS / CHoCH lines', group = gP)
showPanel     = input.bool(true, 'Show dashboard', group = gP)
panelPos      = input.string('Top right', 'Dashboard position', options = ['Top right', 'Top left', 'Bottom right', 'Bottom left', 'Middle right'], group = gP)
bullCss       = input.color(#089981, 'Bullish', group = gP, inline = 'c')
bearCss       = input.color(#f23645, 'Bearish', group = gP, inline = 'c')
warnCss       = input.color(#ff9800, 'Warning', group = gP, inline = 'c')
txtSize       = input.string('Small', 'Text size', options = ['Tiny', 'Small', 'Normal'], group = gP)

//=====================================================================================================================
// TYPES AND GLOBAL VARIABLES
//=====================================================================================================================
// @type   Liquidity level (swing high/low) with its area, touch count and accumulated volume
type liq
	float price
	float top
	float btm
	int   count
	float vol
	bool  crossed
	bool  swept
	int   x1
	line  ln
	label lb

var array<liq> hiLv = array.new<liq>()   // buy-side liquidity (above price)
var array<liq> loLv = array.new<liq>()   // sell-side liquidity (below price)

// @type   Logged trade.  res: 1 = closed in profit · -1 = closed at stop loss · 0 = still running · 2 = cancelled
type tlog
	int    t     // entry time
	int    dir
	float  ent   // entry price
	float  ex    // exit price
	float  r
	float  usd
	int    tp    // best TP reached, 0 if none
	int    res
	string src   // which engine fired the entry

var array<tlog> logArr = array.new<tlog>()

// @type   A drawn BOS / CHoCH line kept alive as an entry level
type bosLvl
	float price
	int   dir
	bool  used

var array<bosLvl> bosArr = array.new<bosLvl>()

// @type   Fair value gap: the three-candle imbalance
type fvg
	float top
	float btm
	int   dir
	bool  mit
	box   bx

var array<fvg> fvgArr = array.new<fvg>()

// --- Liquidity + FVG state machine: 0 idle · 1 sweep done · 2 MSS done · 3 FVG armed
var int   sState    = 0
var int   sDir      = 0
var int   sBar      = na
var float sTop      = na
var float sBtm      = na
var float fvgEntTop = na
var float fvgEntBtm = na

// --- chart marks that expire: every drawing is registered with the time it was created
var array<label> tmpLb  = array.new<label>()
var array<int>   tmpLbT = array.new<int>()
var array<line>  tmpLn  = array.new<line>()
var array<int>   tmpLnT = array.new<int>()

// --- the detailed signal marks live in bounded arrays, so they can never exhaust the object budget
var array<label> sigLbArr = array.new<label>()
var array<line>  sigLnArr = array.new<line>()

// --- structure state
var float iHigh  = na
var float iLow   = na
var int   iHiBar = na
var int   iLoBar = na
var bool  iHiX   = false
var bool  iLoX   = false
var int   iTrend = 0
var float sHigh  = na
var float sLow   = na
var int   sHiBar = na
var int   sLoBar = na
var bool  sHiX   = false
var bool  sLoX   = false
var int   sTrend = 0

// --- counters and statistics
var int    cIntBull = 0
var int    cIntBear = 0
var int    cSwgBull = 0
var int    cSwgBear = 0
var int    cSweepUp = 0
var int    cSweepDn = 0
var int    cLong    = 0
var int    cShort   = 0
var int    cWin     = 0
var int    cLoss    = 0
var int    c1k      = 0
var int    cCancel  = 0
var int    cTimeout = 0
var int    cPanic   = 0   // R3: cierres forzosos por tope de pérdida
var int    cGap     = 0   // A4: huecos de apertura que saltaron el stop
var int    cCapped  = 0   // R1: entradas cuyo stop hubo que recortar al tope
var float  sumR     = 0.0
var float  sumUsd   = 0.0
var string lastBos  = '—'
var string lastSrc  = '—'
var string tSrcTag  = '—'
var int    tSrcIdx  = 0
var float  tBosPx   = na
var string blockTxt = '—'

// --- effectiveness per engine: 0 sweep · 1 BOS/CHoCH · 2 half body · 3 candle · 4 liquidity+FVG
var array<int>   srcN = array.from(0, 0, 0, 0, 0)
var array<int>   srcW = array.from(0, 0, 0, 0, 0)
var array<int>   srcC = array.from(0, 0, 0, 0, 0)
var array<float> srcR = array.from(0.0, 0.0, 0.0, 0.0, 0.0)
var array<float> srcU = array.from(0.0, 0.0, 0.0, 0.0, 0.0)

// --- active trade state
var int   tDir     = 0
var float tEnt     = na
var float tSl      = na
var float tT1      = na
var float tT2      = na
var float tT3      = na
var float tRisk    = na
var float tRiskUsd = na
var int   tEntTime = na
var float mfe      = 0.0
var bool  h1       = false
var bool  h2       = false
var bool  h3       = false
var bool  logged   = false
var int   tBar     = na
var int   lastSigBar = na

var line  lnE = na
var line  lnS = na
var line  ln1 = na
var line  ln2 = na
var line  ln3 = na
var label lbE = na
var label lbS = na
var label lb1 = na
var label lb2 = na
var label lb3 = na
var float lastContR = 0.0
var array<label> contLb = array.new<label>()
var line  sigLn  = na
var label sigArw = na
var label sigTxt = na
var line  cxlLn  = na
var label cxlArw = na
var label cxlTxt = na
var label swpLb  = na
var label mssLb  = na
var line  pnlLn  = na
var label pnlLb  = na

// --- S2: ids of the real strategy orders
string ID_L = 'LONG'
string ID_S = 'SHORT'
string XL1  = 'L TP1'
string XL2  = 'L TP2'
string XL3  = 'L TP3'
string XS1  = 'S TP1'
string XS2  = 'S TP2'
string XS3  = 'S TP3'

//=====================================================================================================================
// BASE CALCULATIONS
//=====================================================================================================================
lblSize = switch txtSize
    'Tiny'  => size.tiny
    'Small' => size.small
    =>         size.normal

// one timezone for every clock on screen, so the log matches the chart
string tzUse = tzOpt == 'Chart exchange' ? syminfo.timezone : tzOpt

vol = nz(volume)
atr = nz(ta.atr(atrLen), nz(ta.atr(14), ta.tr))

// both references are computed on every bar (never inside a conditional) and one is selected
volSma   = ta.sma(vol, volLen)
volMed   = ta.median(vol, volLen)
volRef   = volBase == 'Median' ? volMed : volSma
volRatio = volRef > 0 ? vol / volRef : 1.0

rng    = high - low
inSess = not useSession or not na(time(timeframe.period, sessInput))

// --- A7: filtros de calidad del valor. Todo relativo al precio, para que sirvan igual en AAPL y en un valor de $4.
float dollarVol = volRef * close                       // volumen medio de la vela, en dólares
float atrPct    = close > 0 ? atr / close * 100.0 : 0.0
bool  okPrice   = close >= minPrice and (maxPriceIn <= 0 or close <= maxPriceIn)
bool  okLiquid  = minDollarVol <= 0 or dollarVol >= minDollarVol
bool  okAtr     = atrPct >= minAtrPct and (maxAtrPct <= 0 or atrPct <= maxAtrPct)
bool  okStock   = okPrice and okLiquid and okAtr

// --- A4: primera vela de una sesión nueva, y ventana de cierre forzoso
bool newSession = ta.change(time('D')) != 0
bool eodNow     = eodFlat and not na(time(timeframe.period, eodSess))

// --- candle clock: seconds left until the current candle closes
float secsLeft = math.max(0.0, (time_close - timenow) / 1000.0)
bool  timingOK = not earlySignal or barstate.ishistory or secsLeft <= earlySecs

aggr       = mode == 'Aggressive'
cons       = mode == 'Conservative'
needBias   = cons
needStruct = mode == 'Balanced' or cons
needSweepC = mode == 'Balanced' or cons

// --- S3: dollars earned per point for the WHOLE tranche stack, and for one tranche
// --- A1: EN ACCIONES, 1 PUNTO = $1 POR ACCIÓN.
// Los "dólares por punto" de una posición SON, literalmente, el número de acciones. No hay una constante como
// el $20/pt de NQ: cada operación tiene su propio tamaño, calculado desde su propio stop. Por eso todo el
// dinero de este script es dinámico y vive en `tPP`, no en una variable global fija.
float riskBudget = math.min(riskPerTrade, maxLossUsd)   // el presupuesto nunca supera el techo absoluto
float panicEff   = math.min(panicLossUsd, maxLossUsd)   // el disparador de cierre, tampoco

// reparto de la posición entre los tres objetivos, normalizado a 100 aunque los inputs no sumen exacto
float pctSum = tp1Pct + tp2Pct + tp3Pct
float w1 = pctSum > 0 ? tp1Pct / pctSum : 1.0
float w2 = pctSum > 0 ? tp2Pct / pctSum : 0.0
float w3 = pctSum > 0 ? tp3Pct / pctSum : 0.0

// tamaño de la operación viva: se fija al abrir y no se toca hasta que cierra
var float tShares = 0.0   // acciones de la operación en curso
var float tPP     = 0.0   // dólares por cada $1 de movimiento = acciones

//=====================================================================================================================
// FUNCTIONS
//=====================================================================================================================
// @function  Scans live levels on one side: nearest level, its score, the highest-scoring level and the max score
f_scan(bool above) =>
    array<liq> lv = above ? hiLv : loLv
    float nearP = na
    float nearS = 0.0
    float bestP = na
    float bestS = 0.0
    float maxS  = 0.0
    if lv.size() > 0
        for i = 0 to lv.size() - 1 by 1
            liq l = lv.get(i)
            if not l.crossed
                float sc = l.vol * (1.0 + l.count / 10.0)
                maxS := math.max(maxS, sc)
                bool ok = above ? l.price > close : l.price < close
                if ok
                    if na(nearP) or (above ? l.price < nearP : l.price > nearP)
                        nearP := l.price
                        nearS := sc
                        nearS
                    if sc > bestS
                        bestS := sc
                        bestP := l.price
                        bestP
    [nearP, nearS, bestP, bestS, maxS]

// @function  Creates a new liquidity level and trims the array to the allowed maximum
f_push(bool isHigh, float p, float t, float b, int x, float seedVol) =>
    array<liq> lv = isHigh ? hiLv : loLv
    color css = isHigh ? bearCss : bullCss
    liq l = liq.new(price = p, top = t, btm = b, count = 1, vol = seedVol, crossed = false, swept = false, x1 = x)
    if showLiq
        l.ln := line.new(x, p, bar_index, p, color = color.new(css, 25), width = 1)
        if showLiqLbl
            l.lb := label.new(x, p, '', style = isHigh ? label.style_label_down : label.style_label_up, color = #00000000, textcolor = css, size = lblSize)
            l.lb
    lv.unshift(l)
    if lv.size() > maxLevels
        liq old = lv.pop()
        line.delete(old.ln)
        label.delete(old.lb)

// @function  Registers a label so it can be removed once it expires.
f_keepLb(label l) =>
    if useCleanup
        tmpLb.unshift(l)
        tmpLbT.unshift(time)

// @function  Registers a line so it can be removed once it expires
f_keepLn(line l) =>
    if useCleanup
        tmpLn.unshift(l)
        tmpLnT.unshift(time)

// @function  Turns a size option into a label size
f_size(string x) =>
    switch x
        'Small'  => size.small
        'Normal' => size.normal
        'Large'  => size.large
        =>          size.huge

// @function  Valor en dólares de una distancia de precio, para un tamaño dado de acciones
f_usd(float dist, float pp) =>
    math.abs(dist) * pp

// @function  A1 — acciones que salen de arriesgar `riskBudget` con el stop a `stopDist` de distancia.
//            Los dos frenos (valor de la posición y número de acciones) existen por las acciones baratas:
//            con un stop de $0.04 la fórmula del riesgo pediría 8.750 acciones, y eso ni se ejecuta ni se financia.
f_shares(float stopDist, float px) =>
    float byRisk  = stopDist > 0 ? riskBudget / stopDist : 0.0
    float byValue = px > 0 ? maxPosUsd / px : 0.0
    float n = math.min(byRisk, byValue, maxSharesIn)
    n := roundLot ? math.floor(n / 100) * 100 : math.floor(n)
    math.max(n, 0.0)

// @function  Formats a price using the symbol tick size
f_p(float x) =>
    na(x) ? '—' : str.tostring(x, format.mintick)

// @function  0-100 progress bar for the dashboard
f_bar(float v) =>
    float vv = nz(v, 0)
    int n = math.max(0, math.min(10, math.round(vv / 10)))
    str.repeat('█', n) + str.repeat('·', 10 - n) + '  ' + str.tostring(math.round(vv))

//=====================================================================================================================
// 1) LIQUIDITY ENGINE  (derived from Liquidity Swings)
//=====================================================================================================================
ph = ta.pivothigh(pivLen, pivLen)
pl = ta.pivotlow(pivLen, pivLen)
volSum = math.sum(vol, pivLen + 1)   // computed on every bar (never inside an if)

if not na(ph)
    float hTop = high[pivLen]
    float hBtm = areaMode == 'Wick Extremity' ? math.max(close[pivLen], open[pivLen]) : low[pivLen]
    f_push(true, high[pivLen], hTop, hBtm, bar_index - pivLen, volSum[pivLen])

if not na(pl)
    float lBtm = low[pivLen]
    float lTop = areaMode == 'Wick Extremity' ? math.min(close[pivLen], open[pivLen]) : high[pivLen]
    f_push(false, low[pivLen], lTop, lBtm, bar_index - pivLen, volSum[pivLen])

// --- update touches, volume, sweeps and breaks
bool  sweepUp   = false   // sweep of highs -> SHORT signal
bool  sweepDn   = false   // sweep of lows  -> LONG signal
float sweepUpP  = na
float sweepDnP  = na
float sweepUpS  = 0.0
float sweepDnS  = 0.0
bool  breakUp   = false   // true break of upper liquidity
bool  breakDn   = false

if hiLv.size() > 0
    for i = 0 to hiLv.size() - 1 by 1
        liq l = hiLv.get(i)
        if not l.crossed
            if low < l.top and high > l.btm
                l.count := l.count + 1
                l.vol   := l.vol + vol
                l.vol
            float sc = l.vol * (1.0 + l.count / 10.0)
            if high > l.price and close < l.price
                l.crossed := true
                l.swept   := true
                sweepUp   := true
                if sc > sweepUpS
                    sweepUpS := sc
                    sweepUpP := l.price
                    sweepUpP
            else if close > l.price
                l.crossed := true
                breakUp   := true
                breakUp
            if not na(l.ln)
                line.set_x2(l.ln, l.crossed ? bar_index : bar_index + rightPad)
                if l.crossed
                    line.set_style(l.ln, line.style_dashed)
                    line.set_color(l.ln, color.new(l.swept ? warnCss : bearCss, 55))
            if not na(l.lb)
                label.set_xy(l.lb, l.x1, l.price)
                label.set_text(l.lb, str.tostring(l.vol, format.volume) + ' · ' + str.tostring(l.count))

if loLv.size() > 0
    for i = 0 to loLv.size() - 1 by 1
        liq l = loLv.get(i)
        if not l.crossed
            if low < l.top and high > l.btm
                l.count := l.count + 1
                l.vol   := l.vol + vol
                l.vol
            float sc = l.vol * (1.0 + l.count / 10.0)
            if low < l.price and close > l.price
                l.crossed := true
                l.swept   := true
                sweepDn   := true
                if sc > sweepDnS
                    sweepDnS := sc
                    sweepDnP := l.price
                    sweepDnP
            else if close < l.price
                l.crossed := true
                breakDn   := true
                breakDn
            if not na(l.ln)
                line.set_x2(l.ln, l.crossed ? bar_index : bar_index + rightPad)
                if l.crossed
                    line.set_style(l.ln, line.style_dashed)
                    line.set_color(l.ln, color.new(l.swept ? warnCss : bullCss, 55))
            if not na(l.lb)
                label.set_xy(l.lb, l.x1, l.price)
                label.set_text(l.lb, str.tostring(l.vol, format.volume) + ' · ' + str.tostring(l.count))

if sweepUp
    cSweepUp := cSweepUp + 1
    cSweepUp
if sweepDn
    cSweepDn := cSweepDn + 1
    cSweepDn

//=====================================================================================================================
// 2) SMC STRUCTURE (internal + swing)
//=====================================================================================================================
iph = ta.pivothigh(intLen, intLen)
ipl = ta.pivotlow(intLen, intLen)
sph = ta.pivothigh(swgLen, swgLen)
spl = ta.pivotlow(swgLen, swgLen)

if not na(iph)
    iHigh  := iph
    iHiBar := bar_index - intLen
    iHiX   := false
    iHiX
if not na(ipl)
    iLow   := ipl
    iLoBar := bar_index - intLen
    iLoX   := false
    iLoX
if not na(sph)
    sHigh  := sph
    sHiBar := bar_index - swgLen
    sHiX   := false
    sHiX
if not na(spl)
    sLow   := spl
    sLoBar := bar_index - swgLen
    sLoX   := false
    sLoX

bool   intBull = false
bool   intBear = false
string intTag  = ''
if not na(iHigh) and not iHiX and close > iHigh
    intTag   := iTrend == -1 ? 'CHoCH' : 'BOS'
    iHiX     := true
    iTrend   := 1
    intBull  := true
    cIntBull := cIntBull + 1
    cIntBull
if not na(iLow) and not iLoX and close < iLow
    intTag   := iTrend == 1 ? 'CHoCH' : 'BOS'
    iLoX     := true
    iTrend   := -1
    intBear  := true
    cIntBear := cIntBear + 1
    cIntBear

bool   swgBull = false
bool   swgBear = false
string swgTag  = ''
if not na(sHigh) and not sHiX and close > sHigh
    swgTag   := sTrend == -1 ? 'CHoCH' : 'BOS'
    sHiX     := true
    sTrend   := 1
    swgBull  := true
    cSwgBull := cSwgBull + 1
    cSwgBull
if not na(sLow) and not sLoX and close < sLow
    swgTag   := sTrend == 1 ? 'CHoCH' : 'BOS'
    sLoX     := true
    sTrend   := -1
    swgBear  := true
    cSwgBear := cSwgBear + 1
    cSwgBear

// --- BOS / CHoCH lines: drawn from the broken pivot to the candle that broke it
if showIntBos and intBull and not na(iHiBar)
    line  bl = line.new(iHiBar, iHigh, extendBos ? bar_index + rightPad : bar_index, iHigh, color = color.new(bullCss, 35), style = line.style_dashed, width = bosWidth)
    label bt = label.new(math.round(math.avg(iHiBar, bar_index)), iHigh, 'i' + intTag, style = label.style_label_down, color = #00000000, textcolor = color.new(bullCss, 20), size = size.tiny)
    if cleanBos
        f_keepLn(bl)
        f_keepLb(bt)
if showIntBos and intBear and not na(iLoBar)
    line  bl = line.new(iLoBar, iLow, extendBos ? bar_index + rightPad : bar_index, iLow, color = color.new(bearCss, 35), style = line.style_dashed, width = bosWidth)
    label bt = label.new(math.round(math.avg(iLoBar, bar_index)), iLow, 'i' + intTag, style = label.style_label_up, color = #00000000, textcolor = color.new(bearCss, 20), size = size.tiny)
    if cleanBos
        f_keepLn(bl)
        f_keepLb(bt)
if showSwgBos and swgBull and not na(sHiBar)
    line  bl = line.new(sHiBar, sHigh, extendBos ? bar_index + rightPad : bar_index, sHigh, color = bullCss, style = line.style_solid, width = bosWidth)
    label bt = label.new(math.round(math.avg(sHiBar, bar_index)), sHigh, swgTag, style = label.style_label_down, color = #00000000, textcolor = bullCss, size = size.small)
    if cleanBos
        f_keepLn(bl)
        f_keepLb(bt)
if showSwgBos and swgBear and not na(sLoBar)
    line  bl = line.new(sLoBar, sLow, extendBos ? bar_index + rightPad : bar_index, sLow, color = bearCss, style = line.style_solid, width = bosWidth)
    label bt = label.new(math.round(math.avg(sLoBar, bar_index)), sLow, swgTag, style = label.style_label_up, color = #00000000, textcolor = bearCss, size = size.small)
    if cleanBos
        f_keepLn(bl)
        f_keepLb(bt)

// last structure event stored for the dashboard
if swgBull
    lastBos := '▲ swing ' + swgTag + ' ' + f_p(sHigh)
    lastBos
if swgBear
    lastBos := '▼ swing ' + swgTag + ' ' + f_p(sLow)
    lastBos
if intBull and not swgBull and not swgBear
    lastBos := '▲ internal ' + intTag + ' ' + f_p(iHigh)
    lastBos
if intBear and not swgBull and not swgBear
    lastBos := '▼ internal ' + intTag + ' ' + f_p(iLow)
    lastBos

//---------------------------------------------------------------------------------------------------------------------
// 2.5) BOS / CHoCH LINES AS ENTRY LEVELS
//---------------------------------------------------------------------------------------------------------------------
bool useInt = bosSrc == 'Both' or bosSrc == 'Internal only'
bool useSwg = bosSrc == 'Both' or bosSrc == 'Swing only'

if useBosHalf
    if intBull and useInt and not na(iHigh)
        bosArr.unshift(bosLvl.new(iHigh, 1, false))
    if intBear and useInt and not na(iLow)
        bosArr.unshift(bosLvl.new(iLow, -1, false))
    if swgBull and useSwg and not na(sHigh)
        bosArr.unshift(bosLvl.new(sHigh, 1, false))
    if swgBear and useSwg and not na(sLow)
        bosArr.unshift(bosLvl.new(sLow, -1, false))
    if bosArr.size() > 40
        bosArr.pop()

bool  longBosHalf  = false
bool  shortBosHalf = false
float bosLevelHit  = na
float bodyTop = math.max(open, close)
float bodyBot = math.min(open, close)
float bodySz  = math.max(bodyTop - bodyBot, syminfo.mintick)

if useBosHalf and bosArr.size() > 0
    for i = 0 to bosArr.size() - 1 by 1
        bosLvl b = bosArr.get(i)
        if not b.used
            if b.dir == 1 and close > b.price
                float portion = math.min(1.0, math.max(0.0, (bodyTop - b.price) / bodySz))
                if portion >= halfRatio
                    b.used      := true
                    longBosHalf := true
                    bosLevelHit := b.price
                    bosLevelHit
            else if b.dir == -1 and close < b.price
                float portion = math.min(1.0, math.max(0.0, (b.price - bodyBot) / bodySz))
                if portion >= halfRatio
                    b.used       := true
                    shortBosHalf := true
                    bosLevelHit  := b.price
                    bosLevelHit

//=====================================================================================================================
// 3) TARGET SCAN AND LIQUIDATION METER
//=====================================================================================================================
[upNear, upNearS, upBest, upBestS, upMax] = f_scan(true)
[dnNear, dnNearS, dnBest, dnBestS, dnMax] = f_scan(false)

float distUp = na(upNear) ? float(na) : (upNear - close) / atr
float distDn = na(dnNear) ? float(na) : (close - dnNear) / atr
float proxUp = na(distUp) ? 0.0 : math.max(0.0, 1.0 - distUp / proxAtr)
float proxDn = na(distDn) ? 0.0 : math.max(0.0, 1.0 - distDn / proxAtr)
float strUp  = upMax > 0 and upNearS > 0 ? math.min(1.0, upNearS / upMax) : 0.0
float strDn  = dnMax > 0 and dnNearS > 0 ? math.min(1.0, dnNearS / dnMax) : 0.0
float momUp  = math.max(0.0, math.min(1.0, (close - close[momLen]) / (atr * 1.5)))
float momDn  = math.max(0.0, math.min(1.0, (close[momLen] - close) / (atr * 1.5)))
float volF   = math.min(1.5, volRatio) / 1.5

float riskUp = 100 * (0.45 * proxUp + 0.28 * strUp + 0.17 * momUp + 0.10 * volF)
float riskDn = 100 * (0.45 * proxDn + 0.28 * strDn + 0.17 * momDn + 0.10 * volF)
riskUp := na(upNear) ? 0.0 : riskUp
riskDn := na(dnNear) ? 0.0 : riskDn

warnUp = riskUp >= liqThresh and riskUp[1] < liqThresh
warnDn = riskDn >= liqThresh and riskDn[1] < liqThresh

if showWarn and warnUp
    label.new(bar_index, high + atr * 0.6, '⚠ LIQ ↑\n' + f_p(upNear), style = label.style_label_down, color = color.new(warnCss, 80), textcolor = warnCss, size = size.tiny)
if showWarn and warnDn
    label.new(bar_index, low - atr * 0.6, '⚠ LIQ ↓\n' + f_p(dnNear), style = label.style_label_up, color = color.new(warnCss, 80), textcolor = warnCss, size = size.tiny)

//=====================================================================================================================
// 3.5) CANDLE DOMINANCE SYSTEM
//=====================================================================================================================
float bodyR = math.abs(close - open)
float bodyL = math.abs(close[1] - open[1])
float volR  = vol
float volL  = nz(vol[1])

bool leftBull  = close[1] > open[1]
bool leftBear  = close[1] < open[1]
bool rightBull = close > open
bool rightBear = close < open

// FIX B: the left candle needs a real body. Without this a doji makes every following candle "dominant".
bool leftReal = bodyL >= atr * candleMinBody and bodyL > 0
bool bigger   = leftReal and bodyR > bodyL * candleDom and bodyR > 0
bool volDomOK = volL <= 0 ? true : volR >= volL * candleVolDom

bool domBull = bigger and volDomOK and rightBull and (not oppositeOnly or leftBear)
bool domBear = bigger and volDomOK and rightBear and (not oppositeOnly or leftBull)

// --- zone
float zTop = ta.highest(high, zoneLen)
float zBtm = ta.lowest(low, zoneLen)
float zEq  = math.avg(zTop, zBtm)
bool  disc = close <= zEq
bool  prem = close > zEq
bool  nearLiqDn = not na(dnNear) and close - dnNear <= atr * zoneNear
bool  nearLiqUp = not na(upNear) and upNear - close <= atr * zoneNear

bool zoneBull = switch zoneMode
    'Premium / Discount' => disc
    'Structure'          => iTrend == 1 or sTrend == 1
    'Liquidity / OB'     => nearLiqDn
    =>                      disc and sTrend >= 0 or iTrend == 1 and sTrend == 1

bool zoneBear = switch zoneMode
    'Premium / Discount' => prem
    'Structure'          => iTrend == -1 or sTrend == -1
    'Liquidity / OB'     => nearLiqUp
    =>                      prem and sTrend <= 0 or iTrend == -1 and sTrend == -1

bool candleBull = useCandle and domBull and zoneBull
bool candleBear = useCandle and domBear and zoneBear
int  candleDir  = not useCandle ? 0 : domBull ? 1 : domBear ? -1 : 0

//=====================================================================================================================
// 3.6) LIQUIDITY + FVG SETUP
//=====================================================================================================================
float gapUpBtm = high[2]
float gapDnTop = low[2]
bool  fvgBull  = low > gapUpBtm and low - gapUpBtm >= fvgMinAtr * atr
bool  fvgBear  = high < gapDnTop and gapDnTop - high >= fvgMinAtr * atr

if useFvgSetup and showFvg
    if fvgBull
        box bb = box.new(bar_index - 2, low, bar_index + rightPad, gapUpBtm, border_color = color.new(bullCss, 60), bgcolor = color.new(bullCss, 88))
        fvgArr.unshift(fvg.new(low, gapUpBtm, 1, false, bb))
    if fvgBear
        box bd = box.new(bar_index - 2, gapDnTop, bar_index + rightPad, high, border_color = color.new(bearCss, 60), bgcolor = color.new(bearCss, 88))
        fvgArr.unshift(fvg.new(gapDnTop, high, -1, false, bd))
    if fvgArr.size() > 0
        for i = fvgArr.size() - 1 to 0 by 1
            fvg f = fvgArr.get(i)
            if f.dir == 1 and low <= f.btm or f.dir == -1 and high >= f.top
                box.delete(f.bx)
                fvgArr.remove(i)
            else
                box.set_right(f.bx, bar_index + rightPad)
    if fvgArr.size() > maxFvg
        fvg oldF = fvgArr.pop()
        box.delete(oldF.bx)

bool longFvgSig  = false
bool shortFvgSig = false

if useFvgSetup
    // step 1 - liquidity is swept (FIX I: the candle's own direction decides when both sides are swept)
    if sweepDn and (not sweepUp or close > open)
        sState := 1
        sDir   := 1
        sBar   := bar_index
        if markSeq
            if keepLatest
                label.delete(swpLb)
            swpLb := label.new(bar_index, low, 'SSL sweep', style = label.style_label_up, color = #00000000, textcolor = color.new(bullCss, 20), size = size.tiny)
            f_keepLb(swpLb)
    if sweepUp and (not sweepDn or close <= open)
        sState := 1
        sDir   := -1
        sBar   := bar_index
        if markSeq
            if keepLatest
                label.delete(swpLb)
            swpLb := label.new(bar_index, high, 'BSL sweep', style = label.style_label_down, color = #00000000, textcolor = color.new(bearCss, 20), size = size.tiny)
            f_keepLb(swpLb)

    // the sequence expires if the next step takes too long
    if sState == 1 and not na(sBar) and bar_index - sBar > mssWindow
        sState := 0
        sDir   := 0
        sDir
    if sState == 2 and not na(sBar) and bar_index - sBar > fvgWindow
        sState := 0
        sDir   := 0
        sDir
    if sState == 3 and not na(sBar) and bar_index - sBar > mitWindow
        sState := 0
        sDir   := 0
        sDir

    // step 2 - market structure shift in the direction of the sweep
    if sState == 1 and (not seqStrict or bar_index > sBar)
        bool mssL = fvgUseSwing ? swgBull : intBull or swgBull
        bool mssS = fvgUseSwing ? swgBear : intBear or swgBear
        if sDir == 1 and mssL
            sState := 2
            sBar   := bar_index
            if markSeq
                if keepLatest
                    label.delete(mssLb)
                mssLb := label.new(bar_index, high, 'MSS', style = label.style_label_down, color = #00000000, textcolor = color.new(bullCss, 10), size = size.tiny)
                f_keepLb(mssLb)
        else if sDir == -1 and mssS
            sState := 2
            sBar   := bar_index
            if markSeq
                if keepLatest
                    label.delete(mssLb)
                mssLb := label.new(bar_index, low, 'MSS', style = label.style_label_up, color = #00000000, textcolor = color.new(bearCss, 10), size = size.tiny)
                f_keepLb(mssLb)

    // step 3 - the displacement leaves a fair value gap: that gap is armed as the entry zone
    if sState == 2 and (not seqStrict or bar_index > sBar)
        if sDir == 1 and fvgBull
            sState := 3
            sTop   := low
            sBtm   := gapUpBtm
            sBar   := bar_index
            sBar
        else if sDir == -1 and fvgBear
            sState := 3
            sTop   := gapDnTop
            sBtm   := high
            sBar   := bar_index
            sBar

    // step 4 - price comes back into the gap: entry.  FIX A: `bar_index > sBar` is NOT optional here.
    if sState == 3 and not na(sTop) and not na(sBtm) and not na(sBar) and bar_index > sBar
        if sDir == 1 and low <= sTop and close > sBtm
            longFvgSig := true
            fvgEntTop  := sTop
            fvgEntBtm  := sBtm
            sState     := 0
            sState
        else if sDir == -1 and high >= sBtm and close < sTop
            shortFvgSig := true
            fvgEntTop   := sTop
            fvgEntBtm   := sBtm
            sState      := 0
            sState

//=====================================================================================================================
// 3.9) MENSAJES PARA EL REGISTRO
//     Se construye el JSON a mano: str.format_json no existe en Pine y concatenar es lo que hay.
//     Los números van con str.tostring sin formato para que lleguen como número y no como texto.
//=====================================================================================================================
f_num(float v) => na(v) ? 'null' : str.tostring(v)
f_id() => syminfo.ticker + '-' + str.tostring(tEntTime)

//=====================================================================================================================
// 4) ENTRY ENGINE
//=====================================================================================================================
disp   = math.abs(close - open) >= dispMult * atr or rng >= dispMult * 1.6 * atr
volOK  = volRatio >= minVolRatio
cool   = na(lastSigBar) or bar_index - lastSigBar >= cooldownBar
// S1: the book must be flat for the internal engine AND for the strategy engine
freeSl = tDir == 0 and strategy.position_size == 0

sinceSweepDn = ta.barssince(sweepDn)
sinceSweepUp = ta.barssince(sweepUp)
recentDn = not na(sinceSweepDn) and sinceSweepDn <= confirmBars
recentUp = not na(sinceSweepUp) and sinceSweepUp <= confirmBars

biasLongOK  = not needBias or sTrend >= 0
biasShortOK = not needBias or sTrend <= 0
strLongOK   = not needStruct or iTrend == 1 or intBull
strShortOK  = not needStruct or iTrend == -1 or intBear

longSweep  = useSweep and sweepDn and close > open and (aggr or disp)
shortSweep = useSweep and sweepUp and close < open and (aggr or disp)
longBreak  = useBreak and intBull and disp and (not needSweepC or recentDn)
shortBreak = useBreak and intBear and disp and (not needSweepC or recentUp)

cFiltLong  = not(useCandle and candleFilter) or candleDir >= 0
cFiltShort = not(useCandle and candleFilter) or candleDir <= 0

longCandle  = useCandle and candleEntry and candleBull
shortCandle = useCandle and candleEntry and candleBear

//=====================================================================================================================
// 5) TIERED TP AND SL CALCULATION
//     S3 — `minProfitUsd` is the floor for TP1 in BOTH modes.
//=====================================================================================================================
f_targets(bool isLong, float px, float risk, float nearT, float bestT, float pp) =>
    float t1 = na
    float t2 = na
    float t3 = na
    if tpMode == 'Dollar targets'
        // the targets are a fixed amount of money, translated into points for this contract
        // S3: TP1 can never be worth less than the minimum profit, and TP2/TP3 stay above it
        float tp1Eff = math.max(tp1Usd, minProfitUsd)
        float tp2Eff = math.max(tp2Usd, tp1Eff)
        float tp3Eff = math.max(tp3Usd, tp2Eff)
        float d1 = pp > 0 ? tp1Eff / pp : risk * rr1
        float d2 = pp > 0 ? tp2Eff / pp : risk * rr2
        float d3 = pp > 0 ? tp3Eff / pp : risk * rr3
        t1 := isLong ? px + d1 : px - d1
        t2 := isLong ? px + d2 : px - d2
        t3 := isLong ? px + d3 : px - d3
        // if a real liquidity level sits right next to a target, use the level instead —
        // but never if that would drag TP1 under the minimum profit
        float snapDist = snapAtr * atr
        if snapDist > 0
            if not na(nearT) and math.abs(nearT - t1) <= snapDist
                float cand = nearT
                float candUsd = pp * (isLong ? cand - px : px - cand)
                if candUsd >= minProfitUsd
                    t1 := cand
                    t1
            if not na(bestT) and math.abs(bestT - t2) <= snapDist
                t2 := bestT
                t2
        // optional: never accept a target worse than the minimum R:R
        if useRFloor
            t1 := isLong ? math.max(t1, px + risk * rr1) : math.min(t1, px - risk * rr1)
            t2 := isLong ? math.max(t2, px + risk * rr2) : math.min(t2, px - risk * rr2)
            t3 := isLong ? math.max(t3, px + risk * rr3) : math.min(t3, px - risk * rr3)
            t3
    else   // classic mode: nearest liquidity, magnet by volume, ATR extension
        float strength = volBoost ? math.max(0.6, math.min(2.0, nz(volRatio, 1.0))) : 1.0
        float riskCash = f_usd(risk, pp)
        float needR1   = riskCash > 0 ? minProfitUsd / riskCash : rr1
        float minR1    = math.max(rr1, needR1)
        float minR2    = math.max(rr2, minR1 + 0.5)
        float minR3    = math.max(rr3, minR2 + 0.5)
        t1 := na(nearT) ? isLong ? px + risk * minR1 : px - risk * minR1 : nearT
        t1 := isLong ? math.max(t1, px + risk * minR1) : math.min(t1, px - risk * minR1)
        t2 := na(bestT) ? isLong ? px + risk * minR2 : px - risk * minR2 : bestT
        t2 := isLong ? math.max(t2, t1 + atr * 0.4, px + risk * minR2) : math.min(t2, t1 - atr * 0.4, px - risk * minR2)
        float ext = atr * tp3Ext * strength
        t3 := isLong ? math.max(t2 + ext, px + risk * minR3) : math.min(t2 - ext, px - risk * minR3)
        float capR = math.max(rrMax, minR1 + 1.0)
        t2 := isLong ? math.min(t2, px + risk * capR) : math.max(t2, px - risk * capR)
        t3 := isLong ? math.min(t3, px + risk * capR) : math.max(t3, px - risk * capR)
        t3
    // final guarantee: TP1 < TP2 < TP3 for a long, and the mirror for a short
    t2 := isLong ? math.max(t2, t1 + atr * 0.2) : math.min(t2, t1 - atr * 0.2)
    t3 := isLong ? math.max(t3, t2 + atr * 0.2) : math.min(t3, t2 - atr * 0.2)
    [t1, t2, t3]

// --- computed every bar (tuples cannot be declared inside local blocks)
float baseLo   = ta.lowest(low, slLook)
float baseHi   = ta.highest(high, slLook)
float rawStopL = math.min(baseLo, nz(sweepDnP, baseLo)) - atr * slBuf
float rawStopS = math.max(baseHi, nz(sweepUpP, baseHi)) + atr * slBuf
float riskLong  = math.max(close - rawStopL, atr * 0.15)
float riskShort = math.max(rawStopS - close, atr * 0.15)

// a Liquidity + FVG entry protects itself behind the gap
if longFvgSig and not na(fvgEntBtm)
    rawStopL := math.min(rawStopL, fvgEntBtm - atr * slBuf)
    rawStopL
if shortFvgSig and not na(fvgEntTop)
    rawStopS := math.max(rawStopS, fvgEntTop + atr * slBuf)
    rawStopS

riskLong  := math.max(close - rawStopL, atr * 0.15)
riskShort := math.max(rawStopS - close, atr * 0.15)

// nunca por debajo de un tick: un stop de distancia cero se llenaría al instante
riskLong  := math.max(riskLong,  syminfo.mintick)
riskShort := math.max(riskShort, syminfo.mintick)

//---------------------------------------------------------------------------------------------------------------------
// A1 — TAMAÑO POR RIESGO FIJO  (la diferencia grande con la versión de futuros)
// En futuros el tamaño era fijo y había que RECORTAR EL STOP para que la pérdida cupiera en $350. Eso metía el
// stop dentro del ruido y mataba operaciones que iban a ganar.
// Aquí es al revés: el stop se queda donde el mercado dice que tiene que estar, y lo que se adapta es cuántas
// acciones se compran. Arriesgas los mismos $350 en una acción de $8 que en una de $600, sin tocar el stop.
//---------------------------------------------------------------------------------------------------------------------
float shL = f_shares(riskLong,  close)
float shS = f_shares(riskShort, close)
float ppL = shL   // dólares por cada $1 de movimiento = número de acciones
float ppS = shS
// si ni con todo el presupuesto sale el mínimo de acciones, la señal no es ejecutable
bool sizeOKL = shL >= minSharesIn
bool sizeOKS = shS >= minSharesIn

float stopLong  = close - riskLong
float stopShort = close + riskShort

[lTp1, lTp2, lTp3] = f_targets(true,  close, riskLong,  upNear, upBest, ppL)
[sTp1, sTp2, sTp3] = f_targets(false, close, riskShort, dnNear, dnBest, ppS)

// --- S3: the minimum-profit gate. A signal whose TP1 is not worth `minProfitUsd` is not taken.
float tp1UsdLong  = ppL * (lTp1 - close)
float tp1UsdShort = ppS * (close - sTp1)
bool  profitOKL   = not enforceMinTp or tp1UsdLong  >= minProfitUsd
bool  profitOKS   = not enforceMinTp or tp1UsdShort >= minProfitUsd

longSig  = inSess and okStock and volOK and cool and freeSl and biasLongOK  and strLongOK  and profitOKL and sizeOKL and ((longSweep  or longBreak  or longBosHalf)  and cFiltLong  or longCandle  or longFvgSig)
shortSig = inSess and okStock and volOK and cool and freeSl and biasShortOK and strShortOK and profitOKS and sizeOKS and ((shortSweep or shortBreak or shortBosHalf) and cFiltShort or shortCandle or shortFvgSig)

// the signal is only released in the last seconds of the candle (see "Seconds before close")
longSig  := longSig  and timingOK
shortSig := shortSig and timingOK

// --- why a trigger did not become an entry: the dashboard shows the first gate that blocked it
bool anyTrigL = longSweep  or longBreak  or longBosHalf  or longCandle  or longFvgSig
bool anyTrigS = shortSweep or shortBreak or shortBosHalf or shortCandle or shortFvgSig
if anyTrigL and not longSig or anyTrigS and not shortSig
    string why = not inSess ? 'fuera de sesión' : not okPrice ? 'precio $' + str.tostring(close, '#.##') + ' fuera del rango permitido' : not okLiquid ? 'volumen $' + str.tostring(dollarVol / 1000000, '#.##') + 'M bajo el mínimo' : not okAtr ? 'ATR ' + str.tostring(atrPct, '#.##') + '% fuera de rango' : not freeSl ? 'a trade was already open' : not cool ? 'cooldown between signals' : not volOK ? 'volume x' + str.tostring(volRatio, '#.##') + ' under x' + str.tostring(minVolRatio, '#.##') : anyTrigL and not sizeOKL or anyTrigS and not sizeOKS ? 'riesgo $' + str.tostring(math.round((anyTrigL ? riskLong : riskShort))) + '/acción: no salen ni ' + str.tostring(minSharesIn) + ' acciones con $' + str.tostring(math.round(riskBudget)) : anyTrigL and not biasLongOK or anyTrigS and not biasShortOK ? 'swing bias against it' : anyTrigL and not strLongOK or anyTrigS and not strShortOK ? 'internal structure against it' : anyTrigL and not profitOKL and not anyTrigS or anyTrigS and not profitOKS and not anyTrigL ? 'TP1 worth $' + str.tostring(math.round(anyTrigL ? tp1UsdLong : tp1UsdShort)) + ' < $' + str.tostring(math.round(minProfitUsd)) + ' minimum' : anyTrigL and not cFiltLong or anyTrigS and not cFiltShort ? 'dominant candle against it' : not timingOK ? 'waiting for the last seconds' : 'zone filter'
    blockTxt := (anyTrigL ? 'LONG ' : 'SHORT ') + why
    blockTxt

// avoid opposite signals on the same candle
if longSig and shortSig
    longSig  := close > open
    shortSig := close <= open
    shortSig

//=====================================================================================================================
// 5.5) OPEN THE TRADE  —  internal bookkeeping + REAL strategy order
//=====================================================================================================================
if longSig or shortSig
    bool  isLong = longSig
    float ePx  = close
    float stop = isLong ? stopLong : stopShort
    float a    = isLong ? lTp1 : sTp1
    float b    = isLong ? lTp2 : sTp2
    float c    = isLong ? lTp3 : sTp3

    tDir      := isLong ? 1 : -1
    tEnt      := ePx
    tSl       := stop
    tRisk     := isLong ? riskLong : riskShort
    // A1: el tamaño se congela AQUÍ y gobierna todo el dinero de esta operación hasta que cierre
    tShares   := isLong ? shL : shS
    tPP       := tShares
    tRiskUsd  := f_usd(isLong ? riskLong : riskShort, tPP)
    tT1       := a
    tT2       := b
    tT3       := c
    mfe       := 0.0
    lastContR := 0.0
    logged    := false
    contLb.clear()
    h1 := false
    h2 := false
    h3 := false
    tBar      := bar_index
    tEntTime  := time
    lastSigBar := bar_index

    lastSrc := longFvgSig or shortFvgSig ? '#5 Liquidity + FVG' : longSweep or shortSweep ? '#1 Liquidity sweep' : longBosHalf or shortBosHalf ? '#3 Half body beyond BOS ' + f_p(bosLevelHit) : longBreak or shortBreak ? '#2 BOS/CHoCH displacement' : '#4 Candle dominance'
    tSrcTag := longFvgSig or shortFvgSig ? '#5 LIQ+FVG' : longSweep or shortSweep ? '#1 SWEEP' : longBosHalf or shortBosHalf ? '#3 BOS ½ BODY' : longBreak or shortBreak ? '#2 BOS/CHoCH' : '#4 CANDLE'
    tSrcIdx := longFvgSig or shortFvgSig ? 4 : longSweep or shortSweep ? 0 : longBosHalf or shortBosHalf ? 2 : longBreak or shortBreak ? 1 : 3
    tBosPx  := longBosHalf or shortBosHalf ? bosLevelHit : na
    blockTxt := '— none'

    if isLong
        cLong := cLong + 1
        cLong
    else
        cShort := cShort + 1
        cShort

    // A2: cuenta las entradas en las que mordió el freno de valor de posición o de nº de acciones, es decir,
    // aquellas en las que NO estás arriesgando el presupuesto completo porque la posición no cabía.
    // Un número alto aquí significa que tu riesgo real por operación es menor del que crees.
    if tShares * tRisk < riskBudget * 0.95
        cCapped := cCapped + 1
        cCapped

    // ---------------- S1/S2: THE REAL ORDER ----------------
    // With `process_orders_on_close = true` this market order fills at THIS candle's close, which is exactly the
    // price the internal engine books as `tEnt`. Backtest and dashboard therefore agree.
    strategy.entry(isLong ? ID_L : ID_S, isLong ? strategy.long : strategy.short, qty = tShares, comment = tSrcTag + ' ' + str.tostring(tShares, '#') + 'sh')

    if showTrade
        line.delete(lnE)
        line.delete(lnS)
        line.delete(ln1)
        line.delete(ln2)
        line.delete(ln3)
        label.delete(lbE)
        label.delete(lbS)
        label.delete(lb1)
        label.delete(lb2)
        label.delete(lb3)
        color dc = isLong ? bullCss : bearCss
        lnE := line.new(bar_index, ePx,  bar_index + 1, ePx,  color = color.new(#787b86, 0), style = line.style_solid, width = 1)
        lnS := line.new(bar_index, stop, bar_index + 1, stop, color = bearCss, style = line.style_dashed, width = 1)
        ln1 := line.new(bar_index, a, bar_index + 1, a, color = color.new(dc, 40), style = line.style_dotted, width = 1)
        ln2 := line.new(bar_index, b, bar_index + 1, b, color = color.new(dc, 20), style = line.style_dotted, width = 1)
        ln3 := line.new(bar_index, c, bar_index + 1, c, color = dc, style = line.style_dotted, width = 1)
        lbE := label.new(bar_index, ePx,  'ENTRY ' + f_p(ePx),  style = label.style_label_left, color = #00000000, textcolor = color.new(#787b86, 0), size = size.tiny)
        lbS := label.new(bar_index, stop, 'SL ' + f_p(stop),    style = label.style_label_left, color = #00000000, textcolor = bearCss, size = size.tiny)
        lb1 := label.new(bar_index, a, 'TP1 ' + f_p(a) + '  $' + str.tostring(math.round(tPP * math.abs(a - ePx))), style = label.style_label_left, color = #00000000, textcolor = dc, size = size.tiny)
        lb2 := label.new(bar_index, b, 'TP2 ' + f_p(b), style = label.style_label_left, color = #00000000, textcolor = dc, size = size.tiny)
        lb3 := label.new(bar_index, c, 'TP3 ' + f_p(c), style = label.style_label_left, color = #00000000, textcolor = dc, size = size.tiny)
        lb3

    alert((isLong ? '🟢 LONG ' : '🔴 SHORT ') + syminfo.ticker + ' ' + timeframe.period + '  ·  ' + lastSrc + '\nEntry: ' + f_p(ePx) + '  |  SL: ' + f_p(stop) + '  (' + str.tostring(tShares, '#') + ' acciones · riesgo $' + str.tostring(math.round(tRiskUsd)) + ', tope $' + str.tostring(math.round(panicEff)) + ')' + '\nTP1: ' + f_p(a) + '  TP2: ' + f_p(b) + '  TP3: ' + f_p(c) + '\nTP1 = $' + str.tostring(math.round(tPP * math.abs(a - ePx))) + ' (min $' + str.tostring(math.round(minProfitUsd)) + ')' + '\nVol x' + str.tostring(volRatio, '#.##') + '  |  Liq risk ↑' + str.tostring(math.round(riskUp)) + ' ↓' + str.tostring(math.round(riskDn)), alert.freq_once_per_bar)

    // ── al registro: apertura
    if whOn and whKey != ''
        alert('{"k":"' + whKey + '","ev":"open","id":"' + f_id() + '"' +
              ',"sym":"' + syminfo.ticker + '","tf":"' + timeframe.period + '","tag":"' + whTag + '"' +
              ',"dir":"' + (isLong ? 'long' : 'short') + '","setup":"' + tSrcTag + '","mode":"' + mode + '"' +
              ',"entry":' + f_num(ePx) + ',"sl":' + f_num(stop) +
              ',"tp1":' + f_num(a) + ',"tp2":' + f_num(b) + ',"tp3":' + f_num(c) +
              ',"riskPts":' + f_num(tRisk) + ',"riskUsd":' + f_num(tRiskUsd) +
              ',"vol":' + f_num(volRatio) + ',"atr":' + f_num(atr) +
              ',"liqUp":' + f_num(upNear) + ',"liqDn":' + f_num(dnNear) +
              ',"prov":' + (earlySignal ? 'true' : 'false') + '}', alert.freq_once_per_bar)

//=====================================================================================================================
// 6) ACTIVE TRADE MANAGEMENT
//=====================================================================================================================
float openR   = tDir != 0 and not na(tRisk) and tRisk > 0 ? tDir == 1 ? (close - tEnt) / tRisk : (tEnt - close) / tRisk : float(na)
float openPts = tDir != 0 and not na(tEnt) ? tDir == 1 ? close - tEnt : tEnt - close : float(na)
float openUsd = na(openR) or na(tRiskUsd) ? float(na) : openR * tRiskUsd

bool  contBar   = false
bool  slMoved   = false
bool  hitTp1    = false
bool  hitTp2    = false
bool  hitTp3    = false
bool  hitSl     = false
bool  timedOut  = false
bool  panicOut  = false
bool  forcedOut = false
bool  allOut    = false
bool  closed    = false
bool  cancelled = false
bool  newLog    = false
bool  ambiguous = false
float lastR     = 0.0
float lastUsd   = 0.0
int   lastTp    = 0

if tDir != 0 and not na(tBar) and bar_index > tBar
    // maximum favorable excursion (the entry bar is skipped: the trade opens at its close)
    mfe := tDir == 1 ? math.max(mfe, high - tEnt) : math.max(mfe, tEnt - low)

    contBar := showCont and not na(openR) and openR >= lastContR + contStep and (tDir == 1 ? close > open : close < open)
    if contBar
        lastContR := openR
        label contMark = label.new(bar_index, high + atr * 0.7, (tDir == 1 ? '▲ LONG CONTINUATION  +' : '▼ SHORT CONTINUATION  +') + str.tostring(openR, '#.#') + 'R', style = label.style_label_down, color = #131722, textcolor = tDir == 1 ? markBull : markBear, size = size.small)
        contLb.push(contMark)
        f_keepLb(contMark)
        while contLb.size() > contKeep
            label.delete(contLb.shift())
        alert((tDir == 1 ? '📈 LONG' : '📉 SHORT') + ' continuation on ' + syminfo.ticker + ': +' + str.tostring(openR, '#.##') + 'R in favour', alert.freq_once_per_bar)

    // FIX E: a single candle containing BOTH TP3 and the stop is ambiguous
    if tDir == 1
        bool tp3Hit = high >= tT3
        bool slHit  = low <= tSl
        ambiguous := tp3Hit and slHit
        if not h1 and high >= tT1
            h1 := true
            hitTp1 := true
            hitTp1
        if not h2 and high >= tT2
            h2 := true
            hitTp2 := true
            hitTp2
        if tp3Hit and (not slHit or not worstCase)
            h3 := true
            hitTp3 := true
            tDir := 0
            closed := true
            closed
        else if slHit
            hitSl := true
            tDir := 0
            closed := true
            closed
    else
        bool tp3Hit = low <= tT3
        bool slHit  = high >= tSl
        ambiguous := tp3Hit and slHit
        if not h1 and low <= tT1
            h1 := true
            hitTp1 := true
            hitTp1
        if not h2 and low <= tT2
            h2 := true
            hitTp2 := true
            hitTp2
        if tp3Hit and (not slHit or not worstCase)
            h3 := true
            hitTp3 := true
            tDir := 0
            closed := true
            closed
        else if slHit
            hitSl := true
            tDir := 0
            closed := true
            closed

    //-----------------------------------------------------------------------------------------------------------------
    // R7 — TRAMOS AGOTADOS: la operación está cerrada de verdad, aunque no haya tocado TP3 ni el stop.
    // Esto importa cuando NO se usan los tres tramos. Con un único contrato de NQ la configuración sana es
    // sólo TP1 y TP2 con porcentaje, o sólo TP1. Sin esta comprobación el motor interno
    // seguiría "gestionando" una operación que en el mercado ya está plana, y como `freeSl` exige tDir == 0,
    // bloquearía todas las señales siguientes hasta que el precio tocase TP3 o el stop. El panel y el
    // Strategy Tester contarían cosas distintas.
    //-----------------------------------------------------------------------------------------------------------------
    if tDir != 0
        float wLeft = (h1 ? 0.0 : w1) + (h2 ? 0.0 : w2) + (h3 ? 0.0 : w3)
        if wLeft <= 0
            allOut := true
            closed := true
            tDir   := 0
            tDir

    //-----------------------------------------------------------------------------------------------------------------
    // R3 — RED DE SEGURIDAD DEL TOPE DE PÉRDIDA
    // El stop de R1/R2 ya vive en el mercado, así que en condiciones normales esto no llega a dispararse nunca.
    // Existe para el caso feo: un hueco de apertura que salta por encima del stop, o un stop que por lo que sea
    // no llenó. Si al cerrar la vela la posición está en -$350 o peor, se cierra a mercado y punto.
    //-----------------------------------------------------------------------------------------------------------------
    if tDir != 0 and not na(tEnt) and tPP > 0
        float openLossUsd = (tDir == 1 ? tEnt - close : close - tEnt) * tPP
        if openLossUsd >= panicEff
            panicOut := true
            closed   := true
            tDir     := 0
            cPanic   := cPanic + 1
            cPanic

    //-----------------------------------------------------------------------------------------------------------------
    // Trail the stop into profit.  S3: the stop NEVER secures less than the minimum profit.
    //-----------------------------------------------------------------------------------------------------------------
    if trailOn and tDir != 0 and not na(tEnt)
        float trailTo = na
        // 1 - TP1 touched: lock in at least the minimum profit
        if h1 and tPP > 0
            float lockUsd1 = math.max(trailBE, minProfitUsd)
            float lockPts  = lockUsd1 / tPP
            trailTo := tEnt + (tDir == 1 ? lockPts : -lockPts)
            trailTo
        // 2 - TP2 touched: the stop cannot be worse than TP1
        if h2 and not na(tT1)
            trailTo := na(trailTo) ? tT1 : tDir == 1 ? math.max(trailTo, tT1) : math.min(trailTo, tT1)
            trailTo
        // 3 - the stop follows the best profit reached, minus a cushion, in clean steps.
        //     It does not engage until the resulting lock is at least the minimum profit, so it can never
        //     secure $100 when the rule of the strategy says $150.
        if trailFollow and tPP > 0
            float mfeUsd = mfe * tPP
            if mfeUsd >= minProfitUsd + trailGap
                float rawUsd  = math.max(0.0, mfeUsd - trailGap)
                float lockUsd = trailStep > 0 ? math.floor(rawUsd / trailStep) * trailStep : rawUsd
                lockUsd := math.max(lockUsd, minProfitUsd)
                float lockPt = lockUsd / tPP
                float follow = tEnt + (tDir == 1 ? lockPt : -lockPt)
                trailTo := na(trailTo) ? follow : tDir == 1 ? math.max(trailTo, follow) : math.min(trailTo, follow)
                trailTo
        if not na(trailTo)
            float newSl = tDir == 1 ? math.max(tSl, trailTo) : math.min(tSl, trailTo)
            if newSl != tSl
                tSl := newSl
                slMoved := true
                slMoved

    //-----------------------------------------------------------------------------------------------------------------
    // As soon as TP1 is touched the trade already counts as a winner
    //-----------------------------------------------------------------------------------------------------------------
    if logAtTp and (hitTp1 or hitTp2) and not na(tRisk) and tRisk > 0
        int   tSide = tT1 > tEnt ? 1 : -1
        int   tpNow = h2 ? 2 : 1
        float pxNow = h2 ? tT2 : tT1
        float rNow  = (tSide == 1 ? pxNow - tEnt : tEnt - pxNow) / tRisk
        float uNow  = rNow * nz(tRiskUsd)
        if not logged
            logArr.unshift(tlog.new(t = tEntTime, dir = tSide, ent = tEnt, ex = pxNow, r = rNow, usd = uNow, tp = tpNow, res = 0, src = tSrcTag))
            logged := true
            newLog := true
            if logArr.size() > 60
                logArr.pop()
        else if logArr.size() > 0
            tlog eR = logArr.get(0)
            eR.ex  := pxNow
            eR.r   := rNow
            eR.usd := uNow
            eR.tp  := tpNow
            eR.tp

    //-----------------------------------------------------------------------------------------------------------------
    // A4 — HUECO DE APERTURA Y CIERRE FORZOSO DE SESIÓN
    // La debilidad estructural de operar acciones con un tope de pérdida, y no la puede arreglar ningún stop.
    // Una acción abre con hueco: si abre por debajo de tu stop la orden se ejecuta EN LA APERTURA, no en tu nivel,
    // y pierdes más de lo que pediste. Lo único que se puede hacer es detectarlo en la primera vela y salir sin
    // esperar, en vez de dejar la posición corriendo. Va aquí arriba, antes del bloque de registro, para que
    // estas salidas se apunten en el log como cualquier otra.
    //-----------------------------------------------------------------------------------------------------------------
    if tDir != 0 and not na(tSl)
        bool gapThrough = gapGuard and newSession and (tDir == 1 ? open < tSl : open > tSl)
        if gapThrough or eodNow
            if gapThrough
                cGap := cGap + 1
                cGap
            forcedOut := true
            closed    := true
            tDir      := 0
            tDir

    //-----------------------------------------------------------------------------------------------------------------
    // Timeout
    //-----------------------------------------------------------------------------------------------------------------
    if useTimeout and tDir != 0 and not na(tBar) and bar_index - tBar >= maxBars
        timedOut := true
        closed   := true
        tDir     := 0
        cTimeout := cTimeout + 1
        cTimeout

    //-----------------------------------------------------------------------------------------------------------------
    // No-progress cancellation.  FIX C: it is a real closed trade in every statistic.
    //-----------------------------------------------------------------------------------------------------------------
    if useCancel and tDir != 0 and not h1 and not na(tBar) and not na(tRisk) and bar_index - tBar >= cancelBars and mfe < cancelR * tRisk
        cancelled := true
        int   cSide = tDir
        float rC = (tDir == 1 ? close - tEnt : tEnt - close) / tRisk
        float uC = rC * nz(tRiskUsd)
        sumR    := sumR + rC
        sumUsd  := sumUsd + uC
        cCancel := cCancel + 1
        if rC > 0
            cWin := cWin + 1
            srcW.set(tSrcIdx, srcW.get(tSrcIdx) + 1)
        else
            cLoss := cLoss + 1
            cLoss
        if uC >= minProfitUsd
            c1k := c1k + 1
            c1k
        if rearmLvl and not na(tBosPx) and bosArr.size() > 0
            for bi = 0 to bosArr.size() - 1 by 1
                bosLvl br = bosArr.get(bi)
                if br.used and br.price == tBosPx
                    br.used := false
                    br.used
        srcN.set(tSrcIdx, srcN.get(tSrcIdx) + 1)
        srcC.set(tSrcIdx, srcC.get(tSrcIdx) + 1)
        srcR.set(tSrcIdx, srcR.get(tSrcIdx) + rC)
        srcU.set(tSrcIdx, srcU.get(tSrcIdx) + uC)
        logArr.unshift(tlog.new(t = tEntTime, dir = cSide, ent = tEnt, ex = close, r = rC, usd = uC, tp = 0, res = 2, src = tSrcTag))
        newLog  := true
        lastR   := rC
        lastUsd := uC
        lastTp  := 0
        // ── al registro: cancelada. Cuenta como operación cerrada, no se pierde del histórico.
        if whOn and whKey != ''
            alert('{"k":"' + whKey + '","ev":"close","id":"' + f_id() + '"' +
                  ',"sym":"' + syminfo.ticker + '","tag":"' + whTag + '","setup":"' + tSrcTag + '"' +
                  ',"dir":"' + (cSide == 1 ? 'long' : 'short') + '"' +
                  ',"t":' + str.tostring(time) + ',"exit":' + f_num(close) +
                  ',"r":' + f_num(rC) + ',"usd":' + f_num(uC) + ',"tp":0,"res":"cancel"' +
                  ',"win":' + (rC > 0 ? 'true' : 'false') +
                  ',"bars":' + str.tostring(bar_index - tBar) + ',"mfePts":' + f_num(mfe) + '}',
                  alert.freq_once_per_bar)
        if logArr.size() > 60
            logArr.pop()
        if showTrade and cancelNotice
            float yTop = high + atr * cancelOffset
            if keepLatest
                line.delete(cxlLn)
                label.delete(cxlArw)
                label.delete(cxlTxt)
            cxlLn  := line.new(bar_index, high + atr * 0.20, bar_index, yTop - atr * 0.25, xloc = xloc.bar_index, color = warnCss, style = line.style_dotted, width = 1)
            cxlArw := label.new(bar_index, yTop - atr * 0.25, '', style = label.style_arrowdown, color = warnCss, textcolor = warnCss, size = f_size(arrowSize))
            cxlTxt := label.new(bar_index, yTop, '⚪ ORDER CANCELLED\n' + (cSide == 1 ? 'LONG' : 'SHORT') + '  ' + f_p(tEnt) + '\n' + str.format_time(time, 'HH:mm:ss', tzUse), style = label.style_label_down, color = #131722, textcolor = warnCss, size = size.small)
            f_keepLn(cxlLn)
            f_keepLb(cxlArw)
            f_keepLb(cxlTxt)
        tDir := 0
        tDir

    //-----------------------------------------------------------------------------------------------------------------
    // Log on close.  FIX D: the minimum-profit filter is applied again here.
    //-----------------------------------------------------------------------------------------------------------------
    if closed and not na(tRisk) and tRisk > 0
        int   tSide  = tT1 > tEnt ? 1 : -1
        float exPx   = h3 ? tT3 : allOut ? (h2 ? tT2 : tT1) : timedOut or panicOut or forcedOut ? close : tSl
        float rFinal = (tSide == 1 ? exPx - tEnt : tEnt - exPx) / tRisk
        float usdF   = rFinal * nz(tRiskUsd)
        int   tpF    = h3 ? 3 : h2 ? 2 : h1 ? 1 : 0
        bool  win    = rFinal > 0
        bool  keepRow = win ? not logWinMin or usdF >= minProfitUsd : true
        sumR   := sumR + rFinal
        sumUsd := sumUsd + usdF
        if win
            cWin := cWin + 1
            cWin
        else
            cLoss := cLoss + 1
            cLoss
        if usdF >= minProfitUsd
            c1k := c1k + 1
            c1k
        srcN.set(tSrcIdx, srcN.get(tSrcIdx) + 1)
        srcR.set(tSrcIdx, srcR.get(tSrcIdx) + rFinal)
        srcU.set(tSrcIdx, srcU.get(tSrcIdx) + usdF)
        if win
            srcW.set(tSrcIdx, srcW.get(tSrcIdx) + 1)
        lastR   := rFinal
        lastUsd := usdF
        lastTp  := tpF
        // ── al registro: cierre. Mismo id que la apertura, así la app fusiona las dos mitades.
        if whOn and whKey != ''
            alert('{"k":"' + whKey + '","ev":"close","id":"' + f_id() + '"' +
                  ',"sym":"' + syminfo.ticker + '","tag":"' + whTag + '","setup":"' + tSrcTag + '"' +
                  ',"dir":"' + (tSide == 1 ? 'long' : 'short') + '"' +
                  ',"t":' + str.tostring(time) + ',"exit":' + f_num(exPx) +
                  ',"r":' + f_num(rFinal) + ',"usd":' + f_num(usdF) + ',"tp":' + str.tostring(tpF) +
                  ',"res":"' + (h3 ? 'tp3' : allOut ? 'tp' + str.tostring(tpF) : forcedOut ? 'forced' : panicOut ? 'panic' : timedOut ? 'timeout' : 'sl') + '"' +
                  ',"win":' + (win ? 'true' : 'false') +
                  ',"bars":' + str.tostring(bar_index - tBar) + ',"mfePts":' + f_num(mfe) + '}',
                  alert.freq_once_per_bar)
        if logged and logArr.size() > 0
            if keepRow
                tlog eC = logArr.get(0)
                eC.ex  := exPx
                eC.r   := rFinal
                eC.usd := usdF
                eC.tp  := tpF
                eC.res := win ? 1 : -1
                eC.res
            else
                logArr.remove(0)
        else if keepRow
            logArr.unshift(tlog.new(t = tEntTime, dir = tSide, ent = tEnt, ex = exPx, r = rFinal, usd = usdF, tp = tpF, res = win ? 1 : -1, src = tSrcTag))
            newLog := true
            if logArr.size() > 60
                logArr.pop()

    if showTrade and not na(lnE) and not na(lbE)
        int x2 = bar_index + (tDir == 0 ? 0 : rightPad)
        line.set_x2(lnE, x2)
        line.set_x2(lnS, x2)
        line.set_x2(ln1, x2)
        line.set_x2(ln2, x2)
        line.set_x2(ln3, x2)
        label.set_x(lbE, x2)
        label.set_x(lbS, x2)
        label.set_x(lb1, x2)
        label.set_x(lb2, x2)
        label.set_x(lb3, x2)
        bool slUp = tDir == 1 ? tSl > tEnt : tSl < tEnt
        line.set_y1(lnS, tSl)
        line.set_y2(lnS, tSl)
        label.set_y(lbS, tSl)
        label.set_text(lbS, (slUp ? 'SL ▲ ' : 'SL ') + f_p(tSl) + (slUp ? '  +$' + str.tostring(math.round(tPP * math.abs(tSl - tEnt))) : ''))
        line.set_color(lnS, slUp ? bullCss : bearCss)
        label.set_textcolor(lbS, slUp ? bullCss : bearCss)
        if h1
            label.set_text(lb1, 'TP1 ✔ ' + f_p(tT1))
        if h2
            label.set_text(lb2, 'TP2 ✔ ' + f_p(tT2))
        if hitTp3
            label.set_text(lb3, 'TP3 ✔ ' + f_p(tT3))
        if hitSl
            label.set_text(lbS, 'SL ✖ ' + f_p(tSl))

    // A resolved trade leaves the chart completely.  FIX H: every handle back to `na`.
    if closed and clearOnClose or cancelled and cancelWipe
        line.delete(lnE)
        line.delete(lnS)
        line.delete(ln1)
        line.delete(ln2)
        line.delete(ln3)
        label.delete(lbE)
        label.delete(lbS)
        label.delete(lb1)
        label.delete(lb2)
        label.delete(lb3)
        while sigLnArr.size() > 0
            line.delete(sigLnArr.shift())
        while sigLbArr.size() > 0
            label.delete(sigLbArr.shift())
        if contLb.size() > 0
            for i = 0 to contLb.size() - 1 by 1
                label.delete(contLb.get(i))
        contLb.clear()
        lnE := na
        lnS := na
        ln1 := na
        ln2 := na
        ln3 := na
        lbE := na
        lbS := na
        lb1 := na
        lb2 := na
        lb3 := na
        sigLn  := na
        sigArw := na
        sigTxt := na
        sigTxt

//=====================================================================================================================
// 6.5) S2 — SYNCHRONISE THE REAL ORDERS WITH THE MANAGED LEVELS
//      Three partial exits, each with the CURRENT (possibly trailed) stop attached.
//      Re-issuing an exit with the same id updates its levels; the tranche that already filled is not re-sent.
//=====================================================================================================================
// NOTE: the guard is `tDir`, not `strategy.position_size`. With `process_orders_on_close` the position is still
// reported as flat while the script runs on the entry candle, so guarding on position_size would delay the
// protective bracket by one candle and leave that candle naked. Guarding on the internal direction attaches the
// bracket to the entry order in the same script run, which is the standard Pine pattern.
// A3 — reparto de las acciones entre los tres tramos.
// El resto de la división entera se acumula en el último tramo activo, así la posición sale ENTERA.
// Sin esto, con 137 acciones al 40/35/25 saldrían 54+47+34 = 135 y quedarían 2 colgadas para siempre,
// bloqueando todas las señales posteriores.
float qA = math.floor(tShares * w1)
float qB = math.floor(tShares * w2)
float qC = math.max(0.0, tShares - qA - qB)
if w3 <= 0
    if w2 > 0
        qB := qB + qC
    else
        qA := qA + qC
    qC := 0.0

if tDir == 1 and not na(tSl)
    if not h1 and qA > 0
        strategy.exit(XL1, ID_L, qty = qA, limit = tT1, stop = tSl, comment_profit = 'TP1', comment_loss = 'SL')
    if not h2 and qB > 0
        strategy.exit(XL2, ID_L, qty = qB, limit = tT2, stop = tSl, comment_profit = 'TP2', comment_loss = 'SL')
    if not h3 and qC > 0
        strategy.exit(XL3, ID_L, qty = qC, limit = tT3, stop = tSl, comment_profit = 'TP3', comment_loss = 'SL')

if tDir == -1 and not na(tSl)
    if not h1 and qA > 0
        strategy.exit(XS1, ID_S, qty = qA, limit = tT1, stop = tSl, comment_profit = 'TP1', comment_loss = 'SL')
    if not h2 and qB > 0
        strategy.exit(XS2, ID_S, qty = qB, limit = tT2, stop = tSl, comment_profit = 'TP2', comment_loss = 'SL')
    if not h3 and qC > 0
        strategy.exit(XS3, ID_S, qty = qC, limit = tT3, stop = tSl, comment_profit = 'TP3', comment_loss = 'SL')

// timeout, cancellation y el tope de pérdida cierran también la posición REAL (al cierre de esta vela)
if (timedOut or cancelled or panicOut or forcedOut) and strategy.position_size != 0
    strategy.close_all(comment = forcedOut ? (eodNow ? 'EOD' : 'GAP') : panicOut ? 'MAX LOSS' : timedOut ? 'TIMEOUT' : 'CANCELLED')

// safety net: if the internal engine says flat but the strategy still holds something, flatten it
if tDir == 0 and strategy.position_size != 0 and not timedOut and not cancelled and not panicOut and not forcedOut
    strategy.close_all(comment = 'SYNC')

//=====================================================================================================================
// 6.6) ALERTS
//=====================================================================================================================
if hitTp1 and not (whOn and whMute)
    alert('🎯 TP1 reached ' + syminfo.ticker + ' @ ' + f_p(tT1), alert.freq_once_per_bar)
if hitTp2 and not (whOn and whMute)
    alert('🎯 TP2 reached ' + syminfo.ticker + ' @ ' + f_p(tT2), alert.freq_once_per_bar)
if hitTp3 and not (whOn and whMute)
    alert('🏁 TP3 reached ' + syminfo.ticker + ' @ ' + f_p(tT3), alert.freq_once_per_bar)
if hitSl and not (whOn and whMute)
    alert('🛑 SL hit ' + syminfo.ticker + ' @ ' + f_p(tSl) + (ambiguous ? '  (ambiguous candle: TP3 and SL in the same bar)' : ''), alert.freq_once_per_bar)
if forcedOut and not (whOn and whMute)
    alert((cGap > nz(cGap[1]) ? '🕳️ HUECO en ' : '🔔 Cierre de sesión en ') + syminfo.ticker + ': salida a mercado en ' + f_p(close) + ' (stop estaba en ' + f_p(tSl) + ')', alert.freq_once_per_bar)
if panicOut and not (whOn and whMute)
    alert('⛔ TOPE DE PÉRDIDA en ' + syminfo.ticker + ': la operación llegó a -$' + str.tostring(math.round(panicEff)) + ' y se cerró a mercado en ' + f_p(close), alert.freq_once_per_bar)
if timedOut and not (whOn and whMute)
    alert('⏳ Trade closed at market on ' + syminfo.ticker + ' after ' + str.tostring(maxBars) + ' bars without resolving', alert.freq_once_per_bar)
if cancelled and not (whOn and whMute)
    alert('⚪ Order cancelled on ' + syminfo.ticker + ': no progress after ' + str.tostring(cancelBars) + ' bars', alert.freq_once_per_bar)
if slMoved and not (whOn and whMute)
    alert('🔒 Stop moved on ' + syminfo.ticker + ' to ' + f_p(tSl) + ' — the trade can no longer lose', alert.freq_once_per_bar)
if newLog and not (whOn and whMute)
    alert('📒 Trade logged on ' + syminfo.ticker + ': ' + (lastTp > 0 ? 'TP' + str.tostring(lastTp) : cancelled ? 'cancelled' : 'stop loss') + ', ' + str.tostring(lastR, '#.##') + 'R = $' + str.tostring(math.round(lastUsd)), alert.freq_once_per_bar)
if warnUp and not (whOn and whMute)
    alert('⚠️ Liquidity approaching ABOVE on ' + syminfo.ticker + ' → ' + f_p(upNear) + ' (risk ' + str.tostring(math.round(riskUp)) + '/100)', alert.freq_once_per_bar)
if warnDn and not (whOn and whMute)
    alert('⚠️ Liquidity approaching BELOW on ' + syminfo.ticker + ' → ' + f_p(dnNear) + ' (risk ' + str.tostring(math.round(riskDn)) + '/100)', alert.freq_once_per_bar)

//=====================================================================================================================
// 7) CHART DRAWING
//=====================================================================================================================
bool shpLong  = showShapes and longSig
bool shpShort = showShapes and shortSig
plotshape(shpLong  and shapeSize == 'Tiny',   'ENTRY LONG',         shape.triangleup,   location.belowbar, markBull, size = size.tiny)
plotshape(shpLong  and shapeSize == 'Small',  'ENTRY LONG small',   shape.triangleup,   location.belowbar, markBull, size = size.small)
plotshape(shpLong  and shapeSize == 'Normal', 'ENTRY LONG normal',  shape.triangleup,   location.belowbar, markBull, size = size.normal)
plotshape(shpLong  and shapeSize == 'Large',  'ENTRY LONG large',   shape.triangleup,   location.belowbar, markBull, size = size.large)
plotshape(shpShort and shapeSize == 'Tiny',   'ENTRY SHORT',        shape.triangledown, location.abovebar, markBear, size = size.tiny)
plotshape(shpShort and shapeSize == 'Small',  'ENTRY SHORT small',  shape.triangledown, location.abovebar, markBear, size = size.small)
plotshape(shpShort and shapeSize == 'Normal', 'ENTRY SHORT normal', shape.triangledown, location.abovebar, markBear, size = size.normal)
plotshape(shpShort and shapeSize == 'Large',  'ENTRY SHORT large',  shape.triangledown, location.abovebar, markBear, size = size.large)

if longSig or shortSig
    color sigCss = longSig ? markBull : markBear
    float ySig = high + atr * signalOffset
    sigLn  := line.new(bar_index, high + atr * 0.18, bar_index, ySig - atr * 0.22, xloc = xloc.bar_index, color = sigCss, style = line.style_dotted, width = 2)
    sigArw := label.new(bar_index, ySig - atr * 0.22, '', style = label.style_arrowdown, color = sigCss, textcolor = sigCss, size = f_size(arrowSize))
    sigTxt := label.new(bar_index, ySig, (longSig ? '▲ LONG  ' : '▼ SHORT  ') + f_p(close) + '\n' + str.format_time(time, 'HH:mm:ss', tzUse) + '\n' + tSrcTag, style = label.style_label_down, color = #131722, textcolor = sigCss, size = size.normal)
    sigLnArr.push(sigLn)
    sigLbArr.push(sigArw)
    sigLbArr.push(sigTxt)
    while sigLnArr.size() > sigKeep
        line.delete(sigLnArr.shift())
    while sigLbArr.size() > sigKeep * 2
        label.delete(sigLbArr.shift())

// Live profit line
if showPnl and tDir != 0 and not na(openUsd)
    color pnlCss = pnlColor
    if na(pnlLn)
        pnlLn := line.new(bar_index, close, bar_index, close, xloc = xloc.bar_index, style = line.style_dashed, width = 1)
        pnlLb := label.new(bar_index, close, '', style = label.style_label_left, color = #131722, size = size.normal)
        label.set_textcolor(pnlLb, pnlColor)
    line.set_xy1(pnlLn, nz(tBar, bar_index), close)
    line.set_xy2(pnlLn, bar_index + pnlOffset, close)
    line.set_color(pnlLn, pnlCss)
    line.set_width(pnlLn, 2)
    label.set_xy(pnlLb, bar_index + pnlOffset, close + atr * pnlGap)
    label.set_text(pnlLb, (openUsd >= 0 ? '+$' : '-$') + str.tostring(math.round(math.abs(openUsd))) + '     ' + str.tostring(openPts, '#.##') + ' pts     ' + str.tostring(openR, '#.##') + 'R')
    label.set_textcolor(pnlLb, pnlCss)

if tDir == 0 and not na(pnlLn)
    line.delete(pnlLn)
    label.delete(pnlLb)
    pnlLn := na
    pnlLb := na
    pnlLb

// Live countdown above the running candle
var label clockLb = na
if showClock and barstate.islast
    if na(clockLb)
        clockLb := label.new(bar_index, high, '', style = label.style_none, textcolor = clockCss, size = f_size(clockSize))
        clockLb
    label.set_xy(clockLb, bar_index, high + atr * clockOffset)
    label.set_text(clockLb, str.tostring(math.round(secsLeft)) + 's')
    label.set_textcolor(clockLb, secsLeft <= earlySecs ? clockAlert : clockCss)
    label.set_size(clockLb, f_size(clockSize))

plotshape(showWarn and warnUp, 'Liquidity warning above', shape.xcross, location.abovebar, warnCss, size = size.tiny)
plotshape(showWarn and warnDn, 'Liquidity warning below', shape.xcross, location.belowbar, warnCss, size = size.tiny)

color domCss   = showCandle and candleDir == 1 ? color.new(bullCss, 0) : showCandle and candleDir == -1 ? color.new(bearCss, 0) : na
color trendCss = colorBars ? iTrend == 1 ? color.new(bullCss, 55) : iTrend == -1 ? color.new(bearCss, 55) : na : na
barcolor(na(domCss) ? trendCss : domCss)
plotchar(showDots and useCandle and candleDir == 1 and not longSig,   'Bullish dominant candle', '•', location.belowbar, color.new(bullCss, 40), size = size.tiny)
plotchar(showDots and useCandle and candleDir == -1 and not shortSig, 'Bearish dominant candle', '•', location.abovebar, color.new(bearCss, 40), size = size.tiny)

plot(tDir != 0 ? tEnt : na, 'Active entry', color = color.new(#787b86, 40), display = display.data_window)
plot(tDir != 0 ? tSl : na, 'Active SL', color = color.new(bearCss, 40), display = display.data_window)
plot(riskUp, 'Liquidation risk ↑', display = display.data_window)
plot(riskDn, 'Liquidation risk ↓', display = display.data_window)
plot(volRatio, 'Volume ratio', display = display.data_window)

if useCleanup
    int cutoffT = time - cleanupMin * 60000
    if tmpLb.size() > 0
        for i = tmpLb.size() - 1 to 0 by 1
            if tmpLbT.get(i) < cutoffT
                label.delete(tmpLb.get(i))
                tmpLb.remove(i)
                tmpLbT.remove(i)
    if tmpLn.size() > 0
        for i = tmpLn.size() - 1 to 0 by 1
            if tmpLnT.get(i) < cutoffT
                line.delete(tmpLn.get(i))
                tmpLn.remove(i)
                tmpLnT.remove(i)

//=====================================================================================================================
// 7.5) PDH / PDL — MÁXIMO Y MÍNIMO DEL DÍA ANTERIOR
// Sólo dibujo. No entra en el motor de señales, no toca los objetivos y no filtra nada.
//
// El `[1]` junto con `lookahead_on` es el patrón correcto para pedir un dato de temporalidad superior sin
// repintar: se pide el valor de la vela diaria YA CERRADA, así que en el histórico y en vivo se ve lo mismo.
// Usar `lookahead_on` sin el `[1]` sería hacer trampa — el script vería el máximo del día en curso antes de que
// el día termine y el backtest saldría precioso y falso.
//=====================================================================================================================
[pdhRaw, pdlRaw] = request.security(syminfo.tickerid, 'D', [high[1], low[1]], lookahead = barmerge.lookahead_on)

pdSty = switch pdStyleIn
    'Sólida'      => line.style_solid
    'Discontinua' => line.style_dashed
    =>               line.style_dotted

var line  pdhLn = na
var line  pdlLn = na
var label pdhLb = na
var label pdlLb = na
var int   pdStartBar = 0

bool pdNewDay = ta.change(time('D')) != 0
if pdNewDay
    pdStartBar := bar_index
    if pdKeep
        // soltamos los manejadores sin borrar los objetos: las líneas del día anterior se quedan congeladas
        // donde estaban y el bloque de abajo crea un par nuevo para el día que empieza.
        pdhLn := na
        pdlLn := na
        pdhLb := na
        pdlLb := na
        pdlLb

if showPD and not na(pdhRaw) and not na(pdlRaw)
    if na(pdhLn)
        pdhLn := line.new(pdStartBar, pdhRaw, bar_index, pdhRaw, color = pdhCss, style = pdSty, width = pdWidth)
        pdlLn := line.new(pdStartBar, pdlRaw, bar_index, pdlRaw, color = pdlCss, style = pdSty, width = pdWidth)
        if pdLabels
            pdhLb := label.new(bar_index, pdhRaw, '', style = label.style_label_left, color = #00000000, textcolor = pdhCss, size = size.small)
            pdlLb := label.new(bar_index, pdlRaw, '', style = label.style_label_left, color = #00000000, textcolor = pdlCss, size = size.small)
            pdlLb
    int pdRight = bar_index + pdBackPad
    line.set_xy1(pdhLn, pdStartBar, pdhRaw)
    line.set_xy2(pdhLn, pdRight, pdhRaw)
    line.set_xy1(pdlLn, pdStartBar, pdlRaw)
    line.set_xy2(pdlLn, pdRight, pdlRaw)
    if pdLabels and not na(pdhLb)
        label.set_xy(pdhLb, pdRight, pdhRaw)
        label.set_text(pdhLb, 'PDH ' + f_p(pdhRaw))
        label.set_xy(pdlLb, pdRight, pdlRaw)
        label.set_text(pdlLb, 'PDL ' + f_p(pdlRaw))

plot(showPD ? pdhRaw : na, 'PDH', color = color.new(pdhCss, 100), display = display.data_window)
plot(showPD ? pdlRaw : na, 'PDL', color = color.new(pdlCss, 100), display = display.data_window)

//=====================================================================================================================
// 8) STATUS DASHBOARD + TRADE LOG
//=====================================================================================================================
f_mix(color c1, color c2, float w) =>
    color.rgb(color.r(c1) * (1 - w) + color.r(c2) * w, color.g(c1) * (1 - w) + color.g(c2) * w, color.b(c1) * (1 - w) + color.b(c2) * w, 0)

f_row(table tb, int r, string k, string v, color kc, color vc, color bgc, string sz) =>
    table.cell(tb, 0, r, k, text_color = kc, bgcolor = bgc, text_size = sz, text_halign = text.align_left)
    table.cell(tb, 1, r, v, text_color = vc, bgcolor = bgc, text_size = sz, text_halign = text.align_right)

panelPosition = switch panelPos
    'Top right'    => position.top_right
    'Top left'     => position.top_left
    'Bottom right' => position.bottom_right
    'Bottom left'  => position.bottom_left
    =>                position.middle_right

logPosition = switch logPos
    'Bottom center' => position.bottom_center
    'Bottom left'   => position.bottom_left
    =>                 position.bottom_right

logTxt = switch logSize
    'Tiny'  => size.tiny
    'Small' => size.small
    =>         size.normal

var table panel = table.new(panelPosition, 2, 35, bgcolor = #131722, border_width = 1, border_color = #363a45, frame_width = 1, frame_color = #363a45)
var table logT  = table.new(logPosition, 10, 31, bgcolor = #131722, border_width = 1, border_color = #363a45, frame_width = 1, frame_color = #363a45)

if showPanel and barstate.islast
    bool  live = tDir != 0
    color acc  = tDir == 1 ? bullCss : tDir == -1 ? bearCss : color.new(#787b86, 0)
    color base = #131722
    color bg   = live ? f_mix(base, acc, 0.20) : #1b1f2b
    color bgH  = live ? f_mix(base, acc, 0.65) : #2a2e39
    color txt  = live ? color.new(#ffffff, 0) : #d1d4dc
    color dirC = tDir == 1 ? bullCss : tDir == -1 ? bearCss : txt
    string dirT = tDir == 1 ? '🟢 LONG ACTIVE' : tDir == -1 ? '🔴 SHORT ACTIVE' : '— no trade'
    float winP = cWin + cLoss > 0 ? 100.0 * cWin / (cWin + cLoss) : 0.0

    table.cell(panel, 0, 0, 'SMC LIQUIDITY SNIPER · STRATEGY', text_color = txt, bgcolor = bgH, text_size = lblSize, text_halign = text.align_left)
    table.cell(panel, 1, 0, timeframe.period + ' · ' + mode, text_color = txt, bgcolor = bgH, text_size = lblSize, text_halign = text.align_right)
    f_row(panel, 1,  'Swing trend', sTrend == 1 ? 'Bullish' : sTrend == -1 ? 'Bearish' : 'Neutral', txt, sTrend == 1 ? bullCss : sTrend == -1 ? bearCss : txt, bg, lblSize)
    f_row(panel, 2,  'Internal trend', iTrend == 1 ? 'Bullish' : iTrend == -1 ? 'Bearish' : 'Neutral', txt, iTrend == 1 ? bullCss : iTrend == -1 ? bearCss : txt, bg, lblSize)
    f_row(panel, 3,  'Last BOS / CHoCH', lastBos, txt, txt, bg, lblSize)
    f_row(panel, 4,  'Internal BOS', '▲ ' + str.tostring(cIntBull) + '   ▼ ' + str.tostring(cIntBear), txt, txt, bg, lblSize)
    f_row(panel, 5,  'Swing BOS', '▲ ' + str.tostring(cSwgBull) + '   ▼ ' + str.tostring(cSwgBear), txt, txt, bg, lblSize)
    f_row(panel, 6,  'Liquidity sweeps', '↑ ' + str.tostring(cSweepUp) + '   ↓ ' + str.tostring(cSweepDn), txt, txt, bg, lblSize)
    f_row(panel, 7,  'Current zone', prem ? 'Premium (sell)' : 'Discount (buy)', txt, prem ? bearCss : bullCss, bg, lblSize)
    f_row(panel, 8,  'Dominant candle', candleDir == 1 ? '▲ Bullish x' + str.tostring(bodyL > 0 ? bodyR / bodyL : 0, '#.##') : candleDir == -1 ? '▼ Bearish x' + str.tostring(bodyL > 0 ? bodyR / bodyL : 0, '#.##') : '— no dominance', txt, candleDir == 1 ? bullCss : candleDir == -1 ? bearCss : txt, bg, lblSize)
    f_row(panel, 9,  'Volume  (' + volBase + ')', 'x' + str.tostring(volRatio, '#.##') + '   min x' + str.tostring(minVolRatio, '#.##'), txt, volOK ? volRatio >= 1.5 ? warnCss : txt : bearCss, bg, lblSize)
    f_row(panel, 10, 'Liquidity ↑', na(upNear) ? '—' : f_p(upNear) + '  (' + str.tostring(distUp, '#.#') + ' ATR)', txt, bearCss, bg, lblSize)
    f_row(panel, 11, 'Liquidity ↓', na(dnNear) ? '—' : f_p(dnNear) + '  (' + str.tostring(distDn, '#.#') + ' ATR)', txt, bullCss, bg, lblSize)
    f_row(panel, 12, 'Liquidation risk ↑', f_bar(riskUp), txt, riskUp >= liqThresh ? warnCss : txt, bg, lblSize)
    f_row(panel, 13, 'Liquidation risk ↓', f_bar(riskDn), txt, riskDn >= liqThresh ? warnCss : txt, bg, lblSize)
    f_row(panel, 14, 'Magnet target', riskUp > riskDn ? '↑ ' + f_p(upBest) : '↓ ' + f_p(dnBest), txt, riskUp > riskDn ? bearCss : bullCss, bg, lblSize)
    f_row(panel, 15, 'Trade', dirT, txt, dirC, bg, lblSize)
    f_row(panel, 16, 'Entry / SL', tDir == 0 ? '—' : f_p(tEnt) + '  /  ' + f_p(tSl), txt, txt, bg, lblSize)
    f_row(panel, 17, 'TP1 / TP2 / TP3', na(tT1) ? '—' : f_p(tT1) + ' · ' + f_p(tT2) + ' · ' + f_p(tT3), txt, dirC, bg, lblSize)
    f_row(panel, 18, 'TP progress', na(tT1) ? '—' : (h1 ? 'TP1 ✔ ' : 'TP1 · ') + (h2 ? 'TP2 ✔ ' : 'TP2 · ') + (h3 ? 'TP3 ✔' : 'TP3 ·') + (na(openR) ? '' : '   ' + str.tostring(openR, '#.##') + 'R'), txt, dirC, bg, lblSize)
    float riskNowUsd = tDir != 0 and not na(tRiskUsd) ? tRiskUsd : f_usd(riskLong, ppL)
    float riskNowPts = tDir != 0 and not na(tRisk) ? tRisk : riskLong
    f_row(panel, 19, '★ Min profit rule', '$' + str.tostring(math.round(minProfitUsd)) + (enforceMinTp ? '  · enforced' : '  · not enforced'), txt, warnCss, bg, lblSize)
    f_row(panel, 20, 'Risk / TP1 value', '$' + str.tostring(math.round(riskNowUsd)) + ' / $' + str.tostring(math.round(tDir != 0 and not na(tT1) ? tPP * math.abs(tT1 - tEnt) : math.max(tp1Usd, minProfitUsd))) + '  (' + str.tostring(riskNowPts, '#.##') + ' pts)', txt, txt, bg, lblSize)
    f_row(panel, 21, 'Signals L / S', str.tostring(cLong) + ' / ' + str.tostring(cShort), txt, txt, bg, lblSize)
    f_row(panel, 22, 'Wins / losses  (cxl ' + str.tostring(cCancel) + ' · tope ' + str.tostring(cPanic) + ')', str.tostring(cWin) + ' / ' + str.tostring(cLoss), txt, cWin >= cLoss ? bullCss : bearCss, bg, lblSize)
    f_row(panel, 23, 'Win rate', str.tostring(winP, '#.#') + ' %', txt, winP >= 50 ? bullCss : bearCss, bg, lblSize)
    f_row(panel, 24, 'Cumulative R / $', str.tostring(sumR, '#.##') + 'R   $' + str.tostring(math.round(sumUsd)), txt, sumR >= 0 ? bullCss : bearCss, bg, lblSize)
    f_row(panel, 25, 'Strategy net profit', '$' + str.tostring(math.round(strategy.netprofit)) + '   ·   ' + str.tostring(strategy.closedtrades) + ' closed', txt, strategy.netprofit >= 0 ? bullCss : bearCss, bg, lblSize)
    f_row(panel, 26, 'Trades ≥ $' + str.tostring(math.round(minProfitUsd)), str.tostring(c1k), txt, c1k > 0 ? warnCss : txt, bg, lblSize)
    f_row(panel, 27, 'Candle · closes in', str.format_time(time, 'HH:mm:ss', tzUse) + '  ·  ' + str.tostring(math.round(secsLeft)) + 's', txt, secsLeft <= earlySecs ? warnCss : txt, bg, lblSize)
    f_row(panel, 28, 'Signal source', lastSrc, txt, dirC, bg, lblSize)
    string setupTxt = sState == 1 ? sDir == 1 ? 'Sweep done · waiting MSS ↑' : 'Sweep done · waiting MSS ↓' : sState == 2 ? 'MSS done · waiting FVG' : sState == 3 ? 'FVG armed ' + f_p(sBtm) + ' – ' + f_p(sTop) : '— idle'
    f_row(panel, 29, 'Liquidity + FVG', setupTxt, txt, sState == 3 ? warnCss : sState > 0 ? dirC : txt, bg, lblSize)
    float shNow = tDir != 0 ? tShares : math.max(shL, shS)
    f_row(panel, 30, 'Tamaño', str.tostring(shNow, '#') + ' acciones  ·  $' + str.tostring(math.round(shNow * close)) + ' expuesto  ·  reparto ' + str.tostring(math.round(w1 * 100)) + '/' + str.tostring(math.round(w2 * 100)) + '/' + str.tostring(math.round(w3 * 100)), txt, txt, bg, lblSize)
    // --- R1/R2/R3: el tope de pérdida, siempre a la vista
    float riskRealNow = tDir != 0 ? tRiskUsd : math.max(shL * riskLong, shS * riskShort)
    f_row(panel, 31, '⛔ Riesgo por operación', '$' + str.tostring(math.round(riskRealNow)) + ' de $' + str.tostring(math.round(riskBudget)) + ' presupuestado   ·   tope duro $' + str.tostring(math.round(maxLossUsd)), txt, riskRealNow > maxLossUsd ? bearCss : bullCss, bg, lblSize)
    f_row(panel, 32, 'Huecos · tamaño limitado', str.tostring(cGap) + ' huecos saltaron el stop   ·   ' + str.tostring(cCapped) + ' entradas con tamaño recortado' + (eodFlat ? '   ·   cierre EOD ON' : '   ·   overnight permitido'), txt, cGap > 0 ? bearCss : txt, bg, lblSize)
    f_row(panel, 33, 'PDH / PDL', not showPD or na(pdhRaw) ? '—' : f_p(pdhRaw) + '  /  ' + f_p(pdlRaw) + '   (' + str.tostring((pdhRaw - close) / atr, '#.#') + ' / ' + str.tostring((close - pdlRaw) / atr, '#.#') + ' ATR)', txt, txt, bg, lblSize)
    f_row(panel, 34, 'Last block', blockTxt, txt, blockTxt == '— none' ? txt : warnCss, bg, lblSize)

if showLog and barstate.islast
    table.clear(logT, 0, 0, 9, 30)
    color hdrBg = #2a2e39
    color hdrTx = #d1d4dc
    color rowBg = #1b1f2b
    table.cell(logT, 0, 0, '#',          text_color = hdrTx, bgcolor = hdrBg, text_size = logTxt)
    table.cell(logT, 1, 0, 'Entry time', text_color = hdrTx, bgcolor = hdrBg, text_size = logTxt)
    table.cell(logT, 2, 0, 'Dir',        text_color = hdrTx, bgcolor = hdrBg, text_size = logTxt)
    table.cell(logT, 3, 0, 'Entry',      text_color = hdrTx, bgcolor = hdrBg, text_size = logTxt)
    table.cell(logT, 4, 0, 'Exit',       text_color = hdrTx, bgcolor = hdrBg, text_size = logTxt)
    table.cell(logT, 5, 0, 'TP',         text_color = hdrTx, bgcolor = hdrBg, text_size = logTxt)
    table.cell(logT, 6, 0, 'R',          text_color = hdrTx, bgcolor = hdrBg, text_size = logTxt)
    table.cell(logT, 7, 0, 'Profit',     text_color = hdrTx, bgcolor = hdrBg, text_size = logTxt)
    table.cell(logT, 8, 0, 'Status',     text_color = hdrTx, bgcolor = hdrBg, text_size = logTxt)
    table.cell(logT, 9, 0, 'Setup',      text_color = hdrTx, bgcolor = hdrBg, text_size = logTxt)
    int nLog = math.min(logRows, logArr.size())
    if nLog > 0
        for i = 0 to nLog - 1 by 1
            tlog e  = logArr.get(i)
            color rc = e.dir == 1 ? bullCss : bearCss
            color pc = e.res == 1 ? bullCss : e.res == -1 ? bearCss : warnCss
            string st = e.res == 2 ? '⚪ cancelled' : e.res == 0 ? '⏳ TP' + str.tostring(e.tp) + ' locked' : e.res == -1 ? '✖ stop loss' : e.tp == 3 ? '✔ TP3 reached' : e.tp > 0 ? '✔ trailed at TP' + str.tostring(e.tp) : '✔ closed in profit'
            table.cell(logT, 0, i + 1, str.tostring(i + 1), text_color = hdrTx, bgcolor = rowBg, text_size = logTxt)
            table.cell(logT, 1, i + 1, str.format_time(e.t, 'dd/MM HH:mm:ss', tzUse), text_color = hdrTx, bgcolor = rowBg, text_size = logTxt)
            table.cell(logT, 2, i + 1, e.dir == 1 ? 'LONG' : 'SHORT', text_color = rc, bgcolor = rowBg, text_size = logTxt)
            table.cell(logT, 3, i + 1, f_p(e.ent), text_color = hdrTx, bgcolor = rowBg, text_size = logTxt)
            table.cell(logT, 4, i + 1, f_p(e.ex), text_color = pc, bgcolor = rowBg, text_size = logTxt)
            table.cell(logT, 5, i + 1, e.tp > 0 ? 'TP' + str.tostring(e.tp) : '—', text_color = hdrTx, bgcolor = rowBg, text_size = logTxt)
            table.cell(logT, 6, i + 1, str.tostring(e.r, '#.##') + 'R', text_color = pc, bgcolor = rowBg, text_size = logTxt)
            table.cell(logT, 7, i + 1, (e.usd >= 0 ? '$' : '-$') + str.tostring(math.round(math.abs(e.usd))), text_color = pc, bgcolor = rowBg, text_size = logTxt)
            table.cell(logT, 8, i + 1, st, text_color = pc, bgcolor = rowBg, text_size = logTxt)
            table.cell(logT, 9, i + 1, e.src, text_color = hdrTx, bgcolor = rowBg, text_size = logTxt)
    else
        table.cell(logT, 0, 1, 'No closed trades yet on this chart', text_color = hdrTx, bgcolor = rowBg, text_size = logTxt)

statsPosition = switch statsPos
    'Top left'     => position.top_left
    'Middle left'  => position.middle_left
    'Bottom left'  => position.bottom_left
    =>                position.middle_right

var table statsT = table.new(statsPosition, 6, 20, bgcolor = color(na), border_width = 1, border_color = #363a45, frame_width = 0)

if showStats and barstate.islast
    color sHdrBg = #2a2e39
    color sTx    = #d1d4dc
    color sRowBg = #1b1f2b
    int r0 = statsDrop
    table.cell(statsT, 0, r0, 'SETUP',  text_color = sTx, bgcolor = sHdrBg, text_size = logTxt, text_halign = text.align_left)
    table.cell(statsT, 1, r0, 'Trades', text_color = sTx, bgcolor = sHdrBg, text_size = logTxt)
    table.cell(statsT, 2, r0, 'Win %',  text_color = sTx, bgcolor = sHdrBg, text_size = logTxt)
    table.cell(statsT, 3, r0, 'Cxl',    text_color = sTx, bgcolor = sHdrBg, text_size = logTxt)
    table.cell(statsT, 4, r0, 'R',      text_color = sTx, bgcolor = sHdrBg, text_size = logTxt)
    table.cell(statsT, 5, r0, '$',      text_color = sTx, bgcolor = sHdrBg, text_size = logTxt)
    for k = 0 to 4 by 1
        string nm = switch k
            0 => '#1 Liquidity sweep'
            1 => '#2 BOS / CHoCH'
            2 => '#3 Half body BOS'
            3 => '#4 Candle dominance'
            =>   '#5 Liquidity + FVG'
        int   n  = srcN.get(k)
        int   w  = srcW.get(k)
        float rr = srcR.get(k)
        float wp = n > 0 ? 100.0 * w / n : 0.0
        color rc = rr > 0 ? bullCss : rr < 0 ? bearCss : sTx
        table.cell(statsT, 0, r0 + k + 1, nm, text_color = sTx, bgcolor = sRowBg, text_size = logTxt, text_halign = text.align_left)
        table.cell(statsT, 1, r0 + k + 1, str.tostring(n), text_color = sTx, bgcolor = sRowBg, text_size = logTxt)
        table.cell(statsT, 2, r0 + k + 1, n > 0 ? str.tostring(wp, '#.#') + '%' : '—', text_color = wp >= 50 ? bullCss : sTx, bgcolor = sRowBg, text_size = logTxt)
        table.cell(statsT, 3, r0 + k + 1, str.tostring(srcC.get(k)), text_color = warnCss, bgcolor = sRowBg, text_size = logTxt)
        table.cell(statsT, 4, r0 + k + 1, str.tostring(rr, '#.#') + 'R', text_color = rc, bgcolor = sRowBg, text_size = logTxt)
        float uu = srcU.get(k)
        table.cell(statsT, 5, r0 + k + 1, (uu >= 0 ? '$' : '-$') + str.tostring(math.round(math.abs(uu))), text_color = rc, bgcolor = sRowBg, text_size = logTxt)
    table.cell(statsT, 0, r0 + 6, 'TOTAL', text_color = sTx, bgcolor = sHdrBg, text_size = logTxt, text_halign = text.align_left)
    table.cell(statsT, 1, r0 + 6, str.tostring(cWin + cLoss), text_color = sTx, bgcolor = sHdrBg, text_size = logTxt)
    table.cell(statsT, 2, r0 + 6, cWin + cLoss > 0 ? str.tostring(100.0 * cWin / (cWin + cLoss), '#.#') + '%' : '—', text_color = sTx, bgcolor = sHdrBg, text_size = logTxt)
    table.cell(statsT, 3, r0 + 6, str.tostring(cCancel), text_color = warnCss, bgcolor = sHdrBg, text_size = logTxt)
    table.cell(statsT, 4, r0 + 6, str.tostring(sumR, '#.#') + 'R', text_color = sumR >= 0 ? bullCss : bearCss, bgcolor = sHdrBg, text_size = logTxt)
    table.cell(statsT, 5, r0 + 6, (sumUsd >= 0 ? '$' : '-$') + str.tostring(math.round(math.abs(sumUsd))), text_color = sumUsd >= 0 ? bullCss : bearCss, bgcolor = sHdrBg, text_size = logTxt)

//=====================================================================================================================
// 9) ALERTCONDITIONS (classic alerts)
//=====================================================================================================================
alertcondition(longSig,    'LONG entry',                       'SMC Liquidity Sniper buy signal')
alertcondition(shortSig,   'SHORT entry',                      'SMC Liquidity Sniper sell signal')
alertcondition(warnUp,     'Liquidity near above',             'A liquidity sweep is approaching above price')
alertcondition(warnDn,     'Liquidity near below',             'A liquidity sweep is approaching below price')
alertcondition(sweepUp,    'Sweep of highs',                   'Buy-side liquidity swept (possible bearish reversal)')
alertcondition(sweepDn,    'Sweep of lows',                    'Sell-side liquidity swept (possible bullish reversal)')
alertcondition(candleBull, 'Bullish dominant candle in zone',  'Larger bullish candle in discount zone')
alertcondition(candleBear, 'Bearish dominant candle in zone',  'Larger bearish candle in premium zone')
alertcondition(hitTp1,     'TP1 reached',                      'TP1 reached')
alertcondition(hitTp2,     'TP2 reached',                      'TP2 reached')
alertcondition(hitTp3,     'TP3 reached',                      'TP3 reached')
alertcondition(hitSl,      'SL hit',                           'Stop loss hit')
alertcondition(panicOut,   'Tope de pérdida alcanzado',        'La operación llegó al tope de pérdida y se cerró a mercado')
alertcondition(forcedOut,  'Cierre forzoso (hueco o sesión)',   'La posición se cerró a mercado por hueco de apertura o por cierre de sesión')
alertcondition(timedOut,   'Trade timed out',                  'Trade closed at market after the maximum number of bars')
alertcondition(cancelled,  'Order cancelled',                  'Order cancelled: no progress after the confirmation bars')
alertcondition(contBar,    'Move continuation',                'The move keeps running in the direction of the open entry')