Symbols at Highs & Lows
// This source code is subject to the terms of the Mozilla Public License 2.0 at https://mozilla.org/MPL/2.0/
// © jmosullivan

// @version=5
// Created By jmosullivan
// MM Symbols Highs & Lows 

// For the chosen symbols, this shows a table that indicates (a) if price is near the high or low of 
// day, and (b) if price has broken above the high and low of yesterday. This can be a great indicator 
// that a trend day is occurring in the market. You can customize the indicator to use up to 7 symbols 
// of your choice. You can also customize the appearance so that it only displays when all symbols are 
// at their highs or lows or have broken yesterday's level. Finally, you can customize the % threshold 
// to use when measuring how close to the high/low of day price needs to be in order to be considered 
// "at high/low of day". 

indicator(title='MM Symbols Highs & Lows', shorttitle="MM Symbols HLs", overlay=true)

import jmosullivan/Table/1
import jmosullivan/Utils/3

// Constants

var COLOR_TRANSPARENT = color.rgb(255,255,255,100)
SPACE = "   "

// Configuration

grp4 = "Symbols"
sym1 = input.string(group=grp4, defval="QQQ", 	title="Symbol 1")
sym2 = input.string(group=grp4, defval="IWM", 	title="Symbol 2")
sym3 = input.string(group=grp4, defval="XLF", 	title="Symbol 3")
sym4 = input.string(group=grp4, defval="XLV", 	title="Symbol 4")
sym5 = input.string(group=grp4, defval="", 		title="Symbol 5")
sym6 = input.string(group=grp4, defval="", 		title="Symbol 6")
sym7 = input.string(group=grp4, defval="", 		title="Symbol 7")

grp = "Today's Highs & Lows"
showHLs = input.string(group=grp, defval="Always", title="When to Show", options=["Always", "When SOME are at highs/lows", "When ALL are at highs/lows", "Never"], tooltip="If you don't want to clutter up your charts, you can choose to only show the highs & lows when one or more of the symbols is within the range % of the high or low of day.")
showHLsContent = input.string(group=grp, defval="% in Range", title="What to Show", options=["Words", "% in Range", "Both"], tooltip="In the table, you can choose to just see the word 'High', 'Low', or 'Mid' indicating where price is in the range, or you can choose to see the current % price is away from the top or bottom of the range, or both.")
threshold100 = input.float(group=grp, defval=10, title="% of Day Range", tooltip='When a symbol is within this percentage of the high or low of day, it is considered to be "at the high" or "at the low".')
threshold = threshold100 / 100

grp2 = "Yesterday's Highs & Lows"
showYesterdayBreaks = input.string(group=grp2, defval="Only on Breaks", title="When to Show", options=["Always", "Only on Breaks", "Never"], tooltip="If you don't want to clutter up your charts, you can choose to only show breaks of yesterday's high/low when they occur, or not show them at all.")
showBreakContent = input.string(group=grp2, defval="Both", title="What to Show", options=["Words", "% from break", "Both"], tooltip="In the table, you can choose to just see the word 'Break' when the high or low of previous day is violated (and 'Held' when not violated), or you can choose to see the current % price is away from the breaking point, or both.")

grp3 = "Appearance"
showHeadings = input.bool(group=grp3, defval=true, inline="title", title="Show Headings")
friendlyTextSize = input.string(group=grp3, defval="Tiny", title="Size / Position    ", options=["Tiny", "Small", "Normal", "Large", "Huge"], inline="position and size")
textSize = Utils.getSizeFromString(friendlyTextSize)
friendlyPosition = input.string(group=grp3, defval="Top Right", title="", options=["Top Left", "Top Center", "Top Right", "Middle Left", "Middle Center", "Middle Right", "Bottom Left", "Bottom Center", "Bottom Right"], inline="position and size")
position = Utils.getPositionFromString(friendlyPosition)
verticalOffset = input.int(group=grp3, defval=0, title="Vertical Offset", minval=0, maxval=3, step=1, tooltip="The number of row-sized spaces to place the table away from the top or bottom (depending on the position chosen).")
colorBullishText = input.color(group=grp3, defval=color.rgb(5, 72, 8), title="Bullish Text / BG   ", inline="1")
colorBullish = input.color(group=grp3, defval=color.rgb(75, 255, 178), title="", inline="1")
colorModerateText = input.color(group=grp3, defval=color.white, title="Moderate Text / BG", inline="2")
colorModerate = input.color(group=grp3, defval=color.rgb(40, 98, 255), title="", inline="2")
colorBearishText = input.color(group=grp3, defval=color.white, title="Bearish Text / BG  ", inline="3")
colorBearish = input.color(group=grp3, defval=color.rgb(235, 46, 175), title="", inline="3")

// Types

type Level
    string highs = "Highs"
    string mids = "Mids"
    string lows = "Lows"
    string mixed = "Mixed"

type Globals
    float threshold = 0
    Level level = na
    string allAtLevel = ""
    int atHighsCount = 0
    int atLowsCount = 0
    int brokeYhodCount = 0
    int brokeYlodCount = 0

type Symbol
    string name
    float hod = 0
    float lod = 0
    float last = 0
    float yhod = 0
    float ylod = 0
    bool yhodBreak = false
    bool ylodBreak = false
    string level = "Mids"
    float percentInRange = 0

// Functions

debug(collection, position = position.bottom_left) =>
    t = array.from('symbol', 'hod', 'lod', 'yhod', 'ylod', 'yhod break', 'ylod break', 'level', 'percent in range')
    m = matrix.new<string>()
    matrix.add_row(m, 0, t)
    for symbol in collection
        matrix.add_row(m, matrix.rows(m), array.from(symbol.name, str.tostring(symbol.hod), str.tostring(symbol.lod), str.tostring(symbol.yhod), str.tostring(symbol.ylod), str.tostring(symbol.yhodBreak), str.tostring(symbol.ylodBreak), str.tostring(symbol.level), str.tostring(symbol.percentInRange)))

    Table.fromMatrix(m, position)

requestSymbol(symbolString, globals) =>
    [hod, lod, last, yhod, ylod] = request.security(symbolString, 'D', [high, low, close, high[1], low[1]])
    Symbol symbol = na
    if symbolString != '' and not na(hod)
        percentInRange = (last - lod) / (hod - lod)
        highThreshold = 1 - globals.threshold
        symbol := Symbol.new(
          symbolString, 
          hod, 
          lod, 
          last,
          yhod, 
          ylod, 
          hod > yhod, 
          lod < ylod, 
          percentInRange >= highThreshold ? globals.level.highs : percentInRange <= globals.threshold ? globals.level.lows : globals.level.mids,
          percentInRange
          )
    symbol

addSymbol(globals, collection, symbolString) =>
    symbol = requestSymbol(symbolString, globals)
    if not na(symbol)
        // Count the number of symbols out of the mid-range
        globals.atHighsCount += symbol.level == globals.level.highs ? 1 : 0
        globals.atLowsCount += symbol.level == globals.level.lows ? 1 : 0

        // Determine if all of the symbols are at a particular level together
        if globals.allAtLevel == ''
            globals.allAtLevel := symbol.level
        else if globals.allAtLevel != symbol.level and globals.allAtLevel != globals.level.mixed
            globals.allAtLevel := globals.level.mixed

        // Count how many symbols are above their highs/lows of yesterday
        globals.brokeYhodCount += symbol.yhodBreak ? 1 : 0
        globals.brokeYlodCount += symbol.ylodBreak ? 1 : 0

        // Add the symbol to the collection
        array.push(collection, symbol)

// Indicator Logic

// Create a globals object to track information about all of the symbols
level = Level.new()
globals = Globals.new(threshold, level)

// Add all of the symbols to a collection
collection = array.new<Symbol>()
addSymbol(globals, collection, sym1)
addSymbol(globals, collection, sym2)
addSymbol(globals, collection, sym3)
addSymbol(globals, collection, sym4)
addSymbol(globals, collection, sym5)
addSymbol(globals, collection, sym6)
addSymbol(globals, collection, sym7)

// Determine if the highs/lows have been touched by any of the names

var hlsTouched = false
var yhodBreaks = false
var ylodBreaks = false
if session.isfirstbar_regular
    hlsTouched := false
    yhodBreaks := false
    ylodBreaks := false

if not hlsTouched and globals.atHighsCount > 0 or globals.atLowsCount > 0
    hlsTouched := true

if not yhodBreaks and globals.brokeYhodCount > 0
    yhodBreaks := true

if not ylodBreaks and globals.brokeYlodCount > 0
    ylodBreaks := true

someAtHLs = showHLs == "When SOME are at highs/lows" and (globals.atHighsCount > 0 or globals.atLowsCount > 0)
allAtHLs = showHLs == "When ALL are at highs/lows" and globals.allAtLevel != globals.level.mixed
_showHLs = showHLs == "Always" or someAtHLs or allAtHLs
_showYHODBreaks = showYesterdayBreaks == "Always" or (showYesterdayBreaks == "Only on Breaks" and yhodBreaks)
_showYLODBreaks = showYesterdayBreaks == "Always" or (showYesterdayBreaks == "Only on Breaks" and ylodBreaks)
showSymbols = _showHLs or _showYHODBreaks or _showYLODBreaks

// Loop through the collection building a matrix to be rendered
if barstate.islast and timeframe.in_seconds() <= timeframe.in_seconds('D')
    m = matrix.new<Table.Cell>()
    symArray = array.new<Table.Cell>()
    hlArray = array.new<Table.Cell>()
    yhodArray = array.new<Table.Cell>()
    ylodArray = array.new<Table.Cell>()
    color bgColor = na
    color textColor = na
    if showHeadings
        if showSymbols
            array.push(symArray, Table.Cell.new('HLs', colorBearish, colorBearishText))
        if _showHLs
            hlsBullish = globals.atHighsCount > 0 and globals.atLowsCount == 0
            hlsBearish = globals.atLowsCount > 0 and globals.atHighsCount == 0
            bgColor := hlsBullish ? colorBullish : hlsBearish ? colorBearish : colorModerate
            textColor := hlsBullish ? colorBullishText : hlsBearish ? colorBearishText : colorModerateText
            array.push(hlArray, Table.Cell.new('Range', bgColor, textColor))

        if _showYHODBreaks
            bgColor := globals.brokeYhodCount > 0 ? colorBullish : colorModerate
            textColor := globals.brokeYhodCount > 0 ? colorBullishText : colorModerateText
            array.push(yhodArray, Table.Cell.new('YHOD', bgColor, textColor))
        
        if _showYLODBreaks
            bgColor := globals.brokeYlodCount > 0 ? colorBearish : colorModerate
            textColor := globals.brokeYlodCount > 0 ? colorBearishText : colorModerateText
            array.push(ylodArray, Table.Cell.new('YLOD', bgColor, textColor))

    for symbol in collection
        if showSymbols
            array.push(symArray, Table.Cell.new(str.upper(symbol.name), colorModerate, colorModerateText))

        if _showHLs
            // Add the HL cell
            isBullish = symbol.level == globals.level.highs
            isBearish = symbol.level == globals.level.lows
            bgColor := isBullish ? colorBullish : isBearish ? colorBearish : colorModerate
            textColor := isBullish ? colorBullishText : isBearish ? colorBearishText : colorModerateText
            hlTemplate = switch showHLsContent
                "Both" => "{0}\n{1,number,percent}"
                "Words" => "{0}"
                => "{1,number,percent}"
            content = str.format(hlTemplate, symbol.level, symbol.percentInRange)
            array.push(hlArray, Table.Cell.new(content, bgColor, textColor))

        breakTemplate = switch showBreakContent
            "Words" => "{0}"
            "Both" => "{0}\n{1,number,#.##%}"
            => "{1,number,#.##%}"

        if _showYHODBreaks
            // Add the YHOD cell
            percentAway = (symbol.last - symbol.yhod) / symbol.yhod
            bgColor := symbol.yhodBreak ? colorBullish : colorModerate
            textColor := symbol.yhodBreak ? colorBullishText : colorModerateText
            word = symbol.yhodBreak ? 'Break' : 'Held'
            array.push(yhodArray, Table.Cell.new(str.format(breakTemplate, word, percentAway), bgColor, textColor))

        if _showYLODBreaks
            // Add the YLOD cell
            percentAway = (symbol.last - symbol.ylod) / symbol.ylod
            bgColor := symbol.ylodBreak ? colorBearish : colorModerate
            textColor := symbol.ylodBreak ? colorBearishText : colorModerateText
            word = symbol.ylodBreak ? 'Break' : 'Held'
            array.push(ylodArray, Table.Cell.new(str.format(breakTemplate, word, percentAway), bgColor, textColor))

    if showSymbols
        matrix.add_row(m, 0, symArray)
    if _showHLs
        matrix.add_row(m, matrix.rows(m), hlArray)
    if _showYHODBreaks
        matrix.add_row(m, matrix.rows(m), yhodArray)
    if _showYLODBreaks
        matrix.add_row(m, matrix.rows(m), ylodArray)
    
    // And render the table
    if matrix.rows(m) > 0
        Table.fromMatrix(m, position, verticalOffset, textSize=textSize, blankCellText="\n")