Indicator · Pine Script v6 · On the chart

GEX Levels + Breakout Structure + Session AVWAP — ES/SPX/SPY

by tradelikeadealer

This indicator plots Gamma Exposure (GEX) levels, market breakout structures, and session anchors directly on the price chart for ES, SPX, and SPY.

View on GitHub

GitHub stars2on the repository
Pine versionv6current
Lines of code1,081
Settings40inputs
LicenceMIT
Last update6 weeks ago13 Sep 2026

Add It to Your TradingView Chart

  1. 1Copy the scriptPress Copy script above, or the copy button on the code below.
  2. 2Open the Pine EditorOpen a TradingView chart and choose Pine Editor at the bottom of the screen.
  3. 3Paste over the templateCreate a new indicator, select all of the template text and paste. Press Save and give it a name.
  4. 4Add it to the chartPress Add to chart, then adjust the settings with the gear icon next to its name.

Source Code

1,081 lines of Pine Script v6 from final/tradingview/gex_es_spx_spy.pine, published under MIT. Keep the author's credit and licence lines when you reuse it.

gex_es_spx_spy.pine
//@version=6
// TLADe GEX Levels — ES/SPX/SPY — release 3.5.0 (13 September 2026)
indicator("GEX Levels + Breakout Structure + Session AVWAP — ES/SPX/SPY", overlay=true, max_lines_count=500, max_labels_count=500, max_boxes_count=500)

// ╔══════════════════════════════════════════════════════════════════════════════╗
// ║  INPUTS                                                                      ║
// ╚══════════════════════════════════════════════════════════════════════════════╝

groupTicker = "Ticker Settings"
tickerChoice = input.string("ES", "Display Ticker", options=["ES", "SPX", "SPY"], group=groupTicker)
// Spread is read from the WEEKLY_DATA / live override string (S:NN.NN|L:...). No
// hardcoded default — if the string is missing the S: prefix the convert
// function falls back to identity (= ES strikes shown unchanged).
var float esSpxSpread = 0.0

// The indicator is an intraday tool. On charts above 1H (4H, D, W…) it draws nothing unless
// the user asks for it — GEX lines, profile and session boxes on a daily chart are noise.
showAboveH1 = input.bool(false, "Draw on timeframes above 1H", group=groupTicker, tooltip="Off: on 4H, Daily, Weekly and Monthly charts the indicator stays silent. Turn on to draw there anyway.")
bool tfActive = showAboveH1 or timeframe.in_seconds() <= 3600

groupGex = "GEX Data"
string WEEKLY_DATA = input.text_area("S:6.20|L:7688,ZG,ZERO GAMMA,Zero Gamma~Key pivot where dealer gamma flips~Price magnet,0;7726,MP,MAX PAIN,Max Pain~Strike with maximum option pain~Expiry target,0;7646,EH,EM HIGH,Expected Move HIGH~1-sigma upper boundary~Statistical resistance,0;7550,EL,EM LOW,Expected Move LOW~1-sigma lower boundary~Statistical support,0;7712,VH,VOL HIGH,Vol Band HIGH~Volatility-based resistance~ZG + 25% EM,0;7664,VL,VOL LOW,Vol Band LOW~Volatility-based support~ZG - 25% EM,0;7701,DF,Delta Flip,Delta Flip~Where net dealer delta flips sign~Directional pivot,0;7586,CM,Charm Magnet,Charm Magnet (Pressure)~Charm-driven intraday magnet~Pin risk,0;7656,PW,Put Wall,Strike: 7656~From Spot: +0.66%~Tot GEX: 1.45B~C/P Ratio: 0.4~Hold: 51% | Break: 49%,1450.6;7706,PW,Put Wall,Strike: 7706~From Spot: +1.31%~Tot GEX: 1.23B~C/P Ratio: 1.0~Hold: 46% | Break: 54%,1231.1;7606,PW,Put Wall,Strike: 7606~From Spot: +0.00%~Tot GEX: 934.63M~C/P Ratio: 0.2~Hold: 43% | Break: 57%,934.6;7651,PW,Put Wall,Strike: 7651~From Spot: +0.59%~Tot GEX: 629.15M~C/P Ratio: 0.2~Hold: 42% | Break: 58%,629.2;7631,PW,Put Wall,Strike: 7631~From Spot: +0.33%~Tot GEX: 587.93M~C/P Ratio: 0.1~Hold: 41% | Break: 59%,587.9;7686,PW,Put Wall,Strike: 7686~From Spot: +1.05%~Tot GEX: 518.87M~C/P Ratio: 0.6~Hold: 42% | Break: 58%,518.9;7636,PW,Put Wall,Strike: 7636~From Spot: +0.39%~Tot GEX: 492.92M~C/P Ratio: 0.1~Hold: 41% | Break: 59%,492.9;7661,PW,Put Wall,Strike: 7661~From Spot: +0.72%~Tot GEX: 459.64M~C/P Ratio: 0.2~Hold: 56% | Break: 44%,459.6;7681,PW,Put Wall,Strike: 7681~From Spot: +0.99%~Tot GEX: 450.57M~C/P Ratio: 0.9~Hold: 42% | Break: 58%,450.6;7641,PW,Put Wall,Strike: 7641~From Spot: +0.46%~Tot GEX: 391.01M~C/P Ratio: 0.3~Hold: 41% | Break: 59%,391.0;7696,CW,Call Wall,Strike: 7696~From Spot: +1.18%~Tot GEX: 385.72M~C/P Ratio: 1.1~Hold: 43% | Break: 57%,385.7;7666,PW,Put Wall,Strike: 7666~From Spot: +0.79%~Tot GEX: 360.38M~C/P Ratio: 0.4~Hold: 49% | Break: 51%,360.4;7616,PW,Put Wall,Strike: 7616~From Spot: +0.13%~Tot GEX: 359.00M~C/P Ratio: 0.1~Hold: 55% | Break: 45%,359.0;7626,PW,Put Wall,Strike: 7626~From Spot: +0.26%~Tot GEX: 327.56M~C/P Ratio: 0.3~Hold: 47% | Break: 53%,327.6;7646,PW,Put Wall,Strike: 7646~From Spot: +0.53%~Tot GEX: 315.93M~C/P Ratio: 0.5~Hold: 40% | Break: 60%,315.9;7506,PW,Put Wall,Strike: 7506~From Spot: -1.31%~Tot GEX: 2.22B~C/P Ratio: 0.0~Hold: 54% | Break: 46%,2215.5;7556,PW,Put Wall,Strike: 7556~From Spot: -0.66%~Tot GEX: 1.18B~C/P Ratio: 0.0~Hold: 53% | Break: 47%,1175.3;7496,PW,Put Wall,Strike: 7496~From Spot: -1.45%~Tot GEX: 885.92M~C/P Ratio: 0.0~Hold: 50% | Break: 50%,885.9;7576,PW,Put Wall,Strike: 7576~From Spot: -0.39%~Tot GEX: 756.00M~C/P Ratio: 0.0~Hold: 49% | Break: 51%,756.0;7551,PW,Put Wall,Strike: 7551~From Spot: -0.72%~Tot GEX: 629.31M~C/P Ratio: 0.0~Hold: 49% | Break: 51%,629.3;7586,PW,Put Wall,Strike: 7586~From Spot: -0.26%~Tot GEX: 596.11M~C/P Ratio: 0.0~Hold: 56% | Break: 44%,596.1;7581,PW,Put Wall,Strike: 7581~From Spot: -0.33%~Tot GEX: 494.97M~C/P Ratio: 0.2~Hold: 50% | Break: 50%,495.0;7561,PW,Put Wall,Strike: 7561~From Spot: -0.59%~Tot GEX: 427.38M~C/P Ratio: 0.0~Hold: 56% | Break: 44%,427.4;7541,PW,Put Wall,Strike: 7541~From Spot: -0.85%~Tot GEX: 386.98M~C/P Ratio: 0.0~Hold: 49% | Break: 51%,387.0;7596,PW,Put Wall,Strike: 7596~From Spot: -0.13%~Tot GEX: 374.95M~C/P Ratio: 0.1~Hold: 56% | Break: 44%,375.0;7546,PW,Put Wall,Strike: 7546~From Spot: -0.79%~Tot GEX: 361.67M~C/P Ratio: 0.1~Hold: 49% | Break: 51%,361.7;7566,PW,Put Wall,Strike: 7566~From Spot: -0.53%~Tot GEX: 348.89M~C/P Ratio: 0.1~Hold: 49% | Break: 51%,348.9;7531,PW,Put Wall,Strike: 7531~From Spot: -0.99%~Tot GEX: 309.25M~C/P Ratio: 0.0~Hold: 49% | Break: 51%,309.3;7571,PW,Put Wall,Strike: 7571~From Spot: -0.46%~Tot GEX: 233.41M~C/P Ratio: 0.0~Hold: 49% | Break: 51%,233.4;7526,PW,Put Wall,Strike: 7526~From Spot: -1.05%~Tot GEX: 198.17M~C/P Ratio: 0.1~Hold: 49% | Break: 51%,198.2|P:6306,-0.1,-1;6756,-0.2,-1;6806,-0.5,-1;6906,-0.1,-1;7006,-0.2,-1;7031,-0.2,-1;7056,-0.2,-1;7126,-0.1,-1;7156,-0.1,-1;7166,-0.4,-1;7206,-0.3,-1;7226,-0.3,-1;7256,-0.2,-1;7266,-0.7,-1;7276,-0.1,-1;7281,-0.1,-1;7306,-0.2,-1;7336,-0.4,-1;7346,-0.2,-1;7356,-0.4,-1;7381,-0.3,-1;7386,-0.1,-1;7406,-1.3,-1;7416,-0.1,-1;7426,-0.5,-1;7431,-0.3,-1;7436,-0.4,-1;7441,-0.5,-1;7446,-0.5,-1;7451,-0.2,-1;7456,-1.1,-1;7461,-0.3,-1;7466,-0.2,-1;7471,-0.4,-1;7476,-0.5,-1;7481,-0.7,-1;7486,-0.7,-1;7491,-0.5,-1;7496,-4.3,-1;7501,-0.4,-1;7506,-10.0,-1;7511,-0.3,-1;7516,-0.7,-1;7521,-0.5,-1;7526,-0.8,-1;7531,-1.4,-1;7536,-0.7,-1;7541,-1.8,-1;7546,-1.3,-1;7551,-3.0,-1;7556,-5.4,-1;7561,-2.0,-1;7566,-1.4,-1;7571,-1.0,-1;7576,-3.5,-1;7581,-1.7,-1;7586,-2.6,-1;7591,-0.5,-1;7596,-1.6,-1;7601,-0.6,-1;7606,-3.3,-1;7611,-0.9,-1;7616,-1.4,-1;7621,-0.7,-1;7626,-0.9,-1;7631,-2.2,-1;7636,-1.8,-1;7641,-0.9,-1;7646,-0.5,-1;7651,-2.2,-1;7656,-3.3,-1;7661,-1.4,-1;7666,-0.7,-1;7671,-0.6,-1;7676,-0.2,-1;7681,-0.1,-1;7686,-0.7,-1;7696,0.1,1;7701,0.3,1;7706,-0.1,-1;7711,-0.1,-1;7721,-0.1,-1;7726,1.1,1;7736,0.2,1;7741,0.1,1;7746,0.3,1;7751,-0.8,-1;7756,1.8,1;7761,2.4,1;7766,0.2,1;7771,0.3,1;7776,0.4,1;7781,0.4,1;7791,0.3,1;7796,0.1,1;7801,0.1,1;7806,0.5,1;7816,0.1,1;7826,0.1,1;7831,1.1,1;7836,0.7,1;7856,0.3,1;7861,0.2,1;7876,0.1,1;7881,0.1,1;7896,0.1,1;7906,0.6,1;7916,0.1,1;7926,0.1,1;7956,0.1,1;8106,0.1,1")
gexDataInput = input.text_area("", title="Live GEX Override", group=groupGex, tooltip="Paste live data to override weekly. Leave empty for weekly.")
string gexDataRaw = gexDataInput != "" ? gexDataInput : WEEKLY_DATA
// Robustness: iOS copy/paste can URL-encode the string ("|" -> "%7C",
// space -> "%20") and lowercase the leading "S:". Decode here so the parser
// reads it regardless of how it was copied. Guarded on "%7C" (level tooltips
// already contain "%", e.g. "Break: 20%"). No-op for a clean desktop paste.
string gexDataActive = gexDataRaw
if str.contains(gexDataActive, "%7C") or str.contains(gexDataActive, "%7c")
    gexDataActive := str.replace_all(gexDataActive, "%7C", "|")
    gexDataActive := str.replace_all(gexDataActive, "%7c", "|")
    gexDataActive := str.replace_all(gexDataActive, "%20", " ")
if str.startswith(gexDataActive, "s:")
    gexDataActive := "S:" + str.substring(gexDataActive, 2)

groupGexVis = "GEX Visibility"
showGexLevels = input.bool(true, "Call/Put Walls", group=groupGexVis)
showSystemLevels = input.bool(true, "System (ZG, MaxPain, EM, VolBands)", group=groupGexVis)
showStructureLevels = input.bool(false, "Structure (PDH/PDL/PWH/PWL)", group=groupGexVis)
showCharmMagnet = input.bool(false, "Charm Magnet", group=groupGexVis)
showDeltaFlip = input.bool(true, "Delta Flip", group=groupGexVis)
showProfile = input.bool(true, "GEX Profile Bars", group=groupGexVis)
maxLevelsChoice = input.string("10", "Max GEX Levels", options=["5", "10", "15", "20", "All"], group=groupGexVis)
int maxVisibleLevels = maxLevelsChoice == "All" ? 999 : int(str.tonumber(maxLevelsChoice))
showOnlyNear = input.bool(false, "Show only levels near price", group=groupGexVis)
nearPct = input.float(3.0, "  Radius (%)", step=0.5, minval=0.5, maxval=10.0, group=groupGexVis)
enableThreshold = input.bool(false, "Min GEX Threshold", group=groupGexVis)
gexThreshold = input.float(50.0, "  Min Magnitude (M)", step=10.0, group=groupGexVis)

groupProfile = "GEX Profile Style"
profileWidth = input.int(50, "Bar Width", group=groupProfile, inline="p1")
barThickness = input.int(8, "Bar Height", group=groupProfile, inline="p1")
profileOffset = input.int(15, "Offset", group=groupProfile)

groupBOS = "Breakout Structure (BOS)"
// One switch for the whole group, like Session Boxes and Session AVWAP. The per-timeframe
// toggles below say WHICH breakouts, this one says WHETHER — H1/H4/D are preselected.
showBOS = input.bool(false, "Show Breakout Structure", group=groupBOS)
showBOS_M = input.bool(false, "Monthly", group=groupBOS, inline="bos0")
showBOS_W = input.bool(false, "Weekly", group=groupBOS, inline="bos0")
showBOS_D = input.bool(true, "Daily", group=groupBOS, inline="bos1")
showBOS_H4 = input.bool(true, "4H", group=groupBOS, inline="bos1")
showBOS_H1 = input.bool(true, "1H", group=groupBOS, inline="bos2")
bosKeep = input.int(2, "Keep last N per timeframe", minval=1, maxval=10, group=groupBOS, tooltip="How many live breakouts to keep for each timeframe. When a new one forms, the oldest of that timeframe is dropped. Invalidated breakouts are removed regardless.")
bosLineWidth = input.int(2, "BOS Line Width", minval=1, maxval=4, group=groupBOS)
colorBOS_Bull = input.color(#22c55e, "Bull", group=groupBOS, inline="bosc")
colorBOS_Bear = input.color(#ef4444, "Bear", group=groupBOS, inline="bosc")

groupSess = "Session Boxes"
showSessions = input.bool(false, "Show Session Boxes", group=groupSess)
showAsia = input.bool(true, "Asia", group=groupSess, inline="s1")
showEU = input.bool(true, "Europe", group=groupSess, inline="s1")
showPre = input.bool(true, "Pre-Mkt", group=groupSess, inline="s1")
showUS = input.bool(true, "US RTH", group=groupSess, inline="s1")
colorAsia = input.color(color.new(#f59e0b, 90), "Asia", group=groupSess, inline="sc")
colorEU = input.color(color.new(#3b82f6, 90), "EU", group=groupSess, inline="sc")
colorPre = input.color(color.new(#a855f7, 92), "Pre", group=groupSess, inline="sc2")
colorUS = input.color(color.new(#22c55e, 92), "US", group=groupSess, inline="sc2")
showHistoricalSessions = input.bool(false, "Show Historical (prev days)", group=groupSess, tooltip="When off, only the current trading day is displayed.")

groupAVWAP = "Session AVWAP"
showAVWAP = input.bool(true, "Show Session AVWAPs", group=groupAVWAP)
showAvwapAsia = input.bool(true, "Asia VWAP", group=groupAVWAP, inline="av1")
showAvwapEU = input.bool(true, "EU VWAP", group=groupAVWAP, inline="av1")
showAvwapUS = input.bool(true, "US VWAP", group=groupAVWAP, inline="av1")
showAvwapPD = input.bool(true, "US Prev Day", group=groupAVWAP, inline="av1")
colorAvwapAsia = input.color(#f59e0b, "Asia", group=groupAVWAP, inline="avc")
colorAvwapEU = input.color(#3b82f6, "EU", group=groupAVWAP, inline="avc")
colorAvwapUS = input.color(#22c55e, "US", group=groupAVWAP, inline="avc")
colorAvwapPD = input.color(color.new(#22c55e, 50), "PD", group=groupAVWAP, inline="avc")
showHistoricalAVWAP = input.bool(false, "Show Historical (prev days)", group=groupAVWAP, tooltip="When off, only the current trading day is displayed.")
avwapWidth = input.int(2, "AVWAP Line Width", minval=1, maxval=4, group=groupAVWAP)
showAvwapLabels = input.bool(true, "Labels", group=groupAVWAP, inline="avopt")
showAvwapExtend = input.bool(true, "Extend Lines Right", group=groupAVWAP, inline="avopt")

groupCol = "Colors & Style"
colorScheme = input.string("Wall Street Classic", "Color Theme", options=["Wall Street Classic", "Boreal", "Lady Trader"], group=groupCol)
colorZeroGamma = input.color(#9ca3af, "Zero Gamma", group=groupCol, inline="sys1")
colorMaxPain = input.color(#ef4444, "Max Pain", group=groupCol, inline="sys1")
colorEM = input.color(#3b82f6, "Expected Move", group=groupCol, inline="sys2")
colorVolBand = input.color(#9ca3af, "Vol Bands", group=groupCol, inline="sys2")
colorStructure = input.color(#9ca3af, "Structure", group=groupCol)
colorCharm = input.color(#f97316, "Charm Magnet", group=groupCol)
colorDeltaFlip = input.color(#f59e0b, "Delta Flip", group=groupCol)
lineLeftExt = input.int(50, "Line Left Extension", minval=1, group=groupCol)
lineStyleInput = input.string("Dotted", "Line Style", options=["Solid", "Dashed", "Dotted"], group=groupCol)
lineWidth = input.int(2, "Line Width", minval=1, maxval=4, group=groupCol)
labelSide = input.string("Left", "Label Side", options=["Left", "Right"], group=groupCol, tooltip="Where level labels render. Right places them clear of both candles and the GEX profile bars (recommended on busy charts).")
barColorStyle = input.string("Theme Colors", "Profile Bar Color", options=["Theme Colors", "Greyscale"], group=groupCol)

groupConfluence = "Confluence Zones"
showConfluence = input.bool(false, "Show Confluence Zones", group=groupConfluence)
confluenceMinSize = input.int(3, "Min cluster size for box", minval=2, maxval=5, group=groupConfluence, tooltip="Box drawn only when cluster has ≥N levels")
confluenceEmPct = input.float(7.0, "Band Width (% of EM)", minval=1.0, maxval=30.0, step=0.5, group=groupConfluence, tooltip="Total band as a percentage of the EM range (EM High − EM Low), split half above and half below (7% = ±3.5%). No EM in the data → no confluence zones.")
colorConfluence3 = input.color(color.new(#facc15, 75), "3-level", group=groupConfluence, inline="cc1")
colorConfluence4 = input.color(color.new(#fb923c, 70), "4-level", group=groupConfluence, inline="cc1")
colorConfluence5 = input.color(color.new(#ef4444, 65), "5+ (UBER)", group=groupConfluence, inline="cc1")

groupAlerts = "Alerts"
enableAlerts = input.bool(false, "Enable Level Cross Alerts", group=groupAlerts)
alertWalls = input.bool(true, "Call/Put Walls", group=groupAlerts, inline="al1")
alertSystem = input.bool(true, "ZG / Max Pain / EM / Vol Bands", group=groupAlerts, inline="al1")
alertBOS = input.bool(true, "Breakouts", group=groupAlerts, inline="al1")

// ╔══════════════════════════════════════════════════════════════════════════════╗
// ║  CORE FUNCTIONS                                                              ║
// ╚══════════════════════════════════════════════════════════════════════════════╝

// Identity fallback when esSpxSpread is missing from the data string (= 0):
// shows ES strikes unchanged for SPX/SPY too. Better than applying a stale
// hardcoded default that would silently shift every level.
f_convertPrice(float esPrice) =>
    esSpxSpread <= 0 ? esPrice : tickerChoice == "SPX" ? esPrice - esSpxSpread : tickerChoice == "SPY" ? (esPrice - esSpxSpread) / 10.0 : esPrice

f_formatPrice(float esPrice) =>
    float cv = f_convertPrice(esPrice)
    tickerChoice == "SPY" ? str.tostring(cv, "#.##") : str.tostring(cv, "#")

f_lineStyle(string s) =>
    s == "Solid" ? line.style_solid : s == "Dashed" ? line.style_dashed : line.style_dotted

// One name per level. Labels read
// "price + name" — the code (CW, ZG, EH…) is for the feed and the tooltip, not the
// chart; showing both said the same thing twice ("7466 PW Put Wall").
f_levelName(string code, string feedLabel) =>
    switch code
        "CW" => "Call Wall"
        "PW" => "Put Wall"
        "ZG" => "Zero Gamma"
        "MP" => "Max Pain"
        "EH" => "EM High Globex"
        "EL" => "EM Low Globex"
        "EHR" => "EM High RTH"
        "ELR" => "EM Low RTH"
        "VH" => "Vol High"
        "VL" => "Vol Low"
        "CM" => "Charm Magnet"
        "DF" => "Delta Flip"
        "PDH" => "Previous Day High"
        "PDL" => "Previous Day Low"
        "PWH" => "Previous Week High"
        "PWL" => "Previous Week Low"
        => feedLabel

color colorPos = colorScheme == "Boreal" ? #22d3ee : colorScheme == "Lady Trader" ? #2dd4bf : #22c55e
color colorNeg = colorScheme == "Boreal" ? #f472b6 : colorScheme == "Lady Trader" ? #c084fc : #ef4444

// ╔══════════════════════════════════════════════════════════════════════════════╗
// ║  GEX STATE                                                                   ║
// ╚══════════════════════════════════════════════════════════════════════════════╝

var box[] gexBoxes = array.new_box()
var box[] confluenceBoxes = array.new_box()
var label[] confluenceLabels = array.new_label()
var line[] gexLines = array.new_line()
var label[] gexLabels = array.new_label()
var label timestampLabel = na
var string prevState = ""
// The GEX/system levels currently drawn (chart scale) — what the cross alerts watch.
var float[]  alertPrices = array.new_float()
var string[] alertNames  = array.new_string()
var string[] alertKinds  = array.new_string()
var int lastBarIdx = -1

// BOS state
var line[] bosLines = array.new_line()
var label[] bosLabels = array.new_label()
var float[] bosLevels = array.new_float()
var bool[] bosBullish = array.new_bool()
var string[] bosTF = array.new_string()

// Wall flip tracking (5-min close based — see WALL FLIP section below)
// Walls parsed from data: ES-units strike, type CW/PW, count of consecutive
// 5m closes beyond the strike in the "currently-out" direction, and the
// current flipped flag.
var float[]  wallStrikes  = array.new_float()
var string[] wallTypes    = array.new_string()
var int[]    wallCounters = array.new_int()
var bool[]   wallFlipped  = array.new_bool()
var string   wallParseKey = ""

// ╔══════════════════════════════════════════════════════════════════════════════╗
// ║  BREAKOUT STRUCTURE — M/W/D/H4/H1                                            ║
// ╚══════════════════════════════════════════════════════════════════════════════╝

// ta.change at top-level (Pine requirement)
bool mChanged  = ta.change(time("M")) != 0
bool wChanged  = ta.change(time("W")) != 0
bool dChanged  = ta.change(time("D")) != 0
bool h4Changed = ta.change(time("240")) != 0
bool h1Changed = ta.change(time("60")) != 0

// request.security — top-level calls. Always the LAST CLOSED bar of each timeframe
// (offset [1] with lookahead_on is the non-repainting idiom: the same value on
// historical and realtime chart bars). [2] is the bar before it. With the old
// `lookahead_off` + `close` the breakout was tested on the CLOSED bar in history
// but on the FIRST MINUTE of the developing bar in realtime — a repainting BO.
//   *C1/O1/H1/L1 = the bar that just closed, *H2/L2 = the bar before it
[mC1, mO1, mH1, mL1, mH2, mL2]       = request.security(syminfo.tickerid, "M",   [close[1], open[1], high[1], low[1], high[2], low[2]], lookahead=barmerge.lookahead_on)
[wC1, wO1, wH1, wL1, wH2, wL2]       = request.security(syminfo.tickerid, "W",   [close[1], open[1], high[1], low[1], high[2], low[2]], lookahead=barmerge.lookahead_on)
[dC1, dO1, dH1, dL1, dH2, dL2]       = request.security(syminfo.tickerid, "D",   [close[1], open[1], high[1], low[1], high[2], low[2]], lookahead=barmerge.lookahead_on)
[h4C1, h4O1, h4H1, h4L1, h4H2, h4L2] = request.security(syminfo.tickerid, "240", [close[1], open[1], high[1], low[1], high[2], low[2]], lookahead=barmerge.lookahead_on)
[h1C1, h1O1, h1H1, h1L1, h1H2, h1L2] = request.security(syminfo.tickerid, "60",  [close[1], open[1], high[1], low[1], high[2], low[2]], lookahead=barmerge.lookahead_on)
// Structure levels = last closed day / week (same series, so PDH is yesterday on every bar)
float dPrevH = dH1, float dPrevL = dL1
float wPrevH = wH1, float wPrevL = wL1

// ── 5-minute close stream — used by the Wall Flip tracker below ──
// We pull the 5-min close regardless of the chart's display timeframe, so the
// flip rule (2 consecutive 5m closes beyond the wall) behaves identically on
// a 1m, 5m, 1h, or daily chart.
[close5m, time5m] = request.security(syminfo.tickerid, "5", [close, time], lookahead=barmerge.lookahead_off)
bool new5mBar = ta.change(time5m) != 0


// Breakout on the bar that just closed: its close beyond the previous bar's extreme, with
// the body past the level larger than the shadow left behind.
f_checkBOS(float prevH, float prevL, float c, float h, float l, bool changed, bool enabled) =>
    float bullLevel = na
    float bearLevel = na
    if enabled and changed and not na(prevH) and not na(c)
        if c > prevH
            float bodyAbove = c - prevH
            float upperShadow = h - c
            if bodyAbove > upperShadow and bodyAbove > 0
                bullLevel := prevH
        if c < prevL
            float bodyBelow = prevL - c
            float lowerShadow = c - l
            if bodyBelow > lowerShadow and bodyBelow > 0
                bearLevel := prevL
    [bullLevel, bearLevel]

[mBull, mBear]   = f_checkBOS(mH2, mL2, mC1, mH1, mL1, mChanged, showBOS and showBOS_M and tfActive)
[wBull, wBear]   = f_checkBOS(wH2, wL2, wC1, wH1, wL1, wChanged, showBOS and showBOS_W and tfActive)
[dBull, dBear]   = f_checkBOS(dH2, dL2, dC1, dH1, dL1, dChanged, showBOS and showBOS_D and tfActive)
[h4Bull, h4Bear] = f_checkBOS(h4H2, h4L2, h4C1, h4H1, h4L1, h4Changed, showBOS and showBOS_H4 and tfActive)
[h1Bull, h1Bear] = f_checkBOS(h1H2, h1L2, h1C1, h1H1, h1L1, h1Changed, showBOS and showBOS_H1 and tfActive)

// BO label: triangles by timeframe (▲ H1, ▲▲ H4, ▲▲▲ D — here
// also ▲▲▲▲ W and ▲▲▲▲▲ M), then "BO L/S <tf>", then the price. Before it read "H1+",
// which told the timeframe but not the side.
f_boLabel(bool isBull, string tf, float price) =>
    string t = isBull ? "▲" : "▼"
    int n = tf == "M" ? 5 : tf == "W" ? 4 : tf == "D" ? 3 : tf == "H4" ? 2 : 1
    string tri = ""
    for i = 1 to n
        tri += t
    tri + " BO " + (isBull ? "L" : "S") + " " + tf + " " + str.tostring(price, format.mintick)

// Drop the oldest live breakout of a timeframe once it holds bosKeep of them (arrays are in
// creation order, so the first match is the oldest).
f_trimBOS(string tf) =>
    int kept = 0
    for i = 0 to array.size(bosTF) - 1
        if array.get(bosTF, i) == tf
            kept += 1
    while kept >= bosKeep
        int oldest = -1
        for i = 0 to array.size(bosTF) - 1
            if oldest < 0 and array.get(bosTF, i) == tf
                oldest := i
        if oldest < 0
            break
        line.delete(array.get(bosLines, oldest))
        label.delete(array.get(bosLabels, oldest))
        array.remove(bosLines, oldest)
        array.remove(bosLabels, oldest)
        array.remove(bosLevels, oldest)
        array.remove(bosBullish, oldest)
        array.remove(bosTF, oldest)
        kept -= 1

// Add BOS
f_addBOS(float level, bool isBull, string tf) =>
    if not na(level)
        if array.size(bosTF) > 0
            f_trimBOS(tf)
        float cv = f_convertPrice(level)
        color clr = isBull ? colorBOS_Bull : colorBOS_Bear
        int sty = tf == "M" or tf == "W" or tf == "D" ? 0 : 1
        int w = tf == "M" ? bosLineWidth + 2 : tf == "W" ? bosLineWidth + 1 : tf == "D" ? bosLineWidth : math.max(bosLineWidth - 1, 1)
        line newL = line.new(bar_index, cv, bar_index + 1, cv, color=clr, style=sty == 0 ? line.style_solid : line.style_dashed, width=w, extend=extend.right)
        array.push(bosLines, newL)
        label newLbl = label.new(bar_index, cv, f_boLabel(isBull, tf, cv), color=color.new(clr, 50), textcolor=color.white, style=isBull ? label.style_label_up : label.style_label_down, size=size.small)
        array.push(bosLabels, newLbl)
        array.push(bosLevels, cv)
        array.push(bosBullish, isBull)
        array.push(bosTF, tf)

f_addBOS(mBull, true, "M")
f_addBOS(mBear, false, "M")
f_addBOS(wBull, true, "W")
f_addBOS(wBear, false, "W")
f_addBOS(dBull, true, "D")
f_addBOS(dBear, false, "D")
f_addBOS(h4Bull, true, "H4")
f_addBOS(h4Bear, false, "H4")
f_addBOS(h1Bull, true, "H1")
f_addBOS(h1Bear, false, "H1")

// Invalidation: a breakout dies when a bar OF ITS OWN TIMEFRAME, in the
// opposite direction, closes back through the level — a long BO H1 stays alive until a bearish
// H1 bar closes below it. Not any 1-minute close on a 1-minute chart, which is what `close < lvl`
// did before. Checked once per timeframe, on the bar that just closed.
f_boKilled(bool isBull, float lvl, bool changed, float c, float o) =>
    changed and not na(c) and (isBull ? (c < lvl and c < o) : (c > lvl and c > o))

if array.size(bosLines) > 0
    int i = array.size(bosLines) - 1
    while i >= 0
        float lvl = array.get(bosLevels, i)
        bool isBull = array.get(bosBullish, i)
        string tf = array.get(bosTF, i)
        bool broken = tf == "M" ? f_boKilled(isBull, lvl, mChanged, mC1, mO1) : tf == "W" ? f_boKilled(isBull, lvl, wChanged, wC1, wO1) : tf == "D" ? f_boKilled(isBull, lvl, dChanged, dC1, dO1) : tf == "H4" ? f_boKilled(isBull, lvl, h4Changed, h4C1, h4O1) : f_boKilled(isBull, lvl, h1Changed, h1C1, h1O1)
        if broken
            line.delete(array.get(bosLines, i))
            label.delete(array.get(bosLabels, i))
            array.remove(bosLines, i)
            array.remove(bosLabels, i)
            array.remove(bosLevels, i)
            array.remove(bosBullish, i)
            array.remove(bosTF, i)
        i -= 1

// ╔══════════════════════════════════════════════════════════════════════════════╗
// ║  WALL FLIP TRACKING — 5m close based                                         ║
// ╚══════════════════════════════════════════════════════════════════════════════╝
//
// Rule (per wall, single state per side):
//   CW non-flipped  →  2 consecutive 5m closes ABOVE strike  →  flipped
//   CW flipped      →  2 consecutive 5m closes BELOW strike  →  restored
//   PW non-flipped  →  2 consecutive 5m closes BELOW strike  →  flipped
//   PW flipped      →  2 consecutive 5m closes ABOVE strike  →  restored
//
// A flipped wall keeps its original nature color (CW pink / PW cyan) but is
// rendered with width-1 + a "↺" suffix on its label. Tooltip carries the
// "Flipped X→Y" explanation. Same wall, same identity — only its current role
// switched relative to spot.

// Parse walls from gexDataActive when the data changes. The arrays mirror the
// order in which CW/PW strikes appear, so the index used by tracking stays
// stable across bars.
f_parseWalls(string dataStr) =>
    array.clear(wallStrikes)
    array.clear(wallTypes)
    array.clear(wallCounters)
    array.clear(wallFlipped)
    string rest = dataStr
    if str.startswith(rest, "S:")
        int sEnd = str.pos(rest, "|")
        if sEnd > 2
            rest := str.substring(rest, sEnd + 1)
    string lvlSection = rest
    if str.contains(rest, "|P:")
        int pp = str.pos(rest, "|P:")
        lvlSection := str.substring(rest, 0, pp)
    if str.startswith(lvlSection, "L:")
        lvlSection := str.substring(lvlSection, 2)
    if lvlSection != ""
        string[] pairs = str.split(lvlSection, ";")
        for pair in pairs
            string[] p = str.split(pair, ",")
            if array.size(p) >= 2
                float s = str.tonumber(array.get(p, 0))
                string t = array.get(p, 1)
                if not na(s) and (t == "CW" or t == "PW")
                    array.push(wallStrikes, s)
                    array.push(wallTypes, t)
                    array.push(wallCounters, 0)
                    array.push(wallFlipped, false)

// Lookup helper — returns the current flipped state for a given ES-units strike.
// Falls back to false (not flipped) when the wall isn't tracked.
f_getWallFlipped(float strike) =>
    bool found = false
    int n = array.size(wallStrikes)
    int i = 0
    while i < n and not found
        if array.get(wallStrikes, i) == strike
            found := array.get(wallFlipped, i)
            i := n
        else
            i += 1
    found

// Re-parse walls when the data string changes (= user pastes a live override
// or weekly seed updates). Cheap to call; only the wallStrikes arrays touched.
if wallParseKey != gexDataActive
    wallParseKey := gexDataActive
    f_parseWalls(gexDataActive)

// On every closed 5m bar, evaluate each wall against close5m and bump the
// counter when the close is on the "currently-out" side. Two consecutive
// out-side closes flip (or restore) the wall.
// Note: strikes are in ES units. close5m is in chart-display units, so for
// SPX/SPY we lift it back into ES space before the comparison.
if new5mBar and array.size(wallStrikes) > 0
    float close5mES = close5m
    if esSpxSpread > 0 and tickerChoice == "SPX"
        close5mES := close5m + esSpxSpread
    else if esSpxSpread > 0 and tickerChoice == "SPY"
        close5mES := close5m * 10.0 + esSpxSpread
    int n = array.size(wallStrikes)
    for i = 0 to n - 1
        float strike = array.get(wallStrikes, i)
        string wType = array.get(wallTypes, i)
        bool   isF   = array.get(wallFlipped, i)
        int    cnt   = array.get(wallCounters, i)
        bool beyond = false
        if wType == "CW"
            // Not flipped: CW sits above price → "beyond" = close above strike.
            // Flipped     : CW sits below price → "beyond" = close below strike.
            beyond := isF ? close5mES < strike : close5mES > strike
        else if wType == "PW"
            // Not flipped: PW sits below price → "beyond" = close below strike.
            // Flipped     : PW sits above price → "beyond" = close above strike.
            beyond := isF ? close5mES > strike : close5mES < strike
        if beyond
            cnt += 1
            if cnt >= 2
                isF := not isF
                cnt := 0
        else
            cnt := 0
        array.set(wallCounters, i, cnt)
        array.set(wallFlipped, i, isF)

// ╔══════════════════════════════════════════════════════════════════════════════╗
// ║  SESSION BOXES                                                               ║
// ╚══════════════════════════════════════════════════════════════════════════════╝

int etHour = hour(time, "America/New_York")
int etMin  = minute(time, "America/New_York")
int etMins = etHour * 60 + etMin

// Session detection (wraps midnight for Asia)
bool inAsia = etMins >= 1080 or etMins < 180    // 18:00-03:00
bool inEU   = etMins >= 180 and etMins < 480    // 03:00-08:00
bool inPre  = etMins >= 480 and etMins < 570    // 08:00-09:30
bool inUS   = etMins >= 570 and etMins < 960    // 09:30-16:00

// "Current day" = the LAST futures day on the chart (18:00 ET → 17:00 ET), derived from
// last_bar_time. It used to be "bars younger than 30 hours of wall-clock", which emptied
// the chart of every AVWAP and session box on weekends and holidays.
int lastEtMins = hour(last_bar_time, "America/New_York") * 60 + minute(last_bar_time, "America/New_York")
int lastDayStart = last_bar_time - (lastEtMins >= 1080 ? lastEtMins - 1080 : lastEtMins + 360) * 60000
bool inLastDay = time >= lastDayStart

// Session box tracking with proper lifecycle
var box curAsiaBox = na, var float _asiaHi = na, var float _asiaLo = na, var bool _asiaActive = false
var box curEUBox = na,   var float _euHi = na,   var float _euLo = na,   var bool _euActive = false
var box curPreBox = na,  var float _preHi = na,  var float _preLo = na,  var bool _preActive = false
var box curUSBox = na,   var float _usHi = na,   var float _usLo = na,   var bool _usActive = false

// Limit session boxes to the current (last) futures day unless showHistoricalSessions is on
bool recentSessionOnly = showHistoricalSessions or inLastDay

// Asia
if tfActive and showSessions and showAsia and recentSessionOnly
    if inAsia
        if not _asiaActive
            _asiaActive := true
            _asiaHi := high
            _asiaLo := low
            curAsiaBox := box.new(bar_index, high, bar_index, low, border_color=color.new(colorAsia, 70), border_width=1, bgcolor=colorAsia)
        else
            _asiaHi := math.max(_asiaHi, high)
            _asiaLo := math.min(_asiaLo, low)
            if not na(curAsiaBox)
                box.set_right(curAsiaBox, bar_index)
                box.set_top(curAsiaBox, _asiaHi)
                box.set_bottom(curAsiaBox, _asiaLo)
    else
        _asiaActive := false

// Europe
if tfActive and showSessions and showEU and recentSessionOnly
    if inEU
        if not _euActive
            _euActive := true
            _euHi := high
            _euLo := low
            curEUBox := box.new(bar_index, high, bar_index, low, border_color=color.new(colorEU, 70), border_width=1, bgcolor=colorEU)
        else
            _euHi := math.max(_euHi, high)
            _euLo := math.min(_euLo, low)
            if not na(curEUBox)
                box.set_right(curEUBox, bar_index)
                box.set_top(curEUBox, _euHi)
                box.set_bottom(curEUBox, _euLo)
    else
        _euActive := false

// Pre-Market
if tfActive and showSessions and showPre and recentSessionOnly
    if inPre
        if not _preActive
            _preActive := true
            _preHi := high
            _preLo := low
            curPreBox := box.new(bar_index, high, bar_index, low, border_color=color.new(colorPre, 70), border_width=1, bgcolor=colorPre)
        else
            _preHi := math.max(_preHi, high)
            _preLo := math.min(_preLo, low)
            if not na(curPreBox)
                box.set_right(curPreBox, bar_index)
                box.set_top(curPreBox, _preHi)
                box.set_bottom(curPreBox, _preLo)
    else
        _preActive := false

// US RTH
if tfActive and showSessions and showUS and recentSessionOnly
    if inUS
        if not _usActive
            _usActive := true
            _usHi := high
            _usLo := low
            curUSBox := box.new(bar_index, high, bar_index, low, border_color=color.new(colorUS, 70), border_width=1, bgcolor=colorUS)
        else
            _usHi := math.max(_usHi, high)
            _usLo := math.min(_usLo, low)
            if not na(curUSBox)
                box.set_right(curUSBox, bar_index)
                box.set_top(curUSBox, _usHi)
                box.set_bottom(curUSBox, _usLo)
    else
        _usActive := false

// ╔══════════════════════════════════════════════════════════════════════════════╗
// ║  SESSION AVWAP (Anchored VWAP per session)                                    ║
// ╚══════════════════════════════════════════════════════════════════════════════╝

// AVWAP — uses same session flags as boxes (no wall-clock dependency)
// Each AVWAP resets at its session start and accumulates through US close (16:00).
// All AVWAPs reset at new futures day (Asia start = 18:00 ET).

float src = hlc3
float vol = nz(volume, 1)  // fallback: zero-volume bars get weight 1

// Session start = first bar IN session after being OUT
bool asiaStart = inAsia and not inAsia[1]
bool euStart   = inEU   and not inEU[1]
bool usStart   = inUS   and not inUS[1]

// Track current futures day — reset flag at Asia start
var bool inCurrentDay = false
if asiaStart
    inCurrentDay := true

// Between 16:00-18:00 ET = gap, no futures day
bool inGap = etMins >= 960 and etMins < 1080
if inGap
    inCurrentDay := false

// Asia AVWAP — resets at Asia start, accumulates through US close
var float aSumPV = 0.0
var float aSumV  = 0.0
var bool asiaActive = false
if asiaStart
    aSumPV := src * vol
    aSumV  := vol
    asiaActive := true
else if asiaActive and inCurrentDay
    aSumPV += src * vol
    aSumV  += vol
float avwapAsia = aSumV > 0 ? aSumPV / aSumV : na

// EU AVWAP — resets at EU start, accumulates through US close
var float eSumPV = 0.0
var float eSumV  = 0.0
var bool euActive = false
if asiaStart
    euActive := false  // new day: EU hasn't started yet
if euStart
    eSumPV := src * vol
    eSumV  := vol
    euActive := true
else if euActive and inCurrentDay
    eSumPV += src * vol
    eSumV  += vol
float avwapEU = eSumV > 0 ? eSumPV / eSumV : na

// US AVWAP — resets at US start, accumulates through US close only
var float uSumPV = 0.0
var float uSumV  = 0.0
var bool usActive = false
if asiaStart
    usActive := false  // new day: US hasn't started yet
if usStart
    uSumPV := src * vol
    uSumV  := vol
    usActive := true
else if usActive and inUS
    uSumPV += src * vol
    uSumV  += vol
float avwapUS = uSumV > 0 ? uSumPV / uSumV : na

// US Previous Day AVWAP — promoted at futures day reset, accumulates on all bars
var float pdSumPV = 0.0
var float pdSumV  = 0.0
if asiaStart and uSumV > 0
    // Promote current US → PD before reset
    pdSumPV := uSumPV
    pdSumV  := uSumV
else if pdSumV > 0 and inCurrentDay
    pdSumPV += src * vol
    pdSumV  += vol
float avwapPD = pdSumV > 0 ? pdSumPV / pdSumV : na

// Show AVWAP only when active. When showHistoricalAVWAP is off, limit to the current (last) futures day.
bool recentOnly = showHistoricalAVWAP or inLastDay
float pAsia = tfActive and showAVWAP and showAvwapAsia and asiaActive and inCurrentDay and recentOnly ? avwapAsia : na
float pEU   = tfActive and showAVWAP and showAvwapEU   and euActive   and inCurrentDay and recentOnly ? avwapEU   : na
float pUS   = tfActive and showAVWAP and showAvwapUS   and usActive   and inUS         and recentOnly ? avwapUS   : na
float pPD   = tfActive and showAVWAP and showAvwapPD   and pdSumV > 0 and inCurrentDay and recentOnly ? avwapPD   : na

plot(pAsia, "AVWAP Asia",    color=colorAvwapAsia, linewidth=avwapWidth, style=plot.style_linebr)
plot(pEU,   "AVWAP EU",      color=colorAvwapEU,   linewidth=avwapWidth, style=plot.style_linebr)
plot(pUS,   "AVWAP US",      color=colorAvwapUS,   linewidth=avwapWidth, style=plot.style_linebr)
plot(pPD,   "AVWAP US Prev", color=colorAvwapPD,   linewidth=avwapWidth, style=plot.style_linebr)

// ── AVWAP Extension Lines (dotted, extend right from last value) ──
var line avwapLineAsia = na
var line avwapLineEU   = na
var line avwapLineUS   = na
var line avwapLinePD   = na

if showAVWAP and showAvwapExtend and barstate.islast
    if showAvwapAsia and not na(pAsia)
        if na(avwapLineAsia)
            avwapLineAsia := line.new(bar_index, pAsia, bar_index + 1, pAsia, color=colorAvwapAsia, width=avwapWidth, style=line.style_dotted, extend=extend.right)
        else
            line.set_xy1(avwapLineAsia, bar_index, pAsia)
            line.set_xy2(avwapLineAsia, bar_index + 1, pAsia)

    if showAvwapEU and not na(pEU)
        if na(avwapLineEU)
            avwapLineEU := line.new(bar_index, pEU, bar_index + 1, pEU, color=colorAvwapEU, width=avwapWidth, style=line.style_dotted, extend=extend.right)
        else
            line.set_xy1(avwapLineEU, bar_index, pEU)
            line.set_xy2(avwapLineEU, bar_index + 1, pEU)

    if showAvwapUS and not na(pUS)
        if na(avwapLineUS)
            avwapLineUS := line.new(bar_index, pUS, bar_index + 1, pUS, color=colorAvwapUS, width=avwapWidth, style=line.style_dotted, extend=extend.right)
        else
            line.set_xy1(avwapLineUS, bar_index, pUS)
            line.set_xy2(avwapLineUS, bar_index + 1, pUS)

    if showAvwapPD and not na(pPD)
        if na(avwapLinePD)
            avwapLinePD := line.new(bar_index, pPD, bar_index + 1, pPD, color=colorAvwapPD, width=avwapWidth, style=line.style_dashed, extend=extend.right)
        else
            line.set_xy1(avwapLinePD, bar_index, pPD)
            line.set_xy2(avwapLinePD, bar_index + 1, pPD)

// ── AVWAP Labels ──
// Same anchor and same "price + name" format as every other label on the chart
// (labelSide: left of the candles, or right past the GEX profile). They used to
// sit 3 bars right of the last candle — under the price axis on most layouts,
// which is why nobody saw them.
var label avwapLblAsia = na
var label avwapLblEU   = na
var label avwapLblUS   = na
var label avwapLblPD   = na

f_avwapLabel(label lbl, float y, string name, color clr) =>
    label out = lbl
    int x = labelSide == "Right" ? bar_index + profileOffset + profileWidth + 5 : bar_index - lineLeftExt
    string txt = str.tostring(y, format.mintick) + " " + name
    if na(out)
        out := label.new(x, y, txt, style=label.style_label_left, size=size.small, textcolor=color.white, color=clr)
    else
        label.set_xy(out, x, y)
        label.set_text(out, txt)
    out

if showAVWAP and showAvwapLabels and barstate.islast
    if showAvwapAsia and not na(pAsia)
        avwapLblAsia := f_avwapLabel(avwapLblAsia, pAsia, "AVWAP Asia", colorAvwapAsia)
    if showAvwapEU and not na(pEU)
        avwapLblEU := f_avwapLabel(avwapLblEU, pEU, "AVWAP EU", colorAvwapEU)
    if showAvwapUS and not na(pUS)
        avwapLblUS := f_avwapLabel(avwapLblUS, pUS, "AVWAP US", colorAvwapUS)
    if showAvwapPD and not na(pPD)
        avwapLblPD := f_avwapLabel(avwapLblPD, pPD, "AVWAP US Prev Day", colorAvwapPD)

// ╔══════════════════════════════════════════════════════════════════════════════╗
// ║  GEX LEVELS + PROFILE (last bar only — same logic as GEX Dashboard v6.8)     ║
// ╚══════════════════════════════════════════════════════════════════════════════╝

string stateKey = gexDataActive + str.tostring(confluenceEmPct) + str.tostring(profileWidth) + str.tostring(barThickness) + str.tostring(profileOffset) + colorScheme + barColorStyle + lineStyleInput + str.tostring(lineWidth) + str.tostring(lineLeftExt) + tickerChoice
bool needsRedraw = barstate.islast and (stateKey != prevState or bar_index != lastBarIdx or not tfActive)

if needsRedraw
    // Clear previous
    for b in gexBoxes
        box.delete(b)
    array.clear(gexBoxes)
    for l in gexLines
        line.delete(l)
    array.clear(gexLines)
    for lbl in gexLabels
        label.delete(lbl)
    array.clear(gexLabels)
    if not na(timestampLabel)
        label.delete(timestampLabel)
        timestampLabel := na
    array.clear(alertPrices)
    array.clear(alertNames)
    array.clear(alertKinds)

    lastBarIdx := bar_index
    // silent on timeframes above 1H (drawings cleared above, nothing new drawn)
    string levelsData = ""
    string profileData = ""
    string rawInput = tfActive ? gexDataActive : ""
    float emHighDisp = na
    float emLowDisp = na
    if tfActive
        timestampLabel := label.new(bar_index + profileOffset, high + syminfo.mintick * 50, "TLADe " + tickerChoice, color=color.new(color.gray, 100), style=label.style_label_center, textcolor=color.new(color.gray, 40), size=size.tiny)

    // Parse S:|L:|P:
    if str.startswith(rawInput, "S:")
        int sEnd = str.pos(rawInput, "|")
        if sEnd > 2
            float parsedSpread = str.tonumber(str.substring(rawInput, 2, sEnd))
            if not na(parsedSpread) and parsedSpread > 0
                esSpxSpread := parsedSpread
            rawInput := str.substring(rawInput, sEnd + 1)

    if str.contains(rawInput, "|P:")
        int pipePos = str.pos(rawInput, "|P:")
        string beforePipe = str.substring(rawInput, 0, pipePos)
        profileData := str.substring(rawInput, pipePos + 3)
        if str.startswith(beforePipe, "L:")
            levelsData := str.substring(beforePipe, 2)
    else
        profileData := rawInput

    // ── Draw Levels (identical to GEX Dashboard v6.8) ──
    if levelsData != ""
        string[] levelPairs = str.split(levelsData, ";")
        int gexAboveDrawn = 0
        int gexBelowDrawn = 0
        int halfMax = math.ceil(maxVisibleLevels / 2)

        // Find closest GEX to spot (protected) — and the EM range, which sizes the confluence band
        float closestAbove = na, float closestAboveDist = 999999.0
        float closestBelow = na, float closestBelowDist = 999999.0
        for lvlPair in levelPairs
            string[] p = str.split(lvlPair, ",")
            if array.size(p) >= 3
                float s = str.tonumber(array.get(p, 0))
                string t = array.get(p, 1)
                if not na(s) and t == "EH"
                    emHighDisp := f_convertPrice(s)
                if not na(s) and t == "EL"
                    emLowDisp := f_convertPrice(s)
                if not na(s) and (t == "CW" or t == "PW")
                    float cv = f_convertPrice(s)
                    float dist = math.abs(cv - close)
                    if cv > close and dist < closestAboveDist
                        closestAboveDist := dist
                        closestAbove := cv
                    else if cv <= close and dist < closestBelowDist
                        closestBelowDist := dist
                        closestBelow := cv

        for lvlPair in levelPairs
            string[] p = str.split(lvlPair, ",")
            if array.size(p) >= 3
                float lvlStrike = str.tonumber(array.get(p, 0))
                string lvlType = array.get(p, 1)
                string lvlLabel = array.get(p, 2)
                string lvlTooltip = array.size(p) >= 4 ? str.replace_all(array.get(p, 3), "~", "\n") : ""
                float lvlMag = array.size(p) >= 5 ? str.tonumber(array.get(p, 4)) : 0.0

                if not na(lvlStrike)
                    float cv = f_convertPrice(lvlStrike)
                    bool inRange = not showOnlyNear or (math.abs(close - cv) / close * 100.0 <= nearPct)
                    bool isGex = lvlType == "CW" or lvlType == "PW"
                    bool isSys = lvlType == "ZG" or lvlType == "MP" or lvlType == "EH" or lvlType == "EL" or lvlType == "EHR" or lvlType == "ELR" or lvlType == "VH" or lvlType == "VL"
                    bool isStr = lvlType == "PDH" or lvlType == "PDL" or lvlType == "PWH" or lvlType == "PWL"
                    bool isCM = lvlType == "CM"
                    bool isDF = lvlType == "DF"
                    bool passThresh = not enableThreshold or not isGex or nz(lvlMag) >= gexThreshold or nz(lvlMag) == 0.0
                    bool isAbove = cv > close
                    bool isProtected = (not na(closestAbove) and cv == closestAbove) or (not na(closestBelow) and cv == closestBelow)

                    bool shouldShow = false
                    if isGex and showGexLevels and passThresh
                        shouldShow := isProtected or (isAbove ? gexAboveDrawn < halfMax : gexBelowDrawn < halfMax)
                    else if isSys and showSystemLevels
                        shouldShow := true
                    else if isCM and showCharmMagnet
                        shouldShow := true
                    else if isDF and showDeltaFlip
                        shouldShow := true
                    // isStr (PDH/PDL/PWH/PWL) intentionally NOT drawn from the data
                    // string — PA is now computed autonomously from the chart's own
                    // OHLC below, so it is always in chart scale (no conversion, no
                    // cross-family contamination). The PA in the payload is ignored.

                    if shouldShow and inRange
                        color lvlColor = color.gray
                        int sc = 0
                        string finalTT = lvlTooltip
                        bool isWallFlipped = false

                        if lvlType == "CW"
                            // Nature color is preserved regardless of flip state — a CW
                            // remains a CW (pink/colorNeg) whether acting as resistance
                            // or as flipped potential support. The flip is communicated
                            // via reduced width + "↺" label suffix + tooltip phrasing.
                            isWallFlipped := f_getWallFlipped(lvlStrike)
                            lvlColor := colorNeg
                            if not isProtected
                                if isAbove
                                    gexAboveDrawn += 1
                                else
                                    gexBelowDrawn += 1
                            finalTT += "\n" + (isWallFlipped ? "Flipped Res → Sup (2× 5m close beyond)" : "Resistance")
                        else if lvlType == "PW"
                            isWallFlipped := f_getWallFlipped(lvlStrike)
                            lvlColor := colorPos
                            if not isProtected
                                if isAbove
                                    gexAboveDrawn += 1
                                else
                                    gexBelowDrawn += 1
                            finalTT += "\n" + (isWallFlipped ? "Flipped Sup → Res (2× 5m close beyond)" : "Support")
                        else if lvlType == "ZG"
                            lvlColor := colorZeroGamma
                        else if lvlType == "MP"
                            lvlColor := colorMaxPain
                            sc := 2
                        else if lvlType == "EH" or lvlType == "EL" or lvlType == "EHR" or lvlType == "ELR"
                            lvlColor := colorEM
                            sc := 1
                        else if lvlType == "VH" or lvlType == "VL"
                            lvlColor := colorVolBand
                            sc := 2
                        else if isStr
                            lvlColor := colorStructure
                            sc := 2
                        else if lvlType == "CM"
                            lvlColor := colorCharm
                        else if lvlType == "DF"
                            lvlColor := colorDeltaFlip
                            sc := 1

                        var string ls = line.style_dotted
                        ls := sc == 2 ? line.style_dotted : sc == 1 ? line.style_dashed : f_lineStyle(lineStyleInput)
                        if isCM
                            ls := line.style_solid
                        // Width: flipped CW/PW lose 1pt (min 1) so the line reads as
                        // "weakened in its current role". The user's chosen lineWidth
                        // remains the baseline for non-flipped walls.
                        int useWidth = isWallFlipped ? math.max(lineWidth - 1, 1) : lineWidth
                        array.push(gexLines, line.new(bar_index - lineLeftExt, cv, bar_index + profileOffset, cv, color=lvlColor, style=ls, width=useWidth, extend=extend.both))
                        string flipSuffix = isWallFlipped ? " ↺" : ""
                        string dLabel = f_formatPrice(lvlStrike) + " " + f_levelName(lvlType, lvlLabel) + flipSuffix
                        // Label anchor: "Left" keeps the historical placement (= clear of
                        // candles thanks to lineLeftExt). "Right" places the label past the
                        // GEX profile bars so it overlaps neither candles nor profile.
                        int lblX = labelSide == "Right" ? bar_index + profileOffset + profileWidth + 5 : bar_index - lineLeftExt
                        array.push(gexLabels, label.new(lblX, cv, dLabel, color=lvlColor, style=label.style_label_left, textcolor=color.white, size=size.small, tooltip=finalTT))
                        if isGex or isSys
                            array.push(alertPrices, cv)
                            array.push(alertNames, dLabel)
                            array.push(alertKinds, isGex ? "wall" : "system")

    // ── Draw Price Action (PDH/PDL/PWH/PWL) ──
    // Computed on the chart's OWN OHLC (dPrevH/dPrevL = prior day, wPrevH/wPrevL =
    // prior week, from the top-level request.security). Always in chart scale — no
    // f_convertPrice, no cross-family contamination. Replaces the PA that used to
    // arrive (and leak across tickers) in the data string.
    if tfActive and showStructureLevels
        float[]  paPrices = array.from(dPrevH, dPrevL, wPrevH, wPrevL)
        string[] paNames  = array.from("Previous Day High", "Previous Day Low", "Previous Week High", "Previous Week Low")
        for pi = 0 to 3
            float paPrice = array.get(paPrices, pi)
            if not na(paPrice)
                array.push(gexLines, line.new(bar_index - lineLeftExt, paPrice, bar_index + profileOffset, paPrice, color=colorStructure, style=line.style_dotted, width=lineWidth, extend=extend.both))
                int paLblX = labelSide == "Right" ? bar_index + profileOffset + profileWidth + 5 : bar_index - lineLeftExt
                array.push(gexLabels, label.new(paLblX, paPrice, str.tostring(paPrice, format.mintick) + " " + array.get(paNames, pi), color=colorStructure, style=label.style_label_left, textcolor=color.white, size=size.small))

    // ── Draw Profile Bars ──
    if showProfile and profileData != ""
        string[] forzaPairs = str.split(str.replace_all(profileData, "\n", ""), ";")
        float maxAbsForza = 10.0
        float boxH = barThickness * syminfo.mintick
        int xZero = bar_index + profileOffset

        for pair in forzaPairs
            string[] parts = str.split(pair, ",")
            if array.size(parts) == 3
                float esStrike = str.tonumber(array.get(parts, 0))
                float forza = str.tonumber(array.get(parts, 1))
                float sign = str.tonumber(array.get(parts, 2))
                if not na(esStrike) and not na(forza)
                    float cvStrike = f_convertPrice(esStrike)
                    int barLen = int((forza / maxAbsForza) * profileWidth)
                    float intensity = math.abs(forza) / maxAbsForza
                    int fillT = int(80 - intensity * 50)
                    color barCall = barColorStyle == "Greyscale" ? #a0a0a0 : colorNeg
                    color barPut = barColorStyle == "Greyscale" ? #505050 : colorPos
                    color baseC = sign >= 0 ? barCall : barPut
                    array.push(gexBoxes, box.new(xZero, cvStrike + boxH / 2, xZero + barLen, cvStrike - boxH / 2, border_color=color.new(baseC, 0), border_width=1, bgcolor=color.new(baseC, fillT)))

    // ═════════════════════════════════════════════════════════════════
    // CONFLUENCE ZONES — cluster levels within a band sized on the EM range
    // ═════════════════════════════════════════════════════════════════
    for b in confluenceBoxes
        box.delete(b)
    array.clear(confluenceBoxes)
    for lbl in confluenceLabels
        label.delete(lbl)
    array.clear(confluenceLabels)

    // Band = % of the EM range: the same tolerance on a
    // 60-point day and on a 150-point day means something different in points and the same thing
    // in market terms. Without EH/EL in the data there is no band and no zone — not a default width.
    float emRange = (not na(emHighDisp) and not na(emLowDisp)) ? emHighDisp - emLowDisp : na
    if tfActive and showConfluence and not na(emRange) and emRange > 0
        float band = emRange * (confluenceEmPct / 100.0) / 2.0  // half-band (7% of EM → ±3.5%)

        // Collect ALL levels into parallel arrays (price + name for tooltip)
        float[] allLevels = array.new_float()
        string[] allNames = array.new_string()

        // GEX walls + system + structure from parsed data
        if levelsData != ""
            string[] lpairs = str.split(levelsData, ";")
            for lpair in lpairs
                string[] p = str.split(lpair, ",")
                if array.size(p) >= 2
                    float s = str.tonumber(array.get(p, 0))
                    string t = array.get(p, 1)
                    if not na(s)
                        // PDH/PDL/PWH/PWL excluded here — PA now comes from the
                        // chart's own OHLC (added below), not the data string.
                        bool isTracked = t == "CW" or t == "PW" or t == "ZG" or t == "MP" or t == "EH" or t == "EL" or t == "EHR" or t == "ELR" or t == "VH" or t == "VL"
                        if isTracked
                            array.push(allLevels, f_convertPrice(s))
                            string nm = array.size(p) >= 3 ? array.get(p, 2) : t
                            array.push(allNames, str.tostring(math.round(s)) + " " + f_levelName(t, nm))

        // BOS levels (already converted)
        if array.size(bosLevels) > 0
            for i = 0 to array.size(bosLevels) - 1
                array.push(allLevels, array.get(bosLevels, i))
                string tf = array.size(bosTF) > i ? array.get(bosTF, i) : ""
                bool isBull = array.get(bosBullish, i)
                array.push(allNames, str.tostring(math.round(array.get(bosLevels, i))) + " BO " + (isBull ? "L" : "S") + " " + tf)

        // AVWAP values (each counts as a separate level)
        if not na(pAsia)
            array.push(allLevels, pAsia)
            array.push(allNames, str.tostring(math.round(pAsia)) + " AVWAP Asia")
        if not na(pEU)
            array.push(allLevels, pEU)
            array.push(allNames, str.tostring(math.round(pEU)) + " AVWAP EU")
        if not na(pUS)
            array.push(allLevels, pUS)
            array.push(allNames, str.tostring(math.round(pUS)) + " AVWAP US")
        if not na(pPD)
            array.push(allLevels, pPD)
            array.push(allNames, str.tostring(math.round(pPD)) + " AVWAP US Prev Day")

        // Price Action from the chart's own OHLC (same source as the PA lines).
        if showStructureLevels
            if not na(dPrevH)
                array.push(allLevels, dPrevH)
                array.push(allNames, str.tostring(math.round(dPrevH)) + " Previous Day High")
            if not na(dPrevL)
                array.push(allLevels, dPrevL)
                array.push(allNames, str.tostring(math.round(dPrevL)) + " Previous Day Low")
            if not na(wPrevH)
                array.push(allLevels, wPrevH)
                array.push(allNames, str.tostring(math.round(wPrevH)) + " Previous Week High")
            if not na(wPrevL)
                array.push(allLevels, wPrevL)
                array.push(allNames, str.tostring(math.round(wPrevL)) + " Previous Week Low")

        // Parallel sort by price ascending (bubble sort to keep names in sync)
        int sz = array.size(allLevels)
        for i = 0 to sz - 2
            for j = 0 to sz - i - 2
                if array.get(allLevels, j) > array.get(allLevels, j + 1)
                    float tmpF = array.get(allLevels, j)
                    array.set(allLevels, j, array.get(allLevels, j + 1))
                    array.set(allLevels, j + 1, tmpF)
                    string tmpS = array.get(allNames, j)
                    array.set(allNames, j, array.get(allNames, j + 1))
                    array.set(allNames, j + 1, tmpS)

        // Walk and cluster: consecutive levels within band form a cluster
        int n = array.size(allLevels)
        if n >= 2
            int i = 0
            while i < n
                float clusterMin = array.get(allLevels, i)
                float clusterMax = clusterMin
                int size = 1
                string[] clusterItems = array.new_string()
                array.push(clusterItems, array.get(allNames, i))
                int j = i + 1
                while j < n
                    float next = array.get(allLevels, j)
                    if next - clusterMin <= 2 * band
                        clusterMax := next
                        size += 1
                        array.push(clusterItems, array.get(allNames, j))
                        j += 1
                    else
                        break
                if size >= confluenceMinSize
                    int scoreCapped = math.min(size, 5)
                    color boxColor = scoreCapped >= 5 ? colorConfluence5 : scoreCapped >= 4 ? colorConfluence4 : colorConfluence3
                    array.push(confluenceBoxes, box.new(bar_index - lineLeftExt, clusterMax, bar_index + profileOffset, clusterMin, border_color=color.new(boxColor, 50), border_width=1, bgcolor=boxColor))
                    string txt = scoreCapped >= 5 ? "UBER" : str.tostring(scoreCapped) + "x"
                    string tt = "Confluence " + str.tostring(size) + " levels:\n" + array.join(clusterItems, "\n")
                    array.push(confluenceLabels, label.new(bar_index + profileOffset, (clusterMax + clusterMin) / 2, txt, color=color.new(boxColor, 20), textcolor=color.white, style=label.style_label_left, size=size.small, tooltip=tt))
                i := j

    prevState := stateKey

// ╔══════════════════════════════════════════════════════════════════════════════╗
// ║  ALERTS                                                                      ║
// ╚══════════════════════════════════════════════════════════════════════════════╝

// GEX and system levels: a CLOSE that crosses the level (previous close on one side, this
// close on the other). Evaluated on bar close so a wick through the wall does not fire.
if enableAlerts and array.size(alertPrices) > 0 and barstate.isconfirmed
    for i = 0 to array.size(alertPrices) - 1
        float lvl = array.get(alertPrices, i)
        string kind = array.get(alertKinds, i)
        bool wanted = kind == "wall" ? alertWalls : alertSystem
        if wanted
            bool up = close[1] < lvl and close >= lvl
            bool down = close[1] > lvl and close <= lvl
            if up or down
                alert("TLADe " + array.get(alertNames, i) + " crossed " + (up ? "UP" : "DOWN"), alert.freq_once_per_bar_close)

// Breakout levels: touch-and-close through, as before.
if enableAlerts and alertBOS and array.size(bosLevels) > 0
    for i = 0 to array.size(bosLevels) - 1
        float price = array.get(bosLevels, i)
        string tf = array.get(bosTF, i)
        bool isBull = array.get(bosBullish, i)
        bool touched = high >= price and low <= price
        bool crossUp = touched and close >= price and (open < price or low < price)
        bool crossDown = touched and close <= price and (open > price or high > price)
        if crossUp or crossDown
            string dir = crossUp ? "UP" : "DOWN"
            alert("TLADe BO " + (isBull ? "L" : "S") + " " + tf + " " + str.tostring(price, format.mintick) + " crossed " + dir, alert.freq_once_per_bar)

Overview

GEX Levels + Breakout Structure + Session AVWAP — ES/SPX/SPY renders key options analytics including Zero Gamma, Max Pain, Call and Put Walls, Volatility Bands, and Delta Flip lines parsed from structured data inputs. Additionally, it identifies multi-timeframe Breakout Structure levels and session-based anchored VWAP lines. Users can filter displayed levels by price proximity or magnitude and select options across multiple structure timeframes.

Details

Published by

tradelikeadealer on GitHub

Type

Indicator, Pine Script v6

Draws

On the price chart

Category

Support & resistance, Breakout, Sessions & time, Volatility & bands

Markets

Indices

Timeframes

M1-M5, M15-H1

Risk Notes

No repainting, lookahead or stacked entries were found in its code or README. That says nothing about how well it works: test it first.

Settings

The inputs you can change from the gear icon on the chart. Defaults are the author's.

SettingTypeDefaultVariable
Display TickerstringEStickerChoice
Draw on timeframes above 1HboolfalseshowAboveH1
WEEKLY_DATAtextS:6.20|L:7688,ZG,ZERO GAMMA,Zero Gamma~Key pivot where dealeWEEKLY_DATA
Live GEX OverridetextgexDataInput
Call/Put WallsbooltrueshowGexLevels
System (ZG, MaxPain, EM, VolBands)booltrueshowSystemLevels
Structure (PDH/PDL/PWH/PWL)boolfalseshowStructureLevels
Charm MagnetboolfalseshowCharmMagnet
Delta FlipbooltrueshowDeltaFlip
GEX Profile BarsbooltrueshowProfile
Max GEX Levelsstring10maxLevelsChoice
Show only levels near priceboolfalseshowOnlyNear
Show all 40 settings
SettingTypeDefaultVariable
Radius (%)float3.0nearPct
Min GEX ThresholdboolfalseenableThreshold
Min Magnitude (M)float50.0gexThreshold
Bar Widthint50profileWidth
Bar Heightint8barThickness
Offsetint15profileOffset
Show Breakout StructureboolfalseshowBOS
MonthlyboolfalseshowBOS_M
WeeklyboolfalseshowBOS_W
DailybooltrueshowBOS_D
4HbooltrueshowBOS_H4
1HbooltrueshowBOS_H1
Keep last N per timeframeint2bosKeep
BOS Line Widthint2bosLineWidth
Bullcolor#22c55ecolorBOS_Bull
Bearcolor#ef4444colorBOS_Bear
Show Session BoxesboolfalseshowSessions
AsiabooltrueshowAsia
EuropebooltrueshowEU
Pre-MktbooltrueshowPre
US RTHbooltrueshowUS
Asiacolorcolor.new(#f59e0b, 90)colorAsia
EUcolorcolor.new(#3b82f6, 90)colorEU
Precolorcolor.new(#a855f7, 92)colorPre
UScolorcolor.new(#22c55e, 92)colorUS
Show Historical (prev days)boolfalseshowHistoricalSessions
Show Session AVWAPsbooltrueshowAVWAP
Asia VWAPbooltrueshowAvwapAsia
Third-party code. ForexR did not write, test or endorse this script, and the summary above was generated automatically from its code and README. Indicators describe the past, and a strategy's backtest can look better than live trading, especially when a script repaints. Check it in the Strategy Tester and on a demo account before you rely on it.

Similar Pine Scripts

Donchian Channels (DC)

by mihakralj
99 stars

This indicator calculates and plots Donchian Channels directly on the price chart. It determines the highest high and lowest low over a user-selected lookback period using an optimized monotonic deque algorithm, as well as a middle basis line computed as their

Indicator v6 Volatility & bandsSupport & resistance
Updated 4 weeks ago · Apache-2.0 GitHub repository of Donchian Channels (DC)
56 stars

This indicator marks forex trading sessions and opening ranges on the price chart. It can draw Sydney, Tokyo, Frankfurt, London, and New York session boxes or five- and fifteen-minute opening ranges, plus an optional opening channel and a day-open range. Day r

Indicator v5 Sessions & timeSupport & resistance Repaints
Updated 2 months ago · None stated GitHub repository of Day Trading Booster by DGT
47 stars

The Donchian Channels indicator plots the middle basis line as the average of the high and low over the user-defined period. It also draws the upper band as the highest high and the lower band as the lowest low across the same period on the price chart. The ma

Indicator v6 Volatility & bandsSupport & resistance
Updated 8 months ago · MIT GitHub repository of Donchian Channels (DCHANNEL)

AM TBR — NQ Stats

by TamaraKGreen
46 stars

The AM TBR indicator calculates statistical levels for NQ futures using standard deviation. It plots a sigma ladder showing touch levels at 0.5 to 2.0 standard deviations, marks touch points, displays Mean Absolute Error (MAE) zones after touches, and shows Me

Indicator v6 Volatility & bandsSupport & resistance
Updated 7 weeks ago · None stated GitHub repository of AM TBR — NQ Stats
43 stars

This indicator plots Donchian channel bands and a midline on the price chart. It finds the highest and lowest values over a user-set length, then draws the upper band, lower band, and their average as the basis. The source setting chooses high and low, or the

Indicator v4 Volatility & bandsBreakout
Updated 1 year ago · None stated GitHub repository of Donchian Channels Experiment

More indicators

Trade Your Charts With a Regulated Broker

Compare brokers that connect to TradingView, with spreads, minimum deposits and leverage side by side.

Compare brokers