Estrategia para TradingView. No se instala nada: se copia y se pega.
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')