// ───────────────────────────────────────────────────────────── // Guru IA Zones v3.1 — TradingView Edition // Zonas Supply/Demand con grading A+/A/B, retests, alertas. // Estetica limpia: texto dentro de caja, dashboard de totales, // solo zonas relevantes (C ocultas por default). // ───────────────────────────────────────────────────────────── //@version=6 indicator("Guru IA Zones v3.1", overlay=true, max_boxes_count=200, max_labels_count=200) // ============================================================================ // INPUTS // ============================================================================ swingLen = input.int(7, "Sensibilidad swing", minval=3, maxval=50, tooltip="Bajo = mas zonas (sensible). Alto = menos pero mas importantes", group="Deteccion") maxZones = input.int(4, "Max zonas activas por lado", minval=1, maxval=10, group="Deteccion") minZoneDistance = input.int(10, "Min distancia entre zonas (bars)", minval=1, maxval=200, group="Deteccion") maxZoneHeightATR = input.float(2.5, "Max altura zona (xATR)", minval=0.5, maxval=10.0, step=0.5, group="Tamano zona") minZoneHeightATR = input.float(0.15, "Min altura zona (xATR)", minval=0.05, maxval=2.0, step=0.05, group="Tamano zona") forceZoneHeightATR = input.float(0.4, "Force altura minima (xATR)", minval=0.1, maxval=3.0, step=0.1, tooltip="Zonas finas se expanden a este tamano para que sean visibles", group="Tamano zona") invalidationMethod = input.string("Close", "Invalidacion", options=["Close", "Wick"], group="Reglas") sessionFilter = input.bool(false, "Solo zonas de Londres/NY", group="Reglas") showHistoric = input.bool(false, "Mostrar zonas rotas", group="Reglas") hideCGrade = input.bool(true, "Ocultar zonas grado C (debiles)", group="Reglas", tooltip="Recomendado activado. Las C son ruido.") showDashboard = input.bool(true, "Mostrar dashboard (totales)", group="Display") labelSize = input.string("Normal", "Tamano label", options=["Small","Normal","Large"], group="Display") // Colores por grade — opacos para mejor lectura col_aplus_demand = input.color(color.new(#FFD700, 75), "A+ Demand", group="Colores") col_aplus_supply = input.color(color.new(#FF6B35, 75), "A+ Supply", group="Colores") col_a_demand = input.color(color.new(#10B981, 80), "A Demand", group="Colores") col_a_supply = input.color(color.new(#EF4444, 80), "A Supply", group="Colores") col_b_demand = input.color(color.new(#22D3EE, 85), "B Demand", group="Colores") col_b_supply = input.color(color.new(#F97316, 85), "B Supply", group="Colores") col_c_neutral = input.color(color.new(#9CA3AF, 88), "C (debil)", group="Colores") col_historic = input.color(color.new(#6B7280, 92), "Roto", group="Colores") // Alerts alertsOn = input.bool(true, "Alertas activas", group="Alertas") alertCooldownMins = input.int(15, "Cooldown alertas (min)", minval=0, maxval=120, group="Alertas") nearPctAlert = input.float(0.3, "Distancia 'cerca' (%)", minval=0.05, maxval=2.0, step=0.05, group="Alertas") // ============================================================================ // HELPERS // ============================================================================ f_getPips() => bool isForex = syminfo.type == "forex" bool isJpy = syminfo.currency == "JPY" bool isGold = syminfo.ticker == "XAUUSD" or str.contains(syminfo.ticker, "GOLD") float result = 0.0001 if isForex and isJpy result := 0.01 else if isGold result := 0.1 result f_toPips(val) => math.abs(val) / f_getPips() f_getSession() => h = hour(time, syminfo.timezone) h >= 0 and h < 8 ? "Asia" : h >= 8 and h < 13 ? "London" : h >= 13 and h < 22 ? "NY" : "Other" f_labelSize() => string s = labelSize if s == "Small" size.small else if s == "Large" size.large else size.normal f_calcGrade(int strength, int retests, int age, string session) => float score = float(strength) * 8.0 score := score - math.min(retests, 5) * 3.0 score := score - math.min(age / 200.0, 3.0) * 5.0 if session == "London" or session == "NY" score := score + 8.0 score := math.max(0, math.min(100, score)) string g = score >= 85 ? "A+" : score >= 70 ? "A" : score >= 55 ? "B" : "C" [int(math.round(score)), g] f_gradeColor(string grade, string zoneType, bool broken) => if broken col_historic else if grade == "A+" zoneType == "Supply" ? col_aplus_supply : col_aplus_demand else if grade == "A" zoneType == "Supply" ? col_a_supply : col_a_demand else if grade == "B" zoneType == "Supply" ? col_b_supply : col_b_demand else col_c_neutral f_borderColor(string grade, string zoneType, bool broken) => if broken color.new(#6B7280, 40) else if grade == "A+" zoneType == "Supply" ? color.new(#FF6B35, 0) : color.new(#FFD700, 0) else if grade == "A" zoneType == "Supply" ? color.new(#EF4444, 10) : color.new(#10B981, 10) else if grade == "B" zoneType == "Supply" ? color.new(#F97316, 20) : color.new(#22D3EE, 20) else color.new(#9CA3AF, 40) f_gradeBorderWidth(string grade) => grade == "A+" ? 3 : grade == "A" ? 2 : 1 f_textColor(string zoneType, bool broken) => if broken color.new(color.white, 50) else if zoneType == "Supply" color.new(#FFEEEE, 0) else color.new(#EEFFEE, 0) // ============================================================================ // STATE // ============================================================================ atr = ta.atr(20) type Zone box zBox label infoLbl float top float bottom int startBar string session int retests bool broken bool retestCounted string zoneType int score string grade int strength int alertLastTime var array allZones = array.new() var int lastSupplyBar = 0 var int lastDemandBar = 0 var table dash = table.new(position.bottom_right, 2, 5, bgcolor=color.new(#0a0f1a, 20), border_width=1, border_color=color.new(#6366F1, 60)) // ============================================================================ // CREAR ZONA // ============================================================================ f_createZone(float topRaw, float botRaw, int startBar, string session, string zoneType) => float top = topRaw float bottom = botRaw float mid = (top + bottom) / 2.0 float h = top - bottom if h < atr * forceZoneHeightATR float fh = atr * forceZoneHeightATR top := mid + fh / 2.0 bottom := mid - fh / 2.0 h := fh if h > atr * maxZoneHeightATR float mh = atr * maxZoneHeightATR top := mid + mh / 2.0 bottom := mid - mh / 2.0 h := mh bool valid = h >= atr * minZoneHeightATR color bg = zoneType == "Supply" ? col_b_supply : col_b_demand Zone z = na if valid bx = box.new(startBar, top, bar_index + 50, bottom, bgcolor=bg, border_color=bg, border_width=1, extend=extend.right) z := Zone.new(bx, na, top, bottom, startBar, session, 0, false, false, zoneType, 0, "C", 0, 0) z // ============================================================================ // DETECCION // ============================================================================ ph = ta.pivothigh(high, swingLen, swingLen) pl = ta.pivotlow(low, swingLen, swingLen) if not na(ph) and barstate.isconfirmed session = f_getSession() bool sessionOk = not sessionFilter or session == "London" or session == "NY" if sessionOk and (bar_index - lastSupplyBar >= minZoneDistance) float topRaw = high[swingLen] float botRaw = low[swingLen] int startBarVal = bar_index - swingLen bool merged = false if array.size(allZones) > 0 int from = math.max(0, array.size(allZones) - 12) for i = array.size(allZones) - 1 to from Zone z = array.get(allZones, i) if z.zoneType == "Supply" and not z.broken bool overlap = topRaw >= z.bottom and botRaw <= z.top if overlap z.top := math.max(topRaw, z.top) z.bottom := math.min(botRaw, z.bottom) box.set_top(z.zBox, z.top) box.set_bottom(z.zBox, z.bottom) merged := true break if not merged Zone newZ = f_createZone(topRaw, botRaw, startBarVal, session, "Supply") if not na(newZ) array.push(allZones, newZ) lastSupplyBar := bar_index if not na(pl) and barstate.isconfirmed session = f_getSession() bool sessionOk = not sessionFilter or session == "London" or session == "NY" if sessionOk and (bar_index - lastDemandBar >= minZoneDistance) float topRaw = high[swingLen] float botRaw = low[swingLen] int startBarVal = bar_index - swingLen bool merged = false if array.size(allZones) > 0 int from = math.max(0, array.size(allZones) - 12) for i = array.size(allZones) - 1 to from Zone z = array.get(allZones, i) if z.zoneType == "Demand" and not z.broken bool overlap = topRaw >= z.bottom and botRaw <= z.top if overlap z.top := math.max(topRaw, z.top) z.bottom := math.min(botRaw, z.bottom) box.set_top(z.zBox, z.top) box.set_bottom(z.zBox, z.bottom) merged := true break if not merged Zone newZ = f_createZone(topRaw, botRaw, startBarVal, session, "Demand") if not na(newZ) array.push(allZones, newZ) lastDemandBar := bar_index // ============================================================================ // UPDATE: invalidacion + retests + grade + render // ============================================================================ int countActiveSup = 0 int countActiveDem = 0 int countAplus = 0 int countA = 0 int countB = 0 float bestScore = 0 string bestSide = "—" if array.size(allZones) > 0 int activeSup = 0 int activeDem = 0 int histShown = 0 // Iteramos de mas reciente a mas viejo for i = array.size(allZones) - 1 to 0 Zone z = array.get(allZones, i) box.set_right(z.zBox, bar_index + 50) // Invalidacion if not z.broken if z.zoneType == "Supply" float br = invalidationMethod == "Close" ? close : high if br > z.top z.broken := true else float br = invalidationMethod == "Close" ? close : low if br < z.bottom z.broken := true // Retest counter if not z.broken bool priceInZone = high >= z.bottom and low <= z.top if priceInZone and not z.retestCounted z.retests := z.retests + 1 z.retestCounted := true else if not priceInZone z.retestCounted := false // Grade int age = bar_index - z.startBar float st = 10.0 - (float(z.retests) * 0.8) - math.min(age / 300.0, 3.0) z.strength := int(math.max(0, math.min(10, st))) [scoreV, gradeV] = f_calcGrade(z.strength, z.retests, age, z.session) z.score := scoreV z.grade := gradeV // Visibility bool hide = false if z.broken and not showHistoric hide := true else if z.broken and showHistoric if histShown >= 3 hide := true else histShown := histShown + 1 else if not z.broken if hideCGrade and z.grade == "C" hide := true else if z.zoneType == "Supply" if activeSup >= maxZones hide := true else activeSup := activeSup + 1 else if activeDem >= maxZones hide := true else activeDem := activeDem + 1 // Render box if hide box.set_bgcolor(z.zBox, color.new(color.gray, 100)) box.set_border_color(z.zBox, color.new(color.gray, 100)) else color bgC = f_gradeColor(z.grade, z.zoneType, z.broken) color brC = f_borderColor(z.grade, z.zoneType, z.broken) int bw = f_gradeBorderWidth(z.grade) box.set_bgcolor(z.zBox, bgC) box.set_border_color(z.zBox, brC) box.set_border_width(z.zBox, bw) // Label DENTRO de la caja (mid-left) if not hide float mid = (z.top + z.bottom) / 2.0 float heightPips = f_toPips(z.top - z.bottom) string side = z.zoneType == "Supply" ? "SELL" : "BUY" string txt = side + " " + z.grade + " | " + str.tostring(z.score) + "%" if z.retests > 0 txt := txt + " | R" + str.tostring(z.retests) txt := txt + " · " + str.tostring(int(math.round(heightPips))) + "p" color textC = f_textColor(z.zoneType, z.broken) // Posicion: centrado horizontalmente entre el inicio de la zona // y el bar_index actual, dentro de la caja (sin fondo pill) int lblX = math.min(bar_index - 2, z.startBar + int(math.max(5, (bar_index - z.startBar) / 2))) if na(z.infoLbl) z.infoLbl := label.new(lblX, mid, txt, color=color.new(color.black, 100), textcolor=textC, size=f_labelSize(), style=label.style_none) else label.set_xy(z.infoLbl, lblX, mid) label.set_text(z.infoLbl, txt) label.set_textcolor(z.infoLbl, textC) label.set_style(z.infoLbl, label.style_none) else if not na(z.infoLbl) label.set_textcolor(z.infoLbl, color.new(color.white, 100)) label.set_color(z.infoLbl, color.new(color.white, 100)) // Stats para dashboard if not hide and not z.broken if z.zoneType == "Supply" countActiveSup := countActiveSup + 1 else countActiveDem := countActiveDem + 1 if z.grade == "A+" countAplus := countAplus + 1 else if z.grade == "A" countA := countA + 1 else if z.grade == "B" countB := countB + 1 if z.score > bestScore bestScore := z.score string sidePrefix = z.zoneType == "Supply" ? "SELL " : "BUY " bestSide := sidePrefix + z.grade + " " + str.tostring(z.score) + "%" // ============================================================================ // ALERTS (cooldown) // ============================================================================ if array.size(allZones) > 0 and alertsOn and barstate.isconfirmed int nowMin = int(time / 60000) for i = 0 to array.size(allZones) - 1 Zone z = array.get(allZones, i) if z.broken continue if z.grade == "C" continue float nearDist = (z.top - z.bottom) + (close * nearPctAlert / 100.0) bool hit = false bool near = false if z.zoneType == "Supply" hit := high >= z.bottom and high <= z.top near := not hit and math.abs(close - z.bottom) <= nearDist else hit := low <= z.top and low >= z.bottom near := not hit and math.abs(close - z.top) <= nearDist int sinceAlert = nowMin - z.alertLastTime bool cooldownOk = alertCooldownMins == 0 or sinceAlert >= alertCooldownMins if (hit or near) and cooldownOk z.alertLastTime := nowMin string side = z.zoneType == "Supply" ? "SELL" : "BUY" string typeS = hit ? "TOQUE" : "CERCA" alert(typeS + " " + side + " [" + z.grade + "] " + syminfo.ticker + " @ " + str.tostring(close) + " | Zona " + str.tostring(z.bottom) + "-" + str.tostring(z.top) + " | RS " + str.tostring(z.score) + "%", alert.freq_once_per_bar) // ============================================================================ // DASHBOARD (estilo BigBeluga pero original) // ============================================================================ if barstate.islast and showDashboard table.cell(dash, 0, 0, "GURU IA ZONES v3.1", text_color=color.new(#A5B4FC, 0), text_size=size.small, bgcolor=color.new(#6366F1, 30)) table.cell(dash, 1, 0, syminfo.ticker, text_color=color.new(#A5B4FC, 0), text_size=size.small, bgcolor=color.new(#6366F1, 30)) table.cell(dash, 0, 1, "Supply activas:", text_color=color.new(#FCA5A5, 0), text_size=size.small, text_halign=text.align_left) table.cell(dash, 1, 1, str.tostring(countActiveSup), text_color=color.new(#FCA5A5, 0), text_size=size.small) table.cell(dash, 0, 2, "Demand activas:", text_color=color.new(#6EE7B7, 0), text_size=size.small, text_halign=text.align_left) table.cell(dash, 1, 2, str.tostring(countActiveDem), text_color=color.new(#6EE7B7, 0), text_size=size.small) table.cell(dash, 0, 3, "A+/A/B:", text_color=color.new(#FFFFFF, 0), text_size=size.small, text_halign=text.align_left) table.cell(dash, 1, 3, str.tostring(countAplus) + "/" + str.tostring(countA) + "/" + str.tostring(countB), text_color=color.new(#FFD700, 0), text_size=size.small) table.cell(dash, 0, 4, "Mejor zona:", text_color=color.new(#A1A1AA, 0), text_size=size.small, text_halign=text.align_left) table.cell(dash, 1, 4, bestSide, text_color=color.new(#FFD700, 0), text_size=size.small)