//@version=6 // ============================================================================= // VERISCOPE SESSION MATRIX v2.3 — Timing & Attention Layer // Answers ONLY: "When should I be watching the chart?" // Does NOT draw structure, OB, FVG, or trade signals. Not a signal generator. // ----------------------------------------------------------------------------- // v2.1: removed invalid max_tables_count argument; removed illegal // object.field[1] historical indexing. // v2.2: fixed reversal formula (compared wrong bars, always returned 0%). // Now compares session open vs session close relative to session midpoint. // v2.3: full English UI with in-dashboard language switch (English/Português). // All user-facing strings route through f_t(en, pt). // ============================================================================= indicator("Veriscope Session Matrix", shorttitle = "Session Matrix", overlay = true, max_boxes_count = 100, max_lines_count = 100, max_labels_count = 100) // --------------------------------------------------------------------------- // TYPES // --------------------------------------------------------------------------- type SessionStats array rangeHist = na array sweepHist = na array expansionHist = na array reversalHist = na float sHigh = na float sLow = na float sOpen = na bool wasIn = false box sBox = na // --------------------------------------------------------------------------- // INPUTS // --------------------------------------------------------------------------- GRP_LANG = "Language / Idioma" langInput = input.string("English", "Dashboard Language", options = ["English", "Português"], group = GRP_LANG) GRP_SESS = "Sessions (DST-aware)" asiaSession = input.session("0000-0900", "Asia Session", group = GRP_SESS) asiaTZ = input.string("Asia/Tokyo", "Asia Timezone", group = GRP_SESS) londonSession = input.session("0700-1600", "London Session", group = GRP_SESS) londonTZ = input.string("Europe/London", "London Timezone", group = GRP_SESS) nySession = input.session("0800-1700", "New York Session", group = GRP_SESS) nyTZ = input.string("America/New_York", "New York Timezone", group = GRP_SESS) GRP_KZ = "Kill Zones" londonKZ = input.session("0700-1000", "London Kill Zone", group = GRP_KZ) nyKZ = input.session("0700-1000", "New York Kill Zone", group = GRP_KZ) GRP_STATS = "Session Fingerprint" lookbackDays = input.int(30, "Sessions of History for Fingerprint", minval = 10, maxval = 200, group = GRP_STATS) volAlertMult = input.float(1.3, "Alert when session range >= x times average", minval = 1.0, step = 0.1, group = GRP_STATS) GRP_ALERTS = "Smart Alerts" onlyAlertStrongFingerprint = input.bool(true, "Only alert when session has a strong historical fingerprint", group = GRP_ALERTS) attentionAlertThreshold = input.int(65, "Minimum Attention Index to alert", minval = 0, maxval = 100, group = GRP_ALERTS) GRP_DASH = "Dashboard" showDashboard = input.bool(true, "Show Dashboard", group = GRP_DASH) dashPos = input.string("Top Right", "Position", options = ["Top Right", "Top Left", "Bottom Right", "Bottom Left"], group = GRP_DASH) showBoxes = input.bool(true, "Show Session Boxes", group = GRP_DASH) showBias = input.bool(true, "Show Daily Open Bias Line", group = GRP_DASH) GRP_HEAT = "Weekly Heatmap" showHeatmap = input.bool(true, "Show Weekly Session Heatmap", group = GRP_HEAT) heatmapPos = input.string("Bottom Right", "Heatmap Position", options = ["Top Right", "Top Left", "Bottom Right", "Bottom Left"], group = GRP_HEAT) // --------------------------------------------------------------------------- // LANGUAGE HELPER — every user-facing string routes through this // --------------------------------------------------------------------------- f_t(string en, string pt) => langInput == "Português" ? pt : en // --------------------------------------------------------------------------- // HELPERS // --------------------------------------------------------------------------- f_pos(string p) => switch p "Top Right" => position.top_right "Top Left" => position.top_left "Bottom Right" => position.bottom_right "Bottom Left" => position.bottom_left => position.top_right f_pushBool(array a, bool v, int maxLen) => array.unshift(a, v) if array.size(a) > maxLen array.pop(a) f_pushFloat(array a, float v, int maxLen) => array.unshift(a, v) if array.size(a) > maxLen array.pop(a) f_pct(array a) => n = array.size(a) if n == 0 float(na) else c = 0 for i = 0 to n - 1 if array.get(a, i) c += 1 c / n * 100.0 f_avg(array a) => array.size(a) > 0 ? array.avg(a) : na f_stars(float pctRank) => n = math.max(1, math.min(5, int(math.ceil(pctRank / 20.0)))) s = "" for i = 1 to n s := s + "★" for i = n + 1 to 5 s := s + "☆" s // --------------------------------------------------------------------------- // SESSION DETECTION (DST-safe) // --------------------------------------------------------------------------- inAsia = not na(time(timeframe.period, asiaSession, asiaTZ)) inLondon = not na(time(timeframe.period, londonSession, londonTZ)) inNY = not na(time(timeframe.period, nySession, nyTZ)) inLondonKZ = not na(time(timeframe.period, londonKZ, londonTZ)) inNYKZ = not na(time(timeframe.period, nyKZ, nyTZ)) inOverlap = inLondon and inNY string currentSessionText = inOverlap ? f_t("London x NY Overlap", "Londres x NY Sobreposição") : inNY ? f_t("New York", "Nova York") : inLondon ? f_t("London", "Londres") : inAsia ? f_t("Asia", "Ásia") : f_t("Off-hours", "Fora de sessão") // --------------------------------------------------------------------------- // HEATMAP STORAGE (7 days x 3 sessions) // --------------------------------------------------------------------------- var array heatSum = array.new(21, 0.0) var array heatCount = array.new(21, 0) // --------------------------------------------------------------------------- // PERSISTENT STATS OBJECTS // --------------------------------------------------------------------------- var SessionStats asiaStats = SessionStats.new(rangeHist = array.new(), sweepHist = array.new(), expansionHist = array.new(), reversalHist = array.new()) var SessionStats londonStats = SessionStats.new(rangeHist = array.new(), sweepHist = array.new(), expansionHist = array.new(), reversalHist = array.new()) var SessionStats nyStats = SessionStats.new(rangeHist = array.new(), sweepHist = array.new(), expansionHist = array.new(), reversalHist = array.new()) // sessionIdx: 0 = Asia, 1 = London, 2 = New York (used for heatmap bucket) f_trackSession(SessionStats st, bool inSession, color col, bool drawBox, int sessionIdx) => if inSession if not st.wasIn st.sHigh := high st.sLow := low st.sOpen := open if drawBox and showBoxes st.sBox := box.new(bar_index, high, bar_index, low, border_color = color.new(col, 50), bgcolor = color.new(col, 92), extend = extend.none) else st.sHigh := math.max(st.sHigh, high) st.sLow := math.min(st.sLow, low) if drawBox and showBoxes and not na(st.sBox) box.set_top(st.sBox, st.sHigh) box.set_bottom(st.sBox, st.sLow) box.set_right(st.sBox, bar_index) else if st.wasIn // Session just closed on the previous bar — commit fingerprint + heatmap. rangeDone = st.sHigh - st.sLow avgSoFar = f_avg(st.rangeHist) wasExpansion = not na(avgSoFar) and rangeDone > avgSoFar // Reversal = session closed on the opposite side of its own midpoint // from where it opened (fixed in v2.2 — previous formula was flat 0%). sessionMid = (st.sHigh + st.sLow) / 2.0 openedAbove = st.sOpen > sessionMid closedAbove = close[1] > sessionMid wasReversal = openedAbove != closedAbove f_pushFloat(st.rangeHist, rangeDone, lookbackDays) f_pushBool(st.sweepHist, false, lookbackDays) f_pushBool(st.expansionHist, wasExpansion, lookbackDays) f_pushBool(st.reversalHist, wasReversal, lookbackDays) // heatmap bucket for the day that just closed dIdx = dayofweek(time[1]) - 1 bucket = dIdx * 3 + sessionIdx array.set(heatSum, bucket, array.get(heatSum, bucket) + rangeDone) array.set(heatCount, bucket, array.get(heatCount, bucket) + 1) st.wasIn := inSession f_trackSession(asiaStats, inAsia, color.blue, true, 0) f_trackSession(londonStats, inLondon, color.orange, true, 1) f_trackSession(nyStats, inNY, color.purple, true, 2) asiaAvgRange = f_avg(asiaStats.rangeHist) londonAvgRange = f_avg(londonStats.rangeHist) nyAvgRange = f_avg(nyStats.rangeHist) asiaExpPct = f_pct(asiaStats.expansionHist) londonExpPct = f_pct(londonStats.expansionHist) nyExpPct = f_pct(nyStats.expansionHist) londonRevPct = f_pct(londonStats.reversalHist) nyRevPct = f_pct(nyStats.reversalHist) currentLiveRange = inNY ? (nyStats.sHigh - nyStats.sLow) : inLondon ? (londonStats.sHigh - londonStats.sLow) : inAsia ? (asiaStats.sHigh - asiaStats.sLow) : na currentAvgRange = inNY ? nyAvgRange : inLondon ? londonAvgRange : inAsia ? asiaAvgRange : na volRatio = (not na(currentLiveRange) and not na(currentAvgRange) and currentAvgRange > 0) ? currentLiveRange / currentAvgRange * 100.0 : na currentExpPct = inNY ? nyExpPct : inLondon ? londonExpPct : inAsia ? asiaExpPct : na // --------------------------------------------------------------------------- // ATTENTION INDEX (0-100) — descriptive only, NOT a trade signal // --------------------------------------------------------------------------- overlapPts = inOverlap ? 30 : 0 kzPts = (inLondonKZ or inNYKZ) ? 25 : 0 volPts = na(volRatio) ? 0 : math.max(0, math.min(25, math.round((volRatio - 100.0) / 100.0 * 25))) fingerprintPts = na(currentExpPct) ? 0 : math.round(currentExpPct / 100.0 * 20) attentionIndex = math.max(0, math.min(100, overlapPts + kzPts + volPts + fingerprintPts + (inAsia or inLondon or inNY ? 10 : 0))) // --------------------------------------------------------------------------- // SESSION DNA — explanatory text driven by fingerprint stats // --------------------------------------------------------------------------- f_sessionDNA(string nameEn, string namePt, float expPct, float revPct, int n) => string nm = f_t(nameEn, namePt) if n < 10 nm + f_t(": building history (", ": ainda a construir histórico (") + str.tostring(n) + f_t(" sessions)", " sessões)") else nm + f_t(": expanded above average ", ": expansão acima da média em ") + str.tostring(expPct, "#") + f_t("% | reversed after open ", "% | reversão pós-abertura em ") + str.tostring(revPct, "#") + f_t("% (", "% (") + str.tostring(n) + f_t(" sessions)", " sessões)") londonDNA = f_sessionDNA("London", "Londres", londonExpPct, londonRevPct, array.size(londonStats.rangeHist)) nyDNA = f_sessionDNA("New York", "Nova York", nyExpPct, nyRevPct, array.size(nyStats.rangeHist)) // --------------------------------------------------------------------------- // DAILY OPEN BIAS // --------------------------------------------------------------------------- dOpen = request.security(syminfo.tickerid, "D", open, lookahead = barmerge.lookahead_off) biasText = close > dOpen ? f_t("Bullish (above open)", "Altista (acima da abertura)") : f_t("Bearish (below open)", "Baixista (abaixo da abertura)") // --------------------------------------------------------------------------- // SESSION HIGHLIGHTS // --------------------------------------------------------------------------- bgcolor(inAsia ? color.new(#2962FF, 94) : na, title = "Asia") bgcolor(inLondon ? color.new(#FF6D00, 94) : na, title = "London") bgcolor(inNY ? color.new(#AA00FF, 94) : na, title = "New York") bgcolor(inOverlap ? color.new(color.yellow, 88) : na, title = "Overlap") // --------------------------------------------------------------------------- // MAIN DASHBOARD // --------------------------------------------------------------------------- if showDashboard and barstate.islast var table dash = table.new(f_pos(dashPos), 2, 9, border_width = 1, bgcolor = color.new(color.black, 15)) table.cell(dash, 0, 0, "VERISCOPE SESSION MATRIX", text_color = color.white, bgcolor = color.new(color.black, 0), text_size = size.small) table.cell(dash, 1, 0, "", bgcolor = color.new(color.black, 0)) table.cell(dash, 0, 1, f_t("Current Session", "Sessão Atual"), text_color = color.gray) table.cell(dash, 1, 1, currentSessionText, text_color = color.white) table.cell(dash, 0, 2, f_t("Kill Zone", "Kill Zone"), text_color = color.gray) table.cell(dash, 1, 2, (inLondonKZ or inNYKZ) ? f_t("ACTIVE", "ATIVA") : f_t("no", "não"), text_color = (inLondonKZ or inNYKZ) ? color.lime : color.gray) table.cell(dash, 0, 3, f_t("Overlap", "Sobreposição"), text_color = color.gray) table.cell(dash, 1, 3, inOverlap ? f_t("YES", "SIM") : f_t("no", "não"), text_color = inOverlap ? color.yellow : color.gray) table.cell(dash, 0, 4, f_t("Volatility vs Avg", "Volatilidade vs Média"), text_color = color.gray) table.cell(dash, 1, 4, na(volRatio) ? "—" : str.tostring(volRatio, "#.0") + "%", text_color = na(volRatio) ? color.gray : (volRatio >= volAlertMult * 100 ? color.orange : color.white)) table.cell(dash, 0, 5, f_t("Attention Index", "Índice de Atenção"), text_color = color.gray) table.cell(dash, 1, 5, str.tostring(attentionIndex) + " / 100", text_color = attentionIndex >= 60 ? color.lime : attentionIndex >= 30 ? color.yellow : color.gray) table.cell(dash, 0, 6, f_t("Daily Bias", "Viés Diário"), text_color = color.gray) table.cell(dash, 1, 6, biasText, text_color = close > dOpen ? color.lime : color.red) table.cell(dash, 0, 7, f_t("London DNA", "DNA Londres"), text_color = color.gray, text_size = size.tiny) table.cell(dash, 1, 7, londonDNA, text_color = color.white, text_size = size.tiny) table.cell(dash, 0, 8, f_t("New York DNA", "DNA Nova York"), text_color = color.gray, text_size = size.tiny) table.cell(dash, 1, 8, nyDNA, text_color = color.white, text_size = size.tiny) // --------------------------------------------------------------------------- // WEEKLY HEATMAP (recalculated from visible chart history — no persistence) // --------------------------------------------------------------------------- if showHeatmap and barstate.islast var table heat = table.new(f_pos(heatmapPos), 8, 4, border_width = 1, bgcolor = color.new(color.black, 15)) dayNamesEn = array.from("Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat") dayNamesPt = array.from("Dom", "Seg", "Ter", "Qua", "Qui", "Sex", "Sáb") sessNames = array.from(f_t("Asia", "Ásia"), f_t("London", "Londres"), f_t("NY", "NY")) table.cell(heat, 0, 0, f_t("Session", "Sessão"), text_color = color.white, text_size = size.tiny) for d = 0 to 6 dayLabel = langInput == "Português" ? array.get(dayNamesPt, d) : array.get(dayNamesEn, d) table.cell(heat, d + 1, 0, dayLabel, text_color = color.white, text_size = size.tiny) maxAvg = 0.0 for b = 0 to 20 c = array.get(heatCount, b) if c > 0 a = array.get(heatSum, b) / c if a > maxAvg maxAvg := a for s = 0 to 2 table.cell(heat, 0, s + 1, array.get(sessNames, s), text_color = color.gray, text_size = size.tiny) for d = 0 to 6 bucket = d * 3 + s c = array.get(heatCount, bucket) if c > 0 and maxAvg > 0 a = array.get(heatSum, bucket) / c rank = a / maxAvg * 100.0 table.cell(heat, d + 1, s + 1, f_stars(rank), text_color = color.yellow, text_size = size.tiny) else table.cell(heat, d + 1, s + 1, "—", text_color = color.gray, text_size = size.tiny) // --------------------------------------------------------------------------- // SMART ALERTS // --------------------------------------------------------------------------- londonStrong = londonExpPct >= 55 nyStrong = nyExpPct >= 55 londonAlertOK = onlyAlertStrongFingerprint ? londonStrong : true nyAlertOK = onlyAlertStrongFingerprint ? nyStrong : true londonKZAlert = inLondonKZ and not inLondonKZ[1] and londonAlertOK and attentionIndex >= attentionAlertThreshold nyKZAlert = inNYKZ and not inNYKZ[1] and nyAlertOK and attentionIndex >= attentionAlertThreshold overlapAlert = inOverlap and not inOverlap[1] and attentionIndex >= attentionAlertThreshold alertcondition(londonKZAlert, "London Kill Zone — Strong Fingerprint", "London Kill Zone started with a historically strong fingerprint. Worth watching.") alertcondition(nyKZAlert, "New York Kill Zone — Strong Fingerprint", "New York Kill Zone started with a historically strong fingerprint. Worth watching.") alertcondition(overlapAlert, "London x NY Overlap — High Attention", "Overlap window started with a high Attention Index.") plot(attentionIndex, "Attention Index", display = display.data_window) plot(volRatio, "Vol Ratio", display = display.data_window)