Multi-Timeframe RSI Grid
// 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
// MTF RSI Grid

// The relative strength index (RSI) is a momentum indicator that measures the magnitude of recent price changes to evaluate 
// overbought or oversold conditions. The RSI is normally displayed as an oscillator separately from price and can have a 
// reading from 0 to 100. This indicator displays the current RSI levels at up to 6 timeframes (of your choosing) in a grid. 
// If the RSI levels reach overbought (above 70) or oversold (below 30) conditions, it changes the color to help you see that 
// RSI has reached extreme levels. Note that in tradingview, when the chart is on a higher timeframe, the lower timeframe RSI 
// levels don't calculate properly, so this indicator will hide those values in the grid. If none of your selected values are 
// available, it hides the table completely. 

indicator(title='MM RSI Grid', overlay=true)

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

// Constants

SPACE = "   "

// Configuration

grp = "Appearance"
showTitle = input.bool(group=grp, defval=true, title='Show "RSI" Heading')
friendlyTextSize = input.string(group=grp, defval="Tiny", title="Size / Position  ", options=["Tiny", "Small", "Normal", "Large", "Huge"], inline="position and size")
textSize = Utils.getSizeFromString(friendlyTextSize)
friendlyPosition = input.string(group=grp, 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=grp, 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).")
colorHelp = "Background and text color pairs for moderate and extreme RSI levels, respectively."
colorModerate = input.color(group=grp, defval=color.rgb(40, 98, 255), title="Colors", inline="colors")
colorModerateText = input.color(group=grp, defval=color.white, title="", inline="colors")
colorExtreme = input.color(group=grp, defval=color.rgb(235, 46, 175), title="", inline="colors")
colorExtremeText = input.color(group=grp, defval=color.white, title="", inline="colors", tooltip=colorHelp)

grp2   = "Timeframes"
show1  = input.bool(defval=true, title="", inline="tf1", group=grp2)
show2  = input.bool(defval=true, title="", inline="tf2", group=grp2)
show3  = input.bool(defval=true, title="", inline="tf3", group=grp2)
show4  = input.bool(defval=true, title="", inline="tf4", group=grp2)
show5  = input.bool(defval=true, title="", inline="tf5", group=grp2)
show6  = input.bool(defval=true, title="", inline="tf6", group=grp2)
friendlyTimeframe1 = input.timeframe(defval="5m", title="", inline="tf1", group=grp2, options=["1m", "2m", "5m", "15m", "30m", "45m", "1h", "2h", "3h", "4h", "12h", "1D", "1W", "1M"])
friendlyTimeframe2 = input.timeframe(defval="15m", title="", inline="tf2", group=grp2, options=["1m", "2m", "5m", "15m", "30m", "45m", "1h", "2h", "3h", "4h", "12h", "1D", "1W", "1M"])
friendlyTimeframe3 = input.timeframe(defval="1h", title="", inline="tf3", group=grp2, options=["1m", "2m", "5m", "15m", "30m", "45m", "1h", "2h", "3h", "4h", "12h", "1D", "1W", "1M"])
friendlyTimeframe4 = input.timeframe(defval="4h", title="", inline="tf4", group=grp2, options=["1m", "2m", "5m", "15m", "30m", "45m", "1h", "2h", "3h", "4h", "12h", "1D", "1W", "1M"])
friendlyTimeframe5 = input.timeframe(defval="1D", title="", inline="tf5", group=grp2, options=["1m", "2m", "5m", "15m", "30m", "45m", "1h", "2h", "3h", "4h", "12h", "1D", "1W", "1M"])
friendlyTimeframe6 = input.timeframe(defval="1W", title="", inline="tf6", group=grp2, options=["1m", "2m", "5m", "15m", "30m", "45m", "1h", "2h", "3h", "4h", "12h", "1D", "1W", "1M"])

grp3   = "Superstack Alerts"
ssToolTip = "Superstack alerts are triggered when 3 or more consecutive timeframes (starting from the first one) reach oversold or overbought together. "
// showSuperstackBreadcrumbs = input.bool(defval=false, title="Show arrow icon breadcrumbs on chart when superstack conditions are met.", tooltip=ssToolTip + "Breadcrumbs are a bit messy, cluttering up the chart, but they are useful for seeing when things began and ended on the chart historically.", group=grp3)
showSuperstackLabels = input.bool(defval=true, title="Show label on chart when superstack conditions are met.", tooltip=ssToolTip + " The labels only appear when a superstack is active and disappear when it is finished.", group=grp3)

length = input.int(group="Advanced", defval=14, title="RSI Length")

// Functions

getTimeframePeriodFromFriendlyString(friendlyString) =>
    switch friendlyString
        "1m" => "1"
        "2m" => "2"
        "5m" => "5"
        "15m" => "15"
        "30m" => "30"
        "45m" => "45"
        "1h" => "60"
        "2h" => "120"
        "3h" => "180"
        "4h" => "240"
        "12h" => "1440"
        "1D" => "D"
        "1W" => "W"
        "1M" => "M"
        => "5"

requestSecurityProxy(_tickerid, _timeframe, _expression) => 
    request.security(_tickerid, _timeframe, _expression, lookahead=barmerge.lookahead_on)

requestSecurityLower(tickerid, timeframe, expression) =>
    _timeframe = timeframe.in_seconds('') < timeframe.in_seconds(timeframe) ? '' : timeframe
    array<float> lowerTimeframeArray = request.security_lower_tf(syminfo.tickerid, _timeframe, expression)
    array.size(lowerTimeframeArray) > 0 ? array.get(lowerTimeframeArray, array.size(lowerTimeframeArray) - 1) : na

requestSecurity(tickerid, timeframe, expression) =>
    timeframe.in_seconds('') <= timeframe.in_seconds(timeframe) ? requestSecurityProxy(tickerid, timeframe, expression) : requestSecurityLower(tickerid, timeframe, expression)

addTimeframe(timeframe, rsi, show, superstackTFArray, superstackOBArray, superstackOSArray, dataArray) =>
    if (show)
        overbought = rsi >= 70
        oversold = rsi <= 30
        bgColor = oversold or overbought ? colorExtreme : colorModerate
        textColor = oversold or overbought ? colorExtremeText : colorModerateText
        array.push(superstackTFArray, timeframe)
        array.push(superstackOBArray, overbought)
        array.push(superstackOSArray, oversold)
        array.push(dataArray, Table.Cell.new(
           str.format(" {0} \n {1,number,#} ", timeframe, rsi),
           bgColor,
           textColor
           ))

drawLabel(ssLevel) =>
    if (barstate.islast and showSuperstackLabels)
        ssBreak = ssLevel == 0
        yloc = ssBreak ? close : ssLevel > 0 ? high : low
        txt = str.format("Superstack {0}: {1}", ssLevel > 0 ? "Overbought" : "Oversold", math.abs(ssLevel))
        clr = ssLevel > 0 ? color.red : color.green
        style = ssLevel > 0 ? label.style_label_lower_left : label.style_label_upper_left,
        l = label.new(x=bar_index[0]+1, y=yloc, text=txt, style=style, color=clr, textcolor=color.white, size=size.small)
        // Delete older labels (only have the latest one on the last candle)
        label.delete(l[1])

        // Delete the current one if there's no stairstep and no break
        if (ssLevel == 0)
            label.delete(l[0])

getConsecutiveTrueCount(boolArray) =>
    count = int(0)
    i = int(0)
    isTrue = true
    boolSize = array.size(boolArray)
    while i < boolSize and isTrue == true
        isTrue := array.get(boolArray, i) == true
        count += isTrue ? 1 : 0
        i += 1
    count

// Indicator Logic

// Convert the configured "friendly" timeframe strings back to the ones expected by TradingView
timeframe1 = getTimeframePeriodFromFriendlyString(friendlyTimeframe1)
timeframe2 = getTimeframePeriodFromFriendlyString(friendlyTimeframe2)
timeframe3 = getTimeframePeriodFromFriendlyString(friendlyTimeframe3)
timeframe4 = getTimeframePeriodFromFriendlyString(friendlyTimeframe4)
timeframe5 = getTimeframePeriodFromFriendlyString(friendlyTimeframe5)
timeframe6 = getTimeframePeriodFromFriendlyString(friendlyTimeframe6)

// Arrays to be built up by the indicator
dataArray = array.new<Table.Cell>()
superstackTFArray = array.new<string>()
superstackOBArray = array.new<bool>()
superstackOSArray = array.new<bool>()

// Fetch the RSI levels from the chosen timeframes
rsi = ta.rsi(close, length)
rsi1 = requestSecurity(syminfo.tickerid, timeframe1, rsi)
rsi2 = requestSecurity(syminfo.tickerid, timeframe2, rsi)
rsi3 = requestSecurity(syminfo.tickerid, timeframe3, rsi)
rsi4 = requestSecurity(syminfo.tickerid, timeframe4, rsi)
rsi5 = requestSecurity(syminfo.tickerid, timeframe5, rsi)
rsi6 = requestSecurity(syminfo.tickerid, timeframe6, rsi)

if barstate.islast
    // Build up the array to be rendered
    if (showTitle)
        array.push(dataArray, Table.Cell.new(" RSI ", colorExtreme, colorExtremeText))

    addTimeframe(friendlyTimeframe1, rsi1, show1, superstackTFArray, superstackOBArray, superstackOSArray, dataArray)
    addTimeframe(friendlyTimeframe2, rsi2, show2, superstackTFArray, superstackOBArray, superstackOSArray, dataArray)
    addTimeframe(friendlyTimeframe3, rsi3, show3, superstackTFArray, superstackOBArray, superstackOSArray, dataArray)
    addTimeframe(friendlyTimeframe4, rsi4, show4, superstackTFArray, superstackOBArray, superstackOSArray, dataArray)
    addTimeframe(friendlyTimeframe5, rsi5, show5, superstackTFArray, superstackOBArray, superstackOSArray, dataArray)
    addTimeframe(friendlyTimeframe6, rsi6, show6, superstackTFArray, superstackOBArray, superstackOSArray, dataArray)

    int dataSize = array.size(dataArray)
    if (showTitle)
        dataSize -= 1

    if dataSize > 0
	    Table.fromArray(dataArray, position, verticalOffset, false, textSize, 2, "\n")

// Alert Conditions
obLevel = getConsecutiveTrueCount(superstackOBArray)
osLevel = getConsecutiveTrueCount(superstackOSArray)
ssLevel = obLevel > 2 ? obLevel : osLevel > 2 ? -osLevel : 0
superstackOb3 = obLevel >= 3
superstackOb4 = obLevel >= 4
superstackOb5 = obLevel >= 5
superstackOb6 = obLevel >= 6
superstackOs3 = osLevel >= 3
superstackOs4 = osLevel >= 4
superstackOs5 = osLevel >= 5
superstackOs6 = osLevel >= 6

// Alerts
alertcondition(superstackOb3, title="Superstack: 3 Overbought", message="{{ticker}} Superstack: 3 timeframes overbought")
alertcondition(superstackOb4, title="Superstack: 4 Overbought", message="{{ticker}} Superstack: 4 timeframes overbought")
alertcondition(superstackOb5, title="Superstack: 5 Overbought", message="{{ticker}} Superstack: 5 timeframes overbought")
alertcondition(superstackOb6, title="Superstack: 6 Overbought", message="{{ticker}} Superstack: 6 timeframes overbought")
alertcondition(superstackOs3, title="Superstack: 3 Oversold",   message="{{ticker}} Superstack: 3 timeframes oversold")
alertcondition(superstackOs4, title="Superstack: 4 Oversold",   message="{{ticker}} Superstack: 4 timeframes oversold")
alertcondition(superstackOs5, title="Superstack: 5 Oversold",   message="{{ticker}} Superstack: 5 timeframes oversold")
alertcondition(superstackOs6, title="Superstack: 6 Oversold",   message="{{ticker}} Superstack: 6 timeframes oversold")
alertcondition(superstackOb3 or superstackOs3, title="Superstack: 3 Overbought/Oversold", message="{{ticker}} Superstack: 3 timeframes overbought or oversold")
alertcondition(superstackOb4 or superstackOs4, title="Superstack: 4 Overbought/Oversold", message="{{ticker}} Superstack: 4 timeframes overbought or oversold")
alertcondition(superstackOb5 or superstackOs5, title="Superstack: 5 Overbought/Oversold", message="{{ticker}} Superstack: 5 timeframes overbought or oversold")
alertcondition(superstackOb6 or superstackOs6, title="Superstack: 6 Overbought/Oversold", message="{{ticker}} Superstack: 6 timeframes overbought or oversold")

drawLabel(ssLevel)