Utils
// 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

// @description A collection of convenience and helper functions for indicator and library authors on TradingView
library("Utils", overlay=true)

// @function My version of format number that doesn't have so many decimal places...
// @param num The number to be formatted
// @returns The formatted number
export formatNumber(float num) =>
    var n = num
    var ret = str.format("{0,number,#}", n)
    if (num > 999)
        n := num / 1000
        ret := str.format("{0,number,#.#}", n) + "k"
    if (num > 999999)
        n := num / 1000000
        ret := str.format("{0,number,#.#}", n) + "M"
    ret

// @function Convenience function returns timestamp in yyyy/MM/dd format.
// @param timestamp The timestamp to stringify
// @returns The date string
export getDateString(int timestamp) =>
    str.format('{0,date,yyyy/MM/dd}', timestamp)

// @function Convenience function returns timestamp in yyyy/MM/dd hh:mm format.
// @param timestamp The timestamp to stringify
// @returns The date string
export getDateTimeString(int timestamp) =>
    str.format('{0,date,yyyy/MM/dd hh:mm}', timestamp)

// @function Gets the number of inside bars for the current chart. Can also be passed to request.security to get the same for different timeframes.
// @returns The # of inside bars on the chart right now.
export getInsideBarCount() => 
    ibCount = 0
    i = 0
    maxLoops = 10
    while i <= maxLoops
        if (high[i+1] > high[i] and low[i+1] < low[i])
            ibCount := ibCount + 1
            i := i + 1
        else
            i := maxLoops + 1

    ibCount

// @function Tradingview doesn't give you a nice way to put the label styles into a dropdown for configuration settings. So, I specify them in the following format: ["Center", "Left", "Lower Left", "Lower Right", "Right", "Up", "Upper Left", "Upper Right", "Plain Text", "No Labels"]. This function takes care of converting those custom strings back to the ones expected by tradingview scripts. 
// @param show styleString The plain english version of the string to convert back to the expected TradingView format.
// @param acceptGivenIfNoMatch If no match for styleString is found and this is true, the function will return styleString, otherwise it will return tradingview's preferred default
// @returns The string expected by tradingview functions
export getLabelStyleFromString(string styleString, bool acceptGivenIfNoMatch = true) =>
    ret = switch styleString
        "Down" => label.style_label_down
        "Center" => label.style_label_center
        "Left" => label.style_label_left
        "Label" => label.style_label_left
        "Lower Left" => label.style_label_lower_left
        "Lower Right" => label.style_label_lower_right
        "Right" => label.style_label_right
        "Up" => label.style_label_up
        "Upper Left" => label.style_label_upper_left
        "Upper Right" => label.style_label_upper_right
        "Plain Text" => label.style_none
        => acceptGivenIfNoMatch ? styleString : label.style_label_down
    ret

// @function Given an hour number and minute number, adds them together and returns the sum. To be used by getLevelBetweenTimes when fetching specific price levels during a time window on the day.
// @param hourNumber The hour number
// @param minuteNumber The minute number
// @returns The sum of all the minutes
export getTime(int hourNumber, int minuteNumber) =>
    (hourNumber * 60) + minuteNumber

// @function Given a start and end time, returns the high or low price during that time window.
// @param start The timestamp to start with (# of seconds)
// @param end The timestamp to end with (# of seconds)
// @returns The high or low value
export getHighAndLowBetweenTimes(int start, int end) =>
    // ref: https://www.pinecoders.com/faq_and_code/
    var float h = 0.
    var float l = 10e10
    var reset = true
    minNow = getTime(hour, minute)
    if minNow >= start and minNow <= end
        if reset
            h := high
			l := low
            reset := false
        else
            h := math.max(h, high) 
			l := math.min(l, low)
    else
        reset := true
    [h, l]

// @function Returns an expression that can be used by request.security to fetch the premarket high & low levels in a tuple.
// @returns (tuple) [premarketHigh, premarketLow]
export getPremarketHighsAndLows() =>
    premarketStart = getTime(4, 0)
    premarketEnd = getTime(9, 29)
	getHighAndLowBetweenTimes(premarketStart, premarketEnd)

// @function Returns an expression that can be used by request.security to fetch the after hours high & low levels in a tuple.
// @returns (tuple) [afterHoursHigh, afterHoursLow]
export getAfterHoursHighsAndLows() =>
    afterHoursStart = getTime(16, 0)
    afterHoursEnd = getTime(19, 59)
	getHighAndLowBetweenTimes(afterHoursStart, afterHoursEnd)

// @function Returns an expression that can be used by request.security to fetch the overnight high & low levels in a tuple.
// @returns (tuple) [overnightHigh, overnightLow]
export getOvernightHighsAndLows() =>
    overnightStart = getTime(16, 0)
    overnightEnd = getTime(9, 29)
	getHighAndLowBetweenTimes(overnightStart, overnightEnd)

// @function Returns an expression that can be used by request.security to fetch the high & low levels for premarket, after hours and overnight in a tuple.
// @returns (tuple) [premarketHigh, premarketLow, afterHoursHigh, afterHoursLow, overnightHigh, overnightLow]
export getNonRthHighsAndLows() =>
	[premarketHigh, premarketLow] = getPremarketHighsAndLows()
	[afterHoursHigh, afterHoursLow] = getAfterHoursHighsAndLows()
	overnightHigh = math.max(afterHoursHigh, premarketHigh)
	overnightLow = math.min(afterHoursLow, premarketLow)
    [premarketHigh, premarketLow, afterHoursHigh, afterHoursLow, overnightHigh, overnightLow]

// @function Tradingview doesn't give you a nice way to put the line styles into a dropdown for configuration settings. So, I specify them in the following format: ["Solid", "Dashed", "Dotted", "None/Hidden"]. This function takes care of converting those custom strings back to the ones expected by tradingview scripts. 
// @param styleString Plain english (or TV Standard) version of the style string
// @param acceptGivenIfNoMatch If no match for styleString is found and this is true, the function will return styleString, otherwise it will return tradingview's preferred default
// @returns The string expected by tradingview functions
export getLineStyleFromString(string styleString, bool acceptGivenIfNoMatch = true) =>
    ret = switch styleString
        "Dotted" => line.style_dotted
        "Dashed" => line.style_dashed
        "Solid" => line.style_solid
        => acceptGivenIfNoMatch ? styleString : line.style_solid
    ret

// @function Get the % the current price is away from the given price.
// @returns The % the current price is away from the given price.
export getPercentFromPrice(float price) =>
    diff = price - close
    (price < close ? diff / close : diff / price)

// @function Tradingview doesn't give you a nice way to put the positions into a dropdown for configuration settings. So, I specify them in the following format: ["Top Left", "Top Center", "Top Right", "Middle Left", "Middle Center", "Middle Right", "Bottom Left", "Bottom Center", "Bottom Right"]. This function takes care of converting those custom strings back to the ones expected by tradingview scripts. 
// @param position Plain english position string
// @returns The string expected by tradingview functions
export getPositionFromString(string position) =>
    switch position
        "Top Left" => position.top_left
        "Top Center" => position.top_center
        "Top Right" => position.top_right
        "Middle Left" => position.middle_left
        "Middle Center" => position.middle_center
        "Middle Right" => position.middle_right
        "Bottom Left" => position.bottom_left
        "Bottom Center" => position.bottom_center
        "Bottom Right" => position.bottom_right
        => position

// @function Call request.security with this as the expression to get the average up/down values that can be used with getRsiPrice (below) to calculate the price level where the supplied RSI level would be reached.
// @param rsiLength The length of the RSI requested.
// @returns A tuple containing the avgUp and avgDown values required by the getRsiPrice function.
export getRsiAvgsExpression(int rsiLength) =>
    ep = 2 * rsiLength - 1
    avgUp = ta.ema(math.max(close - close[1], 0), ep)
    avgDown = ta.ema(math.max(close[1] - close, 0), ep)
    rsi = ta.rsi(close, rsiLength)
    [rsi, avgUp, avgDown]

// @function use the values returned by getRsiAvgsExpression() to calculate the price level when the provided RSI level would be reached.
// @param rsiLevel The RSI level to find price at.
// @param rsiLength The length of the RSI to calculate.
// @param avgUp The average move up of RSI.
// @param avgDown The average move down of RSI.
// @returns The price level where the provided RSI level would be met. 
export getRsiPrice(float rsiLevel, int rsiLength, float avgUp, float avgDown) => 
    x = (rsiLength - 1) * (avgDown * rsiLevel / (100 - rsiLevel) - avgUp)
    x >= 0 ? close + x : close + x * (100 - rsiLevel) / rsiLevel

// @function Tradingview doesn't give you a nice way to put the sizes into a dropdown for configuration settings. So, I specify them in the following format: ["Auto", "Huge", "Large", "Normal", "Small", "Tiny"]. This function takes care of converting those custom strings back to the ones expected by tradingview scripts. 
// @param sizeString Plain english size string
// @returns The string expected by tradingview functions
export getSizeFromString(string sizeString) =>
    switch sizeString
        "Auto" => size.auto
        "Huge" => size.huge
        "Large" => size.large
        "Normal" => size.normal
        "Tiny" => size.tiny
        => size.small // Defaulting to small if size is unrecognized

// @function Get the timeframe of the current chart for display
// @returns The string of the current chart timeframe
export getTimeframeOfChart() =>
    string rtf = str.tostring(timeframe.multiplier)
    if (timeframe.isseconds)
        rtf := rtf + "s"
    if (timeframe.isminutes)
        rtf := rtf + "m"
    if (timeframe.isdaily)
        rtf := rtf + "D"
    if (timeframe.isweekly)
        rtf := rtf + "W"
    if (timeframe.ismonthly)
        rtf := rtf + "M"

// @function Helper function for drawings that use xloc.bar_time to help you know the time offset if you want to place the end of the drawing out into the future. This determines the time-size of one candle and then returns a time n candleOffsets into the future. 
// @param candleOffset The number of items to find singular/plural for.
// @returns The future time
export getTimeNowPlusOffset(int candleOffset) =>
	oneCandleInMS = timeframe.in_seconds() * 1000
	time + (oneCandleInMS * candleOffset)

// @function Given a start and end time, returns the sum of all volume across bars during that time window.
// @param start The timestamp to start with (# of seconds)
// @param end The timestamp to end with (# of seconds)
// @returns The volume
export getVolumeBetweenTimes(int start, int end) =>
    var float vol = 0
    var reset = true
    minNow = (hour * 60) + minute
    if minNow >= start and minNow <= end
        if reset
            vol := volume
            reset := false
        else
            vol += volume
    else
        reset := true
    vol

// @function Returns true if the current bar occurs on today's date.
// @returns True if current bar is today
export isToday() =>
    year(timenow) == year(time) and month(timenow) == month(time) and dayofmonth(timenow) == dayofmonth(time)

// @function Pads a label string so that it appears properly in or not in a label. When label.style_none is used, this will make sure it is left-aligned instead of center-aligned. When any other type is used, it adds a single space to the right so there is padding against the right end of the label.
// @param labelText The string to be padded
// @param labelStyle The style of the label being padded for.
// @returns The padded string
export padLabelString(string labelText, string labelStyle) =>
    SPACE = " "
    returnString = labelText
    _labelStyle = getLabelStyleFromString(labelStyle)
    if _labelStyle == label.style_none
        for i = 0 to str.length(labelText)
            returnString := SPACE + returnString
    else
        returnString := returnString + SPACE

    returnString

// @function Helps format a string for plural/singular. By default, if you only provide num, it will just return "s" for plural and nothing for singular (eg. plural(numberOfCats)). But you can optionally specify the full singular/plural words for more complicated nomenclature (eg. plural(numberOfBenches, 'bench', 'benches'))
// @param num The number of items to find singular/plural for.
// @param singular The string to return if num is singular. Defaults to an empty string. 
// @param plural The string to return if num is plural. Defaults to 's' so you can just add 's' to the end of a word.
// @returns The singular or plural provided strings depending on the num provided.
export plural(int num, string singular = '', string plural = 's') =>
    num != 1 ? plural : singular

// @function Get the # of seconds in a given timeframe. Tradingview's timeframe.in_seconds() expects a simple string, and we often need to use series string, so this is an alternative to get you the value you need.
// @param tf The timeframe to convert
// @returns The number of secondsof that timeframe
export timeframeInSeconds(string timeframe) =>
    sMinute = 60
    sHour = sMinute * 60
    sDay = sHour * 24
    sWeek = sDay * 7
    sMonth = timeframe.in_seconds("M") // TV has a weird value for this, so I'm just using theirs
    letter = str.substring(timeframe, str.length(timeframe) - 1)
    multiplier = switch letter
        "M" => sMonth
        "W" => sWeek
        "D" => sDay
        "H" => sHour
        => sMinute

    number = int(str.tonumber(timeframe))
    increment = na(number) ? 1 : number
    increment * multiplier

// @function Convert a timeframe string to a consistent standard.
// @param tf The timeframe string to convert
// @returns The standard format for the string, or the unchanged value if it is unknown.
export timeframeToString(string tf) =>
	suffix = switch
    	timeframe.isseconds => 's'
    	timeframe.isminutes and timeframe.multiplier <= 60 => 'm'
    	timeframe.isseconds and timeframe.multiplier > 60 => 'h'
    	timeframe.isdaily => 'D'
    	timeframe.isweekly => 'W'
    	timeframe.ismonthly => 'M'
		=> ''
	multiplier = timeframe.multiplier
	if suffix == 'h' and multiplier > 60
		multiplier := math.floor(multiplier/60)
		// If there are additional minutes unaccounted for, concatenate them in the suffix
		if multiplier * 60 != timeframe.multiplier
			minutes = timeframe.multiplier - (multiplier * 60)
			suffix := str.format('h{0}m', minutes)

	suffix == '' ? tf : str.format('{0}{1}', multiplier, suffix)

// Sample usage of the user-friendly input conversions

friendlyLineStyle = input.string('Solid', 'Line Style', ["Solid", "Dashed", "Dotted", "None/Hidden"])
lineStyle = getLineStyleFromString(friendlyLineStyle)

friendlyLabelStyle = input.string('Left', 'Label Position', ["Center", "Left", "Lower Left", "Lower Right", "Right", "Up", "Upper Left", "Upper Right", "Plain Text", "No Labels"])
labelStyle = getLabelStyleFromString(friendlyLabelStyle)

// Note: there are no tables in this library, but this example provided nonetheless...
friendlyPosition = input.string('Bottom Center', 'Table Position', ["Top Left", "Top Center", "Top Right", "Middle Left", "Middle Center", "Middle Right", "Bottom Left", "Bottom Center", "Bottom Right"])
position = getPositionFromString(friendlyPosition)

// Example of the various string & timeframe functions

// Variables 

[premarketHigh, premarketLow] = request.security(syminfo.tickerid, timeframe.period, getPremarketHighsAndLows())
percentFromPremarketHigh = getPercentFromPrice(premarketHigh)*100
friendlyVolume = formatNumber(volume)
date = getDateString(time)
dateTime = getDateTimeString(time)
insideBarCount = getInsideBarCount()
fiveCandlesIntoTheFuture = getTimeNowPlusOffset(5)
numberOfDogs = 1
labelTemplate = 'Premarket High: {0,number,currency} ({1})'
  + '\nVolume: {2}'
  + '\nDate String: {3}'
  + '\nDateTime String: {4}'
  + '\nInside Bar Count: {5}'
  + '\nDogs: {6} dog{7}'
labelString = str.format(labelTemplate, premarketHigh, str.tostring(percentFromPremarketHigh, format.percent), friendlyVolume, date, dateTime, insideBarCount, numberOfDogs, plural(numberOfDogs))
var line exampleLine = na
var label exampleLabel = na

// Drawings

if lineStyle != "None/Hidden"
	if na(exampleLine)
		exampleLine := line.new(time[1], premarketHigh, time[0], premarketHigh, xloc.bar_time, extend.both, color.blue, lineStyle)
	else
		line.set_x2(exampleLine, fiveCandlesIntoTheFuture)
		line.set_y1(exampleLine, premarketHigh)
		line.set_y2(exampleLine, premarketHigh)

if labelStyle != "No Labels"
	bgColor = labelStyle == label.style_none ? #ffffff00 : color.blue
	textColor = labelStyle == label.style_none ? color.blue : color.white
	if na(exampleLabel)
		exampleLabel := label.new(time, premarketHigh, '', xloc.bar_time, yloc.price, bgColor, labelStyle, textColor, size.small)
	else
		label.set_xy(exampleLabel, fiveCandlesIntoTheFuture, premarketHigh)
		label.set_text(exampleLabel, padLabelString(labelString, labelStyle))


// Example of how to get the price level associated with an RSI level
rsiLength = 14
[hourRsi, avgUp, avgDown] = request.security(syminfo.tickerid, '60', getRsiAvgsExpression(rsiLength), lookahead=barmerge.lookahead_on)
hourOverbought = getRsiPrice(70, rsiLength, avgUp, avgDown)
hourOversold = getRsiPrice(30, rsiLength, avgUp, avgDown)
plot(hourOverbought, 'Hourly RSI Overbought', color.red)
plot(hourOversold, 'Hourly RSI Oversold', color.green)
