Key Levels (Open, Premarket, & Yesterday)
// This source code is subject to the terms of the Mozilla Public License 2.0 at https://mozilla.org/MPL/2.0/
// © liquid-trader

// This indicator automatically identifies and draws high-probability support / resistance levels (key levels).
// Initially, it only tracked yesterdays highs / lows, premarket highs / lows, as well as yesterdays end of day
// moving averages. It has since expanded to capture other key levels traders commonly use.
// More here: https://www.tradingview.com/script/UtD0cGM2-Key-Levels-Premarket-Yesterday/

//@version=5
indicator("Key Levels (Open, Premarket, & Yesterday)", "Key Levels", overlay=true, max_lines_count=500, max_labels_count=500)



// ---------------------------------------------------- SETTINGS --------------------------------------------------- //

// Base Colors
none = color.new(color.black, 100), clr1 = color.gray, clr2 = color.orange, clr3 = color.blue, clr4 = color.fuchsia, clr5 = color.red, clr6 = color.aqua, clr7 = color.purple, clr8 = color.maroon, clr9 = color.lime

// Group Labels
g1 = "Recent High / Low Pairs", g2 = "Distant High / Low Pairs", g3 = "Single Values", g4 = "Moving Averages", g5 = "Custom Levels", g6 = "General Settings", g7 = "Price Proximity", g8 = "Market Hours"

// Todays  High / Low Settings
tdHiLo = input.bool(true, "Today                ", "High & low line colors. Enabling \"Mkt Hrs Only\" (Market Hours Only) limits todays high / low to the highest high and lowest low within regular trading hours.\n\nDisabling \"Mkt Hrs Only\" will include Premarket and After Hours levels, if they exceed the levels during market hours.", inline="tdHL", group=g1, display=display.none)
tdHiClr = input.color(color.new(clr2, 66), "", inline="tdHL", group=g1, display=display.none)
tdLoClr = input.color(color.new(clr2, 66), "", inline="tdHL", group=g1, display=display.none)
tdMktOnly = input.bool(true, "Mkt Hrs Only", inline="tdHL", group=g1, display=display.none)

// Opening Range Settings
orHiLo = input.bool(false, "Opening Range", "Line & Background colors, and the number of minutes that qualify as \"the open\" during the first market hour.", inline="orHL", group=g1, display=display.none)
orClr = input.color(color.new(clr6, 50), "", inline="orHL", group=g1, display=display.none)
orBoxClr = input.color(color.new(clr6, 95), "", inline="orHL", group=g1, display=display.none)
orRange = input.int(15, "", 1, 60, inline="orHL", group=g1, display=display.none), var box orBox = na

// Near Open High / Low Settings
noHiLo = input.bool(false, "Near Open        ", "High & low line colors, and the number of minutes before the open that qualifies as \"near the open\". If your chart settings have extended hours turned off, these will not display.", inline="noHL", group=g1, display=display.none)
noHiClr = input.color(clr5, "", inline="noHL", group=g1, display=display.none)
noLoClr = input.color(clr5, "", inline="noHL", group=g1, display=display.none)
noRange = input.int(60, "", 0, inline="noHL", group=g1, display=display.none)

// Premarket High / Low Settings
pmHiLo = input.bool(true, "Premarket         ", "High & low line colors. If your chart settings have extended hours turned off, these will not display.", inline="pmHL", group=g1, display=display.none)
pmHiClr = input.color(color.new(clr2, 33), "", inline="pmHL", group=g1, display=display.none)
pmLoClr = input.color(color.new(clr2, 33), "", inline="pmHL", group=g1, display=display.none)

// Overnight High / Low Settings
onHiLo = input.bool(false, "Overnight         ", "High & low line colors. If your chart settings have extended hours turned off, or you are not trading in a session that begins before midnight, these will not display.", inline="onHL", group=g1, display=display.none)
onHiClr = input.color(clr8, "", inline="onHL", group=g1, display=display.none)
onLoClr = input.color(clr8, "", inline="onHL", group=g1, display=display.none)

// After-Hours High / Low Settings
ahHiLo = input.bool(false, "After Hours       ", "High & low line colors. If your chart settings have extended hours turned off, these will not display.", inline="ahHL", group=g1, display=display.none)
ahHiClr = input.color(clr7, "", inline="ahHL", group=g1, display=display.none)
ahLoClr = input.color(clr7, "", inline="ahHL", group=g1, display=display.none)

// Yesterday High / Low Settings
ydHiLo = input.bool(true, "Yesterday         ", "High & low line colors. Enabling \"Mkt Hrs Only\" (Market Hours Only) limits yesterdays high / low to the highest high and lowest low within regular trading hours.\n\nDisabling \"Mkt Hrs Only\" will include Premarket and After Hours levels, if they exceed the levels during market hours.", inline="ydHL", group=g1, display=display.none)
ydHiClr = input.color(clr2, "", inline="ydHL", group=g1, display=display.none)
ydLoClr = input.color(clr2, "", inline="ydHL", group=g1, display=display.none)
ydMktOnly = input.bool(true, "Mkt Hrs Only", inline="ydHL", group=g1, display=display.none)

// This Weeks High / Low Settings
twHiLo = input.bool(false, "This Week        ", "High & low line colors.", inline="twHL", group=g2, display=display.none)
twHiClr = input.color(color.new(clr1, 50), "", inline="twHL", group=g2, display=display.none)
twLoClr = input.color(color.new(clr1, 50), "", inline="twHL", group=g2, display=display.none)

// Last Weeks High / Low Settings
lwHiLo = input.bool(false, "Last Week        ", "High & low line colors.", inline="lwHL", group=g2, display=display.none)
lwHiClr = input.color(color.new(clr1, 50), "", inline="lwHL", group=g2, display=display.none)
lwLoClr = input.color(color.new(clr1, 50), "", inline="lwHL", group=g2, display=display.none)

// This Months High / Low Settings
tmHiLo = input.bool(false, "This Month       ", "High & low line colors.", inline="tmHL", group=g2, display=display.none)
tmHiClr = input.color(color.new(clr1, 50), "", inline="tmHL", group=g2, display=display.none)
tmLoClr = input.color(color.new(clr1, 50), "", inline="tmHL", group=g2, display=display.none)

// Last Months High / Low Settings
lmHiLo = input.bool(false, "Last Month       ", "High & low line colors.", inline="lmHL", group=g2, display=display.none)
lmHiClr = input.color(color.new(clr1, 50), "", inline="lmHL", group=g2, display=display.none)
lmLoClr = input.color(color.new(clr1, 50), "", inline="lmHL", group=g2, display=display.none)

// This Quarters High / Low Settings
tqHiLo = input.bool(false, "This Quarter     ", "High & low line colors.", inline="tqHL", group=g2, display=display.none)
tqHiClr = input.color(color.new(clr1, 50), "", inline="tqHL", group=g2, display=display.none)
tqLoClr = input.color(color.new(clr1, 50), "", inline="tqHL", group=g2, display=display.none)

// Last Quarters High / Low Settings
lqHiLo = input.bool(false, "Last Quarter     ", "High & low line colors.", inline="lqHL", group=g2, display=display.none)
lqHiClr = input.color(color.new(clr1, 50), "", inline="lqHL", group=g2, display=display.none)
lqLoClr = input.color(color.new(clr1, 50), "", inline="lqHL", group=g2, display=display.none)

// This Years High / Low Settings
tyHiLo = input.bool(false, "This Year          ", "High & low line colors.", inline="tyHL", group=g2, display=display.none)
tyHiClr = input.color(color.new(clr1, 50), "", inline="tyHL", group=g2, display=display.none)
tyLoClr = input.color(color.new(clr1, 50), "", inline="tyHL", group=g2, display=display.none)

// Last Years High / Low Settings
lyHiLo = input.bool(false, "Last Year          ", "High & low line colors.", inline="lyHL", group=g2, display=display.none)
lyHiClr = input.color(color.new(clr1, 50), "", inline="lyHL", group=g2, display=display.none)
lyLoClr = input.color(color.new(clr1, 50), "", inline="lyHL", group=g2, display=display.none)

// Todays Opening Settings
O = "open", H = "high", L = "low", C = "close", HL2 = "hl2", HLC3 = "hlc3", OHLC4 = "ohlc4", HLCC4 = "hlcc4"
tdOpn = input.bool(false, "Todays Open        ", "Line color, and the value to use from todays first market bar.", inline="tdOpen", group=g3, display=display.none)
tdOpnClr = input.color(color.new(clr1, 50), "", inline="tdOpen", group=g3, display=display.none)
tdOpnSrc = input.string(O, "", [O, H, L, C, HL2, HLC3, OHLC4, HLCC4], inline="tdOpen", group=g3, display=display.none)

// Yesterdays Close Settings
ydCls = input.bool(false, "Yesterdays Close ", "Line color, and the value to use from yesterdays last market bar.", inline="ydClose", group=g3, display=display.none)
ydClsClr = input.color(color.new(clr1, 50), "", inline="ydClose", group=g3, display=display.none)
ydClsSrc = input.string(C, "", [O, H, L, C, HL2, HLC3, OHLC4, HLCC4], inline="ydClose", group=g3, display=display.none)

// Yesterdays Opening Settings
ydOpn = input.bool(false, "Yesterdays Open  ", "Line color, and the value to use from yesterdays first market bar.", inline="ydOpen", group=g3, display=display.none)
ydOpnClr = input.color(color.new(clr1, 50), "", inline="ydOpen", group=g3, display=display.none)
ydOpnSrc = input.string(O, "", [O, H, L, C, HL2, HLC3, OHLC4, HLCC4], inline="ydOpen", group=g3, display=display.none)

// Todays VWAP Settings
tdVWAP = input.bool(false, "Todays VWAP               ", "Line color of todays Volume Weighted Average Price.", inline="tdV", group=g4, display=display.none)
tdVpClr = input.color(clr3, "", inline="tdV", group=g4, display=display.none)

// Yesterday VWAP Settings
ydVWAP = input.bool(false, "Yesterdays EOD VWAP", "Line color of yesterdays end of day Volume Weighted Average Price.", inline="ydV", group=g4, display=display.none)
ydVpClr = input.color(color.new(clr3, 50), "", inline="ydV", group=g4, display=display.none)

// Todays MA Settings
tdMA = input.bool(false, "Todays MA                    ", "Line color of todays Moving Average. The MA parameters are below.", inline="tdM", group=g4, display=display.none)
tdMaClr = input.color(clr4, "", inline="tdM", group=g4, display=display.none)

// Yesterday MA Settings
ydMA = input.bool(false, "Yesterdays EOD MA     ", "Line color of yesterdays end of day Moving Average. The MA parameters are below.", inline="ydM", group=g4, display=display.none)
ydMaClr = input.color(color.new(clr4, 50), "", inline="ydM", group=g4, display=display.none)

// Moving Average Settings
maLen = input.int(135, "MA Params.", 1, tooltip="Length, source, and type of the Moving Average.",  inline="ma", group=g4, display=display.none)
maSrc = input.string(HLC3, "", [O, H, L, C, HL2, HLC3, OHLC4, HLCC4], inline="ma", group=g4, display=display.none)
maTyp = input.string("EMA", "", ["SMA", "EMA", "SMMA (RMA)", "WMA", "VWMA"], inline="ma", group=g4, display=display.none)

// Custom Level Settings
cstm1 = input.bool(false, "", "Title, price, and color of a custom level.", inline="c1", group=g5, display=display.none)
c1Lbl = input.string("Custom Level 1", "", inline="c1", group=g5, display=display.none)
c1Num = input.price(12.34, "", inline="c1", group=g5, display=display.none)
c1Clr = input.color(clr9, "", inline="c1", group=g5, display=display.none)
cstm2 = input.bool(false, "", "Title, price, and color of a custom level.", inline="c2", group=g5, display=display.none)
c2Lbl = input.string("Custom Level 2", "", inline="c2", group=g5, display=display.none)
c2Num = input.price(56.78, "", inline="c2", group=g5, display=display.none)
c2Clr = input.color(clr9, "", inline="c2", group=g5, display=display.none)

// Label Settings
showLabel = input.bool(true, "Show Labels","Label visibility, with options to abbreviate the text of the label and include the price of the level, as well as change the background & text colors", inline="labels", group=g6, display=display.none)
inclPrice = input.bool(true, "Incl. Price", inline="labels", group=g6, display=display.none)
abbreviate = input.bool(true, "Abrv.", inline="labels", group=g6, display=display.none)
lblClr = input.color(none, "", inline="labels", group=g6, display=display.none)
txtClr = input.color(clr1, "", inline="labels", group=g6, display=display.none)

// Older Level Settings
showOldLevels = input.bool(false, "Show older levels", "Visibility & color of older levels.\n\nOlder levels will not display if \"Toggle Visibility\" is also enabled.", inline="old", group=g6, display=display.none)
oldColor = input.color(clr1, "", inline="old", group=g6, display=display.none)

// Line Settings
truncate = input.bool(false, "Max line length    ", "Truncates the lines so they do not stretch back to their origin.", inline="max line length", group=g6, display=display.none)
truncLen = input.int(15, "", 0, inline="max line length", group=g6, display=display.none)
width = input.int(2, "Line Width & Style       ", 1, 10, 1, inline="lines", group=g6, display=display.none)
style = input.string("Solid", "", ["Solid", "Dashed", "Dotted"], inline="lines", group=g6, display=display.none), lineStyle = style == "Dotted" ? line.style_dotted : style == "Dashed" ? line.style_dashed : line.style_solid

// Price Proximity Settings
atrZone = input.bool(false, "ATR Zones", "This will show a 5 bar ATR zone around the level when price is proximal to a given line. Half the ATR is above the line, and half the ATR is below the line.", group=g7, display=display.none)
showProx = input.bool(false, "Toggle Visibility", "This will hide levels by default, and only show a level when price is proximal to a given line.", inline="prox vis", group=g7, display=display.none)
extend = input.bool(false, "Line Length Override", "This will extend the line back to its origin when price is proximal to a given line and \"Max Line Length\" is enabled.", inline="extend", group=g7, display=display.none)
prxRng = input.float(0.025, "Range of Proximity ( % )", 0, tooltip="How close price needs to be for an enabled proximity setting to take effect. This is a percentage (not monetary) value.", group=g7, display=display.none)

// Market Hours Settings
mktHrs = input.session("0930-1600", "Start / End Time", tooltip = "A 24 hour format, where 0930 is 9:30 AM, 1600 is 4:00 PM, etc.", group=g8, display=display.none)
zone = input("America/New_York", "Time Zone", "Any IANA time zone ID. Ex: America/New_York\n\nYou can also use \"syminfo.timezone\", which inherits the time zone of the exchange of the chart.", group=g8, display=display.none)
timezone = zone == "syminfo.timezone" ? syminfo.timezone : zone



// ----------------------------------------------------- CORE ------------------------------------------------------ //

// Declare object to store bar values. 
type barVals
    float High  = high
    float Low   = low
    int   Index = time
bar = barVals.new()

// Declare object to track key levels.
type level
    bool   InitState
    bool   Show
    color  Color
    string Name
    string ToolTip
    float  Level
    float  Price
    int    Index
    int    OldIndex
    line   Line
    label  Label
    box    Range
var keyLevels = array.new<level>()

// Methods for interacting with keyLevels.
method Get(int i) => keyLevels.get(i)
method set_field(int i, bool Show = na, float Level = na, int Index = na, int OldIndex = na) =>
    kl = Get(i)
    if not na(Show)
        kl.Show := Show
    if not na(Level)
        kl.Level := Level
    if not na(Index)
        kl.Index := Index
    if not na(OldIndex)
        kl.OldIndex := OldIndex

// Declare object to track indices within keyLevels.
type index
    int tdH = 0
    int tdL = 1
    int orH = 2
    int orL = 3
    int noH = 4
    int noL = 5
    int pmH = 6
    int pmL = 7
    int onH = 8
    int onL = 9
    int ahH = 10
    int ahL = 11
    int ydH = 12
    int ydL = 13
    int twH = 14
    int twL = 15
    int lwH = 16
    int lwL = 17
    int tmH = 18
    int tmL = 19
    int lmH = 20
    int lmL = 21
    int tqH = 22
    int tqL = 23
    int lqH = 24
    int lqL = 25
    int tyH = 26
    int tyL = 27
    int lyH = 28
    int lyL = 29
    int tdO = 30
    int ydC = 31
    int ydO = 32
    int tdVWAP = 33
    int ydVWAP = 34
    int tdMA = 35
    int ydMA = 36
    int custom1 = 37
    int custom2 = 38
var id = index.new()

// Functions to identify every other, and every 4th, index.
evenNum(i) => i % 2 == 0
thisWMY(i) => math.floor(i / 2) % 2 != 0

// Functions for initializing key level strings.
acronym(fullStr) =>
    wordList = str.split(fullStr, " "), firstCharacters = ""
    for word in wordList
        firstCharacters += str.match(word, "\\w")
    firstCharacters

rangeStrings(i, abrvStr, fullStr) =>
    ohlc = "", thisLast = "", possesive = ""
    switch i < id.tdO
        true  => ohlc := evenNum(i) ? "High" : "Low"
        false => ohlc := evenNum(i) ? "Open" : "Close"
    if i >= id.twH and i <= id.lyL
        thisLast := thisWMY(i) ? "This " : "Last "
    if i <= id.tdL or (i >= id.ydH and i <= id.ydO)
        possesive := "s"
    name = switch abbreviate
        true  => " " + acronym(thisLast) + abrvStr + acronym(ohlc) + " "
        false => " " + ohlc + " • " + thisLast + fullStr + " "
    tooltip = " " + thisLast + fullStr + possesive + " " + ohlc + " "
    [name, tooltip]

customName(fullStr) =>
    acronym = acronym(fullStr)
    " " + (abbreviate ? acronym : fullStr) + " "

// Initialize keyLevels array.
var Levels_Not_Initialized = true
if Levels_Not_Initialized

    // Create temporary arrays to help initialize the keyLevels object array.
    fullStr = array.from("Today", "Opening Range", "Near Open", "Premarket", "Overnight", "After Hours", "Yesterday", "Week", "Month", "Quarter", "Year")
    abrvStr = array.from("TD", "OR", "NO", "PM", "ON", "AH", "YD", "W", "M", "Q", "Y")
    colors  = array.from(tdHiClr, tdLoClr, orClr,  orClr,  noHiClr, noLoClr, pmHiClr, pmLoClr, onHiClr, onLoClr, ahHiClr, ahLoClr, ydHiClr, ydLoClr, twHiClr, twLoClr, lwHiClr, lwLoClr, tmHiClr, tmLoClr, lmHiClr, lmLoClr, tqHiClr, tqLoClr, lqHiClr, lqLoClr, tyHiClr, tyLoClr, lyHiClr, lyLoClr, tdOpnClr, ydClsClr, ydOpnClr, tdVpClr, ydVpClr, tdMaClr, ydMaClr, c1Clr, c2Clr)
    lvlVis  = array.from(tdHiLo,  tdHiLo,  orHiLo, orHiLo, noHiLo,  noHiLo,  pmHiLo,  pmHiLo,  onHiLo,  onHiLo,  ahHiLo,  ahHiLo,  ydHiLo,  ydHiLo,  twHiLo,  twHiLo,  lwHiLo,  lwHiLo,  tmHiLo,  tmHiLo,  lmHiLo,  lmHiLo,  tqHiLo,  tqHiLo,  lqHiLo,  lqHiLo,  tyHiLo,  tyHiLo,  lyHiLo,  lyHiLo,  tdOpn,    ydCls,    ydOpn,    tdVWAP,  ydVWAP,  tdMA,    ydMA,    cstm1, cstm2)
    
    // Set visibility.
    for v in lvlVis
        keyLevels.push(level.new(v,v))

    // Set strings.
    for [i, kl] in keyLevels
        if kl.Show
            kl.Color := colors.get(i)
            // Highs & Lows of Today & Yesterday
            if i <= id.ydL
                j = math.floor(i/2)
                [name, tooltip] = rangeStrings(i, abrvStr.get(j), fullStr.get(j))
                kl.Name := name
                kl.ToolTip := tooltip
            // Highs & Lows of This & Last Week, Month, & Year
            else if i > id.ydL and i <= id.lyL
                j = math.floor(i/4 + 3.5)
                [name, tooltip] = rangeStrings(i, abrvStr.get(j), fullStr.get(j))
                kl.Name := name
                kl.ToolTip := tooltip
            // Open & Close of Today & Yesterday
            else if i > id.lyL and i < id.tdVWAP
                j = (i == id.tdO ? id.tdH : id.ydH)/2
                [name, tooltip] = rangeStrings(i, abrvStr.get(j), fullStr.get(j))
                kl.Name := name
                kl.ToolTip := tooltip
            // VWAP, MA, and Custom Levels
            else 
                j = i - id.tdVWAP
                if j <= 3
                    full = evenNum(j) ? fullStr.get(id.tdH/2) : fullStr.get(id.ydH/2)
                    abrv = evenNum(j) ? abrvStr.get(id.tdH/2) : abrvStr.get(id.ydH/2)
                    day = abbreviate ? abrv : full
                    nStr = j < 2 ? "VWAP" : (abbreviate ? str.tostring(maTyp) : str.tostring(maTyp) + " " + str.tostring(maLen))
                    tStr = j < 2 ? "s VWAP " : "s " + str.tostring(maLen) + " " + str.tostring(maTyp) + " "
                    prefix = "", suffix = "", delim  = ""
                    if abbreviate
                        prefix := " " + day + " ", suffix := " ", delim := ""
                    else
                        prefix := " ", suffix := day + " ", delim := " • "
                    kl.Name := prefix + nStr + delim + suffix
                    kl.ToolTip := full + tStr
                else
                    cStr = j == 2 ? c1Lbl : c2Lbl
                    cLvl = j == 2 ? c1Num : c2Num
                    kl.Name := customName(cStr)
                    kl.ToolTip := cStr + " "
                    kl.Level := cLvl
    Levels_Not_Initialized := false



// --------------------------------------------------- TIME LOGIC -------------------------------------------------- //

// Function to convert bar_time into a bar_index.
numOfBars(n) =>
    timeframe.multiplier * (
     timeframe.isseconds ? 1000 :
     timeframe.isminutes ? 60000 :
     timeframe.isdaily   ? 86400000 :
     timeframe.isweekly  ? 604800000 :
     timeframe.ismonthly ? 2629800000 :
     1) * n

// Functions to normalize market times ranges.
normalizeTime(t) =>
    str = str.tostring(t)
    while str.length(str) < 4
        str := "0" + str
    str
setTimeNearOpen(t) =>
    r  = noRange
    hr = math.floor(t / 100)
    m  = t % 100, m := m - r
    if m < 0
        while m < 0
            hr -= 1
            m += 60
    normalizeTime(hr * 100 + m)

// Convert trader specified market time variables to strings.
marketHours = mktHrs
mktStartTime = str.substring(mktHrs, 0, 4)
mktEndTime = str.substring(mktHrs, 5, 9)
preMktHours = "0000-" + mktStartTime
nearMktOpen = setTimeNearOpen(str.tonumber(mktStartTime)) + "-" + mktStartTime
openingMins = mktStartTime + "-" + normalizeTime(str.tonumber(mktStartTime) + orRange)
minutesInDay = "1440"
mktCloseToMidnight = mktEndTime + "-2400"

// Define session segments.
premarket = not na(time(minutesInDay, preMktHours, timezone))
nearOpen = not na(time(minutesInDay, nearMktOpen, timezone))
marketOpen = not na(time(minutesInDay, openingMins, timezone))
market = not na(time(minutesInDay, marketHours, timezone))

newSession = session.isfirstbar, var newSessionBeforeMidnight = false
newSessionBeforeMidnight := newSession ? hour(time, timezone) * 60 + minute(time, timezone) < 1440 : market ? false : newSessionBeforeMidnight

afterHours = not na(time(minutesInDay, mktCloseToMidnight, timezone)) and not newSessionBeforeMidnight
overnight = not na(time(minutesInDay, mktCloseToMidnight, timezone)) and newSessionBeforeMidnight
firstPremarketBar = premarket and (not premarket[1] or newSession)
firstNearOpenBar = nearOpen and (not nearOpen[1] or newSession)
firstMarketBar = market and (not market[1] or newSession)
lastMarketBar = market[1] and (not market or newSession)
firstAfterHoursBar = afterHours and market[1]
firstOvernightBar = overnight and newSession
var beforeMarketOpen = false, beforeMarketOpen := newSession and not market ? true : market ? false : beforeMarketOpen
var firstMarketBarIndex = time, var lastMarketBarIndex = time

// Define annual segments.
newWeek    = timeframe.change("W")
newMonth   = timeframe.change("M")
newQuarter = timeframe.change("3M")
newYear    = timeframe.change("12M")

// Increment the session count. Used to disregard incomplete data from the first session on the chart.
var session = -1, session += newSession and session < 1 ? 1 : 0



// ----------------------------------------------- RECENT LEVEL LOGIC ---------------------------------------------- //

// Functions to update recent levels.
src(src) =>
    switch src
        O => open
        H => high
        L => low
        C => close
        HL2 => hl2
        HLC3 => hlc3
        OHLC4 => ohlc4
        HLCC4 => hlcc4

saveOldIndices(a, b) =>
    for i = a to b
        i.set_field(OldIndex = Get(i).Index)

set(i, lvl) =>
    i.set_field(Level = lvl, Index = bar.Index)

setHiLo(a, b) =>
    set(a, bar.High)
    set(b, bar.Low)

setHiLoLevels(firstBar, ifTrue, a, b) =>
    if firstBar
        saveOldIndices(a, b)
        setHiLo(a, b)
    if bar.High > Get(a).Level and ifTrue
        set(a, bar.High)
    if bar.Low < Get(b).Level and ifTrue
        set(b, bar.Low)
    
// Save certain bar indices that are necessary for showing old levels correctly, but may be overwritten.
if newSession
    saveOldIndices(id.tdH, id.onL)
    saveOldIndices(id.tdVWAP, id.custom2)
    saveOldIndices(id.tdO, id.tdO)
    
// Track opening range levels.
if market and orHiLo
    setHiLoLevels(firstMarketBar, marketOpen, id.orH, id.orL)

// Track near open levels.
if nearOpen and noHiLo
    setHiLoLevels(firstNearOpenBar, true, id.noH, id.noL)

// Track premarket levels.
if premarket and pmHiLo and ((not noHiLo) or (noHiLo and not nearOpen))
    setHiLoLevels(firstPremarketBar, not firstPremarketBar, id.pmH, id.pmL)

// Track overnight levels.
if overnight and onHiLo
    setHiLoLevels(firstOvernightBar, true, id.onH, id.onL)

// Track after-hours levels.
if afterHours and ahHiLo
    setHiLoLevels(firstAfterHoursBar, true, id.ahH, id.ahL)

// Track daily levels.
if not newSession
    // Todays Levels
    if (not tdMktOnly or (tdMktOnly and market)) and tdHiLo
        firstBar = tdMktOnly ? firstMarketBar : firstPremarketBar
        setHiLoLevels(firstBar, true, id.tdH, id.tdL)
    // Yesterdays Levels
    if (not ydMktOnly or (ydMktOnly and market)) and ydHiLo
        setHiLoLevels(false, true, id.ydH, id.ydL)

// Track the daily open.
if firstMarketBar and tdOpn
    set(id.tdO, src(tdOpnSrc))

// Track the daily close.
if lastMarketBar and ydCls
    set(id.ydC, src(ydClsSrc))

// Track todays VWAP
if tdVWAP
    id.tdVWAP.set_field(Level = ta.vwap)

// Track todays MA
movAvg = switch maTyp
    "SMA" => ta.sma(src(maSrc), maLen)
    "EMA" => ta.ema(src(maSrc), maLen)
    "SMMA (RMA)" => ta.rma(src(maSrc), maLen)
    "WMA" => ta.wma(src(maSrc), maLen)
    "VWMA" => ta.vwma(src(maSrc), maLen)
if tdMA
    id.tdMA.set_field(Level = movAvg, Index = bar.Index - numOfBars(maLen - 1))

// Track yesterdays EOD VWAP and MA.
if newSession
    if ydVWAP
        set(id.ydVWAP, ta.vwap[1])
    if ydMA
        set(id.ydMA, movAvg)

// Set first market bar index.
if firstMarketBar and not newSession
    firstMarketBarIndex := bar.Index
else if firstMarketBar[1] and newSession[1]
    firstMarketBarIndex := bar.Index[1]

// Set last market bar index.
if market[1] and (afterHours or newSession) 
    lastMarketBarIndex := bar.Index[1]



// ---------------------------------------------- DISTANT LEVEL LOGIC ---------------------------------------------- //

// Functions to update distant levels.
reqSec(tf) =>
    request.security(ticker.modify(syminfo.tickerid, session.extended), tf, [high, high[1], low, low[1]], lookahead=barmerge.lookahead_on)

getDistVal(i) =>
    wmy = math.floor(i/4 - id.twH/4)
    [thisHi, lastHi, thisLo, lastLo] = switch wmy 
        0 => reqSec("W")
        1 => reqSec("M")
        2 => reqSec("3M")
        3 => reqSec("12M")
    evenNum(i) ? thisWMY(i) ? thisHi : lastHi : thisWMY(i) ? thisLo: lastLo

newWMY(i) =>
    switch thisWMY(i)
        true  => (newWeek  and (i == id.twH or i == id.twL)) or (newMonth and (i == id.tmH or i == id.tmL)) or (newQuarter and (i == id.tqH or i == id.tqL)) or (newYear  and (i == id.tyH or i == id.tyL))
        false => (newWeek  and (i == id.lwH or i == id.lwL)) or (newMonth and (i == id.lmH or i == id.lmL)) or (newQuarter and (i == id.lqH or i == id.lqL)) or (newYear  and (i == id.lyH or i == id.lyL))

distLevelTouch(i, v) =>
    session.ismarket and (evenNum(i) ? bar.High >= v : bar.Low <= v)

// Track distant levels (this / last week, month, year).
for i = id.twH to id.lyL
    kl = Get(i)

    // Set value if empty, or if symbol has extended hours and it's the first market bar.
    if na(kl.Level) or (session.ispremarket[1] and session.ismarket)
        kl.Level := getDistVal(i)

    // If THIS week / month / year …
    if thisWMY(i)
        if newWMY(i) or distLevelTouch(i, kl.Level)
            kl.OldIndex := kl.Index
            kl.Level := getDistVal(i)
            kl.Index := bar.Index
            // If a new month starts in the middle of a weekly bar…
            if (i >= id.tmH and i <= id.tqL) and timeframe.isweekly and dayofmonth > 3 and not distLevelTouch(i, kl.Level)
                kl.Index := bar.Index[1]

    // If LAST week / month / year …
    else if kl.Show
        if newWMY(i)
            kl.Level := session.ispremarket ? Get(i - 2).Level : getDistVal(i)
            kl.Index := Get(i - 2).OldIndex



// -------------------------------------------- OBJECT UPDATE FUNCTIONS -------------------------------------------- //

labelString(kl, t) =>
    number = str.tostring(math.round_to_mintick(kl.Level)) + " "
    switch t
        "text"    => kl.Name + (inclPrice ? "• " + number : "" )
        "tooltip" => kl.ToolTip + "@ " + number

method newLine(level kl, int x1) =>
    kl.Line := line.new(x1, kl.Level, bar.Index + numOfBars(1), kl.Level, xloc.bar_time, extend.none, kl.Color, lineStyle, width)
    kl.Price := kl.Level

method newLabel(level kl) =>
    kl.Label := showLabel ? label.new(bar.Index + numOfBars(10), kl.Level, labelString(kl, "text"), xloc.bar_time, yloc.price, lblClr, label.style_label_left, txtClr, size.normal, text.align_center, labelString(kl, "tooltip")) : na

showFirstBarLevels(a, b) =>
    for i = a to b
        kl = Get(i)
        kl.newLine(kl.Index)
        kl.newLabel()

updateX(i, kl) =>
    if i != id.orH and i != id.orL
        kl.Line.set_x1(kl.Index)
    kl.Line.set_x2(bar.Index + numOfBars(1))
    kl.Label.set_x(bar.Index)

updateY(kl) => 
    kl.Price := kl.Level
    kl.Line.set_y1(kl.Level)
    kl.Line.set_y2(kl.Level)
    kl.Label.set_y(kl.Level)

updateLabelString(kl) =>
    kl.Label.set_text(labelString(kl, "text"))
    kl.Label.set_tooltip(labelString(kl, "tooltip"))

updateLevel(i, kl) =>
    updateX(i, kl)
    updateY(kl)
    updateLabelString(kl)

updateOpeningRange() =>
    orBox.set_right(bar.Index + numOfBars(10))
    if firstMarketBar
        orBox.set_left(bar.Index)
        orBox.set_border_width(0)
        orBox.set_border_color(none)
        orBox.set_bgcolor(orBoxClr)
    if marketOpen
        orBox.set_top(Get(id.orH).Level)
        orBox.set_bottom(Get(id.orL).Level)
    if truncate
        orBox.set_left(bar.Index - numOfBars(truncLen))

manageHiLo(firstBar, timeOfDay, a, b) =>
    // Turn on level visibility.
    if firstBar
        showFirstBarLevels(a, b)
    // Update levels.
    if timeOfDay
        A = Get(a)
        B = Get(b)
        if bar.High == A.Level
            updateLevel(a, A)
        if bar.Low == B.Level
            updateLevel(b, B)

manageDistHiLo(a) =>
    if evenNum(a)
        A = Get(a)
        if A.Show
            b = a + 1
            B = Get(b)
            updateLevel(a, A)
            updateLevel(b, B)

manageMA(a) =>
    if not evenNum(a)
        A = Get(a)
        if A.Show
            updateY(A)
            updateLabelString(A)
            if a == id.tdMA
                A.Line.set_x1(A.Index)

hideLevel(a, b) =>
    for i = a to b
        i.set_field(Show = false)



// ----------------------------------------------- NEW SESSION LOGIC ----------------------------------------------- //

// On the first bar of a new session with complete data…
if newSession and session > 0

    // Reset level visibility to their intial states.
    for kl in keyLevels
        kl.Show := kl.InitState
    
    // Disable levels that are not meaningful or measurable…
    if timeframe.isdwm
        // Disable intrabar levels.
        hideLevel(id.tdO, id.ydMA)
        hideLevel(id.tdH, timeframe.isweekly ? id.lwL : timeframe.ismonthly ? timeframe.multiplier < 3 ? id.lmL : id.lqL : id.ydL)
    else
        if market and market[1]
            // Disable all levels that are not part of market hours.
            hideLevel(id.noH, id.ahL)
        else
            if not afterHours[1]
                // Disable after hours levels.
                hideLevel(id.ahH, id.ahL)
            if market
                // Disable near open, premarket, and overnight levels.
                hideLevel(id.noH, id.onL)
            else if premarket
                // Disable overnight Levels.
                hideLevel(id.onH, id.onL)
        
    // If the trader wants levels from the previous session to remain visible…
    if showOldLevels and not showProx

        // Reformat opening range box.
        orBox.set_bgcolor(color.new(oldColor, 95))
        orBox.set_left(firstMarketBarIndex)
        orBox.set_right(lastMarketBarIndex)

        // Only reformat lines and labels of recent levels.
        for [i, kl] in keyLevels
            if (i < id.twH or i > id.lyL) and timeframe.isintraday
                openingRange = i == id.orH or i == id.orL
                // Lines
                kl.Line.set_x1(openingRange ? firstMarketBarIndex : kl.OldIndex)
                kl.Line.set_x2(openingRange ? lastMarketBarIndex  : bar.Index[1])
                kl.Line.set_color(oldColor)
                kl.Line.set_style(line.style_dotted)
                kl.Line.set_width(1)
                kl.Line.set_extend(extend.none)
                // Labels
                kl.Label.set_x(openingRange ? lastMarketBarIndex : bar.Index[1])
                kl.Label.set_color(none)
                kl.Label.set_textcolor(oldColor)
                kl.Label.set_style(label.style_label_lower_right)
            else
                kl.Line.delete()
                kl.Label.delete()
    else 
        // Otherwise, delete the old stuff.
        orBox.delete()
        for kl in keyLevels
            kl.Line.delete()
            kl.Label.delete()

    // Create new opening range box for current sesison.
    orBox := box.new(na, na, na, na, xloc = xloc.bar_time)

    // Create new levels for the current session.
    for [i, kl] in keyLevels
        if kl.Show
            x1 = i < id.tdVWAP ? kl.Index : bar.Index
            kl.newLine(x1)
            kl.newLabel()
            if i < id.ahH or i == id.tdO
                kl.Line.delete()
                kl.Label.delete()

    // Reset bar indices…
    for [i, kl] in keyLevels
        // If key level will extend into the next session…
        if (i >= id.ahH and i <= id.ydL) or (i >= id.tdO and i <= id.ydO)
            kl.OldIndex := kl.Index
        // If key level is recent…
        else if i < id.twH or i > id.lyL
            kl.Index := bar.Index



// ----------------------------------------------- BAR BY BAR LOGIC ------------------------------------------------ //

// Update todays levels.
if tdHiLo
    firstBar  = tdMktOnly ? firstMarketBar : firstPremarketBar
    timeOfDay = tdMktOnly ? market : premarket or market or afterHours
    manageHiLo(firstBar, timeOfDay, id.tdH, id.tdL)

// Update todays opening value and range.
if tdOpn
    manageHiLo(firstMarketBar, false, id.tdO, id.tdO)
if orHiLo
    manageHiLo(firstMarketBar, marketOpen, id.orH, id.orL)
    updateOpeningRange()

// Update near open, premarket, and overnight levels.
if beforeMarketOpen
    if noHiLo
        manageHiLo(firstNearOpenBar, nearOpen, id.noH, id.noL)
    if pmHiLo
        manageHiLo(firstPremarketBar, premarket and not (nearOpen and noHiLo), id.pmH, id.pmL)
    if onHiLo
        manageHiLo(firstOvernightBar, overnight, id.onH, id.onL)

// Update todays VWAP and MA.
for i = id.tdVWAP to id.tdMA
    manageMA(i)

// Update weekly, monthly, and yearly levels.
for i = id.twH to id.lyL
    manageDistHiLo(i)

// Allow custom level to extend left if chart is not intraday.
if timeframe.isdwm
    for i = id.custom1 to id.custom2
        kl = Get(i)
        if kl.Show
            kl.Line.set_x1(na)

// Hide after hours levels if they overlap yesterdays levels.
if newSession and ahHiLo and ydHiLo and not ydMktOnly
    for i = 0 to 1
        aftHrs = Get(id.ahH + i)
        daily  = Get(id.ydH + i)
        if aftHrs.Level == daily.Level
            aftHrs.Line.delete(), aftHrs.Label.delete()

// Adjust lines and labels.
for kl in keyLevels
    kl.Label.set_x(bar.Index + numOfBars(10))
    kl.Line.set_x2(bar.Index + numOfBars(10))
    if truncate
        kl.Line.set_x1(bar.Index - numOfBars(truncLen))

// Reset daily levels.
resetDailyLevels = newSession or firstPremarketBar
if resetDailyLevels or (firstMarketBar and tdMktOnly)
    setHiLo(id.tdH, id.tdL)
if resetDailyLevels or (firstMarketBar and ydMktOnly)
    setHiLo(id.ydH, id.ydL)
if firstMarketBar
    set(id.ydO, src(ydOpnSrc))



// ----------------------------------------- PRICE & LEVEL PROXIMITY LOGIC ----------------------------------------- //

// Proximity functions.
getLevelRange(kl) =>
    lvlRng   = kl.Price * (prxRng / 100)
    lvlRngHi = kl.Price + lvlRng
    lvlRngLo = kl.Price - lvlRng
    [kl.Price, lvlRng, lvlRngHi, lvlRngLo]

priceWithinLevelProximity(lvlRngHi, lvlRngLo) =>
    (bar.High < lvlRngHi and bar.High > lvlRngLo)
     or (bar.Low < lvlRngHi and bar.Low > lvlRngLo)
     or (bar.High > lvlRngHi and bar.Low < lvlRngLo)
     or (bar.High[1] < lvlRngHi and bar.High[1] > lvlRngLo)
     or (bar.Low[1] < lvlRngHi and bar.Low[1] > lvlRngLo)
     or (bar.High[1] > lvlRngHi and bar.Low[1] < lvlRngLo)

proximal(kl) =>
    [lvl, lvlRng, lvlRngHi, lvlRngLo] = getLevelRange(kl)
    proximal = priceWithinLevelProximity(lvlRngHi, lvlRngLo)
    
// If proximity settings are enabled…
if (truncate and extend) or showProx or atrZone
    // Set proximity ATR value.
    prxAtr = ta.atr(5) / 2
    // Check if bars are proximal to key levels.
    for [i, kl] in keyLevels
        if kl.Show
            if proximal(kl)
                if truncate and extend
                    // Override truncated line length.
                    x1 = i == id.orH or i == id.orL ? firstMarketBarIndex
                     : i == id.ydH or i == id.ydL ? kl.OldIndex
                     : kl.Index
                    kl.Line.set_x1(x1)
                if showProx
                    // Show level and label.
                    kl.Line.set_color(kl.Color)
                    labelColor = i < id.onH and overnight ? none : lblClr
                    textColor = i < id.onH and overnight ? none : txtClr
                    if showLabel
                        kl.Label.delete()
                        kl.newLabel()
                if atrZone
                    // Show average true range zone around level.
                    [lvl, lvlRng, lvlRngHi, lvlRngLo] = getLevelRange(kl)
                    boxBgClr = i < id.onH and overnight ? none : color.new(kl.Color, 90)
                    boxBorderClr = i < id.onH and overnight ? none : color.new(kl.Color, 80)
                    kl.Range.delete()
                    kl.Range := box.new(kl.Line.get_x1(), lvl + prxAtr, kl.Line.get_x2(), lvl - prxAtr, boxBorderClr, 1, bgcolor = boxBgClr, xloc = xloc.bar_time)
            else
                if truncate and extend
                    // Re-truncate lines.
                    kl.Line.set_x1(bar.Index - numOfBars(truncLen))
                if showProx
                    // Hide level and label.
                    orBox.delete()
                    kl.Line.set_color(none)
                    if showLabel
                        kl.Label.delete()
                if atrZone
                    // Hide ATR zone.
                    kl.Range.delete()
                    kl.Range := box.new(na, na, na, na, xloc = xloc.bar_time)