historicalrange
// This source code is subject to the terms of the Mozilla Public License 2.0 at https://mozilla.org/MPL/2.0/
// © HeWhoMustNotBeNamed

//   __    __            __       __  __                  __       __                        __      __    __              __      _______             __    __                                          __ 
//  /  |  /  |          /  |  _  /  |/  |                /  \     /  |                      /  |    /  \  /  |            /  |    /       \           /  \  /  |                                        /  |
//  $$ |  $$ |  ______  $$ | / \ $$ |$$ |____    ______  $$  \   /$$ | __    __   _______  _$$ |_   $$  \ $$ |  ______   _$$ |_   $$$$$$$  |  ______  $$  \ $$ |  ______   _____  ____    ______    ____$$ |
//  $$ |__$$ | /      \ $$ |/$  \$$ |$$      \  /      \ $$$  \ /$$$ |/  |  /  | /       |/ $$   |  $$$  \$$ | /      \ / $$   |  $$ |__$$ | /      \ $$$  \$$ | /      \ /     \/    \  /      \  /    $$ |
//  $$    $$ |/$$$$$$  |$$ /$$$  $$ |$$$$$$$  |/$$$$$$  |$$$$  /$$$$ |$$ |  $$ |/$$$$$$$/ $$$$$$/   $$$$  $$ |/$$$$$$  |$$$$$$/   $$    $$< /$$$$$$  |$$$$  $$ | $$$$$$  |$$$$$$ $$$$  |/$$$$$$  |/$$$$$$$ |
//  $$$$$$$$ |$$    $$ |$$ $$/$$ $$ |$$ |  $$ |$$ |  $$ |$$ $$ $$/$$ |$$ |  $$ |$$      \   $$ | __ $$ $$ $$ |$$ |  $$ |  $$ | __ $$$$$$$  |$$    $$ |$$ $$ $$ | /    $$ |$$ | $$ | $$ |$$    $$ |$$ |  $$ |
//  $$ |  $$ |$$$$$$$$/ $$$$/  $$$$ |$$ |  $$ |$$ \__$$ |$$ |$$$/ $$ |$$ \__$$ | $$$$$$  |  $$ |/  |$$ |$$$$ |$$ \__$$ |  $$ |/  |$$ |__$$ |$$$$$$$$/ $$ |$$$$ |/$$$$$$$ |$$ | $$ | $$ |$$$$$$$$/ $$ \__$$ |
//  $$ |  $$ |$$       |$$$/    $$$ |$$ |  $$ |$$    $$/ $$ | $/  $$ |$$    $$/ /     $$/   $$  $$/ $$ | $$$ |$$    $$/   $$  $$/ $$    $$/ $$       |$$ | $$$ |$$    $$ |$$ | $$ | $$ |$$       |$$    $$ |
//  $$/   $$/  $$$$$$$/ $$/      $$/ $$/   $$/  $$$$$$/  $$/      $$/  $$$$$$/  $$$$$$$/     $$$$/  $$/   $$/  $$$$$$/     $$$$/  $$$$$$$/   $$$$$$$/ $$/   $$/  $$$$$$$/ $$/  $$/  $$/  $$$$$$$/  $$$$$$$/ 
//                                                                                                                                                                                                          
//                                                                                                                                                                                                          
//
//@version=5

// @description Library provices a method to calculate historical percentile range of series.
library("historicalrange")
import HeWhoMustNotBeNamed/enhanced_ta/10 as eta
// @function calculates historical percentrank of the source
// @param source Source for which historical percentrank needs to be calculated. Source should be ranging between 0-100. If using a source which can beyond 0-100, use short term percentrank to baseline them.
// @returns pArray - percentrank array which contains how many instances of source occurred at different levels.
//          upperPercentile - percentile based on higher value
//          lowerPercentile - percentile based on lower value
//          median - median value of the source
//          max - max value of the source
export hpercentrank(float source, simple bool invert = false) =>
    if(source < 0 or source > 100)
        runtime.error('Source need to be between 0 and 100. Use percentrank to limit the source between 0-100')
    precisionMultiplier = 10
    var pArray = array.new_int(100*precisionMultiplier+2, 0)
    index = math.round(source*precisionMultiplier)
    array.set(pArray, index, array.get(pArray, index)+1)
    upperPortion = array.slice(pArray, index+1, array.size(pArray))
    lowerPortion = array.slice(pArray, 0, index)

    total = array.sum(pArray)
    upper = array.sum(upperPortion)
    lower = array.sum(lowerPortion)
    same = array.get(pArray, index)
    
    upperPercentile = (total-upper)*100/total
    lowerPercentile = (total-lower)*100/total
    percentrank = invert ? lowerPercentile : upperPercentile
    cumulativeCount = 0
    medianIndex = 0
    maxIndex = 0
    for i=0 to array.size(pArray)-1
        cumulativeCount += array.get(pArray, i)
        if(cumulativeCount < total/2)
            medianIndex := i+1
        if(cumulativeCount == total)
            maxIndex := i
            break
            
    median = medianIndex/precisionMultiplier
    max = maxIndex/precisionMultiplier
    [source, index, pArray, percentrank, median, max]
    

// @function returns stats on historical distance from ath in terms of percentage
// @param source for which stats are calculated
// @returns percentile and related historical stats regarding distance from ath
export distancefromath(float source = close, float highSource = high)=>
    var ath = highSource
    ath := na(ath)? highSource : math.max(ath, highSource)
    distanceFromAth = ath - source
    percentageFromAth = distanceFromAth*100/ath
    hpercentrank(percentageFromAth, true)
    

// @function returns stats on historical distance from moving average in terms of percentage
// @param maType Moving Average Type : Can be sma, ema, hma, rma, wma, vwma, swma, highlow, linreg, median
// @param length Moving Average Length
// @param source for which stats are calculated
// @returns percentile and related historical stats regarding distance from ath
export distancefromma(float source, simple string maType, simple int length)=>
    ma = eta.ma(source, maType, length)

    percentFromMa = (close-ma)/close
    var ath = percentFromMa
    var atl = percentFromMa
    ath := na(ath)? percentFromMa : math.max(ath, percentFromMa)
    atl := na(atl)? percentFromMa : math.min(atl, percentFromMa)
    
    relativeDistance = (percentFromMa-atl)*100/(ath-atl)
    hpercentrank(relativeDistance)
    

// @function returns percentrank and stats on historical bpercentb levels
// @param source Moving Average Source
// @param maType Moving Average Type : Can be sma, ema, hma, rma, wma, vwma, swma, highlow, linreg, median
// @param length Moving Average Length
// @param multiplier Standard Deviation multiplier
// @param sticky - sticky boundaries which will only change when value is outside boundary.
// @returns percentile and related historical stats regarding Bollinger Percent B
export bpercentb(float source=close, simple string maType="sma", simple int length=20, float multiplier=2.0, simple bool sticky=false) =>
    percentb = math.min(math.max(eta.bpercentb(source, maType, length, multiplier, sticky), 0), 100)
    hpercentrank(percentb)
    

// @function returns percentrank and stats on historical kpercentk levels
// @param source Moving Average Source
// @param maType Moving Average Type : Can be sma, ema, hma, rma, wma, vwma, swma, highlow, linreg, median
// @param length Moving Average Length
// @param multiplier Standard Deviation multiplier
// @param useTrueRange - if set to false, uses high-low.
// @param sticky - sticky boundaries which will only change when value is outside boundary.
// @returns percentile and related historical stats regarding Keltener Percent K
export kpercentk(float source=close, simple string maType="ema", simple int length=20, float multiplier=2, simple bool useTrueRange=true, simple bool sticky=false) =>
    percentk = math.min(math.max(eta.kpercentk(source, maType, length, multiplier, useTrueRange, sticky), 0), 100)
    hpercentrank(percentk)
    

// @function returns percentrank and stats on historical dpercentd levels
// @param useAlternateSource - Custom source is used only if useAlternateSource is set to true
// @param alternateSource - Custom source
// @param length - donchian channel length
// @param sticky - sticky boundaries which will only change when value is outside boundary.
// @returns percentile and related historical stats regarding Donchian Percent D
export dpercentd(simple int length=20, simple bool useAlternateSource = false, float alternateSource = close, simple bool sticky=false) =>
    dpercentd = math.min(math.max(eta.dpercentd(length, useAlternateSource, alternateSource, sticky), 0), 100)
    hpercentrank(dpercentd)
    

// @function oscillator - returns Choice of oscillator with custom overbought/oversold range
// @param type - oscillator type. Valid values : cci, cmo, cog, mfi, roc, rsi, stoch, tsi, wpr
// @param length - Oscillator length - not used for TSI
// @param shortLength - shortLength only used for TSI
// @param longLength - longLength only used for TSI
// @param source - custom source if required
// @param highSource - custom high source for stochastic oscillator
// @param lowSource - custom low source for stochastic oscillator
// @param method - Valid values for method are : sma, ema, hma, rma, wma, vwma, swma, highlow, linreg, median
// @param highlowLength - length on which highlow of the oscillator is calculated
// @param sticky - overbought, oversold levels won't change unless crossed
// @returns percentile and related historical stats regarding oscillator  
export oscillator(simple string type="rsi", simple int length=14, simple int shortLength = 13, simple int longLength = 25,
                     float source = close, float highSource = high, float lowSource = low,
                     simple string method="highlow", simple int highlowLength=50, simple bool sticky=false, simple bool useDynamicRange = false)=>
    [oscillator, overbought, oversold] = eta.oscillator(type, length, shortLength, longLength, source, highSource, lowSource, method, highlowLength, sticky)
    var ath = oscillator
    var atl = oscillator
    ath := na(ath)? oscillator : math.max(ath, oscillator)
    atl := na(atl)? oscillator : math.min(atl, oscillator)
    oscHigh = useDynamicRange ? overbought : ath
    oscLow = useDynamicRange ? oversold : atl
    oscPercent = math.min(math.max((oscillator-oscLow)*100/(oscHigh-oscLow), 0), 100)
    hpercentrank(oscPercent)


// ******************************************************* Code Examples **************************************************************
type = input.string("oscillator", options=["distancefromath", "distancefromma", "bpercentb", "kpercentk", "dpercentd", "oscillator"], group="Stats")
displayDetailedStats = input.bool(true, title="Detailed Stats", group="Stats")

masource = input.source(close, title="Source", group="Moving Average/Bands")
matype = input.string("sma", title="Type", group="Moving Average/Bands", options=["sma", "ema", "hma", "rma", "wma", "vwma", "swma", "linreg", "median"])
malength = input.int(20, title="Length", group="Moving Average/Bands")
multiplier = input.float(2.0, step=0.5, title="Multiplier", group="Moving Average/Bands")
useTrueRange = input.bool(true, title="Use True Range (KC)", group="Moving Average/Bands")
useAlternateSource = input.bool(false, title="Use Alternate Source (DC)", group="Moving Average/Bands")
sticky = input.bool(true, title="Sticky", group="Moving Average/Bands")

oscType = input.string("rsi", title="Oscillator Type", group="Oscillator", options=["cci", "cmo", "cog", "mfi", "roc", "rsi", "stoch", "tsi", "wpr"])
oLength = input.int(14, title="Length", group="Oscillator")
shortLength = input.int(9, title="Short Length (TSI)", group="Oscillator")
longLength = input.int(9, title="Long Length (TSI)", group="Oscillator")
method = input.string("highlow", title="Dynamic Overvought/Oversold Calculation method", group="Oscillator", options=["highlow", "sma", "ema", "hma", "rma", "wma", "vwma", "swma", "linreg", "median"])
highlowlength = input.int(50, title="Length", group="Oscillator")
useDynamicRange = input.bool(false, title="Dynamic Range", group="Oscillator")
osticky = input.bool(true, title="Sticky", group="Oscillator")

[source, index, pArray, percentile, median, max] = switch(type)
    "distancefromath" => distancefromath()
    "distancefromma" => distancefromma(masource, matype, malength)
    "bpercentb" => bpercentb(masource, matype, malength, multiplier, sticky)
    "kpercentk" => kpercentk(masource, matype, malength, multiplier, useTrueRange, sticky)
    "dpercentd" => dpercentd(malength, useAlternateSource, masource, sticky)
    "oscillator" => oscillator(oscType, oLength, shortLength, longLength, method = method, highlowLength=highlowlength,
                         sticky=osticky, useDynamicRange=useDynamicRange)

textSize = input.string(size.small, title='Text Size', options=[size.tiny, size.small, size.normal, size.large, size.huge], group='Table', inline='text')
headerColor = input.color(color.maroon, title='Header', group='Table', inline='GC')
neutralColor = input.color(#FFEEBB, title='Cell', group='Table', inline='GC')
presentColor = input.color(color.aqua, title='Present', group='Table', inline='GC')
medianColor = input.color(color.olive, title='Median', group='Table', inline='GC')
total = array.sum(pArray)
color cellColor = color.blue
var dateStart = str.tostring(year) + '/' + str.tostring(month) + '/' + str.tostring(dayofmonth)
dateEnd = str.tostring(year) + '/' + str.tostring(month) + '/' + str.tostring(dayofmonth)
if(displayDetailedStats and total!=0 and barstate.islastconfirmedhistory)
    legend = table.new(position=position.middle_right, columns=2, rows=5, border_width=1)
    table.cell(table_id=legend, column=0, row=0, text='Legend', bgcolor=color.teal, text_color=color.white, text_size=textSize)
    table.cell(table_id=legend, column=0, row=1, text='Value Range', bgcolor=color.black, text_color=color.white, text_size=textSize)
    table.cell(table_id=legend, column=1, row=1, text='        ', bgcolor=headerColor, text_color=color.white, text_size=textSize)
    table.cell(table_id=legend, column=0, row=2, text='Percentile Range', bgcolor=color.black, text_color=color.white, text_size=textSize)
    table.cell(table_id=legend, column=1, row=2, text='        ', bgcolor=neutralColor, text_color=color.white, text_size=textSize)
    table.cell(table_id=legend, column=0, row=3, text='Present Range', bgcolor=color.black, text_color=color.white, text_size=textSize)
    table.cell(table_id=legend, column=1, row=3, text='        ', bgcolor=presentColor, text_color=color.white, text_size=textSize)
    table.cell(table_id=legend, column=0, row=4, text='Median Range', bgcolor=color.black, text_color=color.white, text_size=textSize)
    table.cell(table_id=legend, column=1, row=4, text='        ', bgcolor=medianColor, text_color=color.white, text_size=textSize)

    detailedTable = table.new(position=position.middle_center, columns=21, rows=10, border_width=1)
    table.cell(table_id=detailedTable, column=0, row=0, text=dateStart + ' to ' + dateEnd, bgcolor=color.teal, text_color=color.white, text_size=textSize)
    table.cell(table_id=detailedTable, column=0, row=1, text=syminfo.tickerid, bgcolor=color.teal, text_color=color.white, text_size=textSize)
        
    headerArray = array.new_string()
    valueArray = array.new_string()
    cellColorArray = array.new_color()
    precisionMultiplier = 10
    for i=0 to 99
        end = math.min((i+1)*precisionMultiplier, array.size(pArray))
        slice = array.slice(pArray, 0, end)
        count = array.sum(slice)
        row = math.floor(i/10)
        col = (i%10) * 2 + 1
        if (count != 0)
            percent = count*100/total
            headerText = '0 - ' + str.tostring(end/precisionMultiplier)
            cColor = math.floor(index/precisionMultiplier) == i ? presentColor : math.floor(median) == i ? medianColor : neutralColor
            
            array.push(headerArray, headerText)
            array.push(valueArray, str.tostring(percent, format.percent))
            array.push(cellColorArray, cColor)
        if(count == total)
            break

    rowCol = math.ceil(math.sqrt(array.size(headerArray)))
    
    counter = 0
    for i=0 to rowCol-1
        for j=0 to rowCol-1
            row = i
            col = j*2 + 1
            if(counter >= array.size(headerArray))
                break
            table.cell(table_id=detailedTable, column=col, row=row, text=array.get(headerArray, counter), bgcolor=headerColor, text_color=color.white, text_size=textSize)
            table.cell(table_id=detailedTable, column=col + 1, row=row, text=array.get(valueArray, counter), bgcolor=array.get(cellColorArray, counter), text_color=color.black, text_size=textSize)
            counter += 1


plot(displayDetailedStats? na : percentile, title="Historical Percentile", color=cellColor)
