Intrabar Run Count Indicator [tbiktag]
// This source code is subject to the terms of the Mozilla Public License 2.0 at https://mozilla.org/MPL/2.0/
// © tbiktag

//@version=5
indicator("Intrabar Run Count Indicator [tbiktag]", "IRCI", overlay = false)

// --- External libs ---
import PineCoders/lower_tf/4 as PCltf
//

// --- Constant variables ---
//     Titles and tooltips
string T0    = "N intrabars"
string T1    = "Confidence level"
string T2    = "Bar coloring" 
string T3    = "Background coloring" 
string T4    = "Show infobox" 
string T5    = "Signal / bar color     "
string T6    = "Test statistic color   " 
string T7    = "Lines / levels color   " 
string TT0   = "Select approximately how many intrabars will be analyzed for each chart bar. 
 To define the optimal lower timeframe in relation to the selected number of intrabars,
 the script uses the lower_ltf library published by TradingView."
string TT1   = "Select the desired confidence level that defines the critical value. 
 If the test statistic exceeds the critical value, it suggests non-randomness."
string TT2   = "The signal is triggered if \n 
 •the test statistic exceeds the critical value, and \n
 •there is sufficient number of data points in the sample (>20)." 
string TT3   = "If selected, bar coloring uses the following color scheme: \n
 •Signal color: the test statistic is greater than the critical value. \n
 •Transparent: not enough data point in the sample. \n
 •Default chart color: the test statistic is below the critical value."
string TT4   = "The infobox displays the number of intrabars used for analysis."
string GR0   = "Input"
string GR1   = "Style"
//
//     LTF distinction 
string LTF1  = "~50 intrabars per chart bar"
string LTF2  = "~100 intrabars per chart bar"
string LTF3  = "~250 intrabars per chart bar"
//
//     Minimum sample size for runs test
int    THRS  = 20
//
//     Color scheme
color  RED   = #f1363f
color  YELL  = #f1e836
color  GREY  = #CCCCCC
//
//     Confidence levels
float  ZC80  = 1.280 //alpha = 0.20
float  ZC90  = 1.645 //alpha = 0.10
float  ZC95  = 1.960 //alpha = 0.05
//

// --- Inputs ---
string ltfRes  = input.string(LTF2,  T0, [LTF1, LTF2, LTF3],    TT0, '', GR0)
string zMode   = input.string('90%', T1, ["80%", "90%", "95%"], TT1, '', GR0) 
bool   isColB  = input.bool(true,    T2,                        TT3, '', GR1)
bool   isBG    = input.bool(true,    T3,                        '',  '', GR1)
bool   isIBox  = input.bool(false,   T4,                        TT4, '', GR1)
color  CS      = input.color(YELL,   T5,                        '',  '', GR1)
color  C0      = input.color(RED,    T6,                        '',  '', GR1)
color  CL      = input.color(GREY,   T7,                        '',  '', GR1)
//

// --- Functions ---
runsTest(data) =>
    //
    // Calculate the test statistic of the Wald-Wolfowitz runs test,
    // by analyzing the signs of the input array elements.
    //
    // Input:  data,  float array  :: input data set
    // Output: zStat, float        :: test statistic
    //
    int   n        = array.size(data)
    int   nP       = 0
    int   nM       = 0
    int   R        = 1
    float lastR    = n>0?array.get(data, 0):na
    for  i = 0 to n>THRS?n-1:na
        iR = array.get(data, i)
        if iR > 0 
            nP    += 1
        else
            nM    += 1
        if   i > 0 and math.sign(iR) != math.sign(lastR)
            lastR := iR
            R     += 1 
    float mean     = 2*nP*nM/n + 1
    float sigma    = math.sqrt((mean-1)*(mean-2)/(n-1))
    float zStat    = (R - mean)/sigma
    zStat
//

// --- Calculations ---
var string   lSt        = PCltf.ltf(ltfRes, '', '', '', '', '', '', '', LTF1, LTF2, LTF3)
array<float> ltfReturns = request.security_lower_tf(syminfo.tickerid, lSt, ta.change(close))
float        zStat      = runsTest(ltfReturns)
int          nDataPts   = array.size(ltfReturns)
[nIBs, nCBs, avgIBs]    = PCltf.ltfStats(ltfReturns)
//
float  zCrit   = switch zMode
    "80%"       => ZC80
    "90%"       => ZC90
    "95%"       => ZC95
bool    isH1   = math.abs(zStat) >= zCrit
//

// --- Visuals ---
//     Define plot colors
color colZ   = isH1?color.new(C0, 30):color.new(C0, 60)
color colB   = switch
    isColB and isH1      => CS
    isColB and na(zStat) => color.new(CL, 100) 
    not (isColB or isH1) => na
//
//     Plot
plotZ  = plot(zStat,  'Test Statistic',             color=colZ,  style=plot.style_area)
plotshape(isH1,       'Signal', style=shape.circle, color=colB,  location=location.top)
plotshape(isH1,       'Signal', style=shape.circle, color=colB,  location=location.bottom)
plotC0 = hline(0.0,   'Zero',                       color=color.new(CL, 60))
plotC1 = hline( zCrit,'Critical Value',             color=color.new(CL, 60))
plotC2 = hline(-zCrit,'Critical Value',             color=color.new(CL, 60))
fill(plotC0, plotC1, 0,  zCrit, color.new(CL, 99),  color.new(CL, 80), display=isBG?display.all:display.none)
fill(plotC0, plotC2, 0, -zCrit, color.new(CL, 99),  color.new(CL, 80), display=isBG?display.all:display.none)
barcolor(colB)
//
//     Infobox
if isIBox
    var table iBox  = table.new(position.bottom_left, 1, 2, bgcolor = #FDE6E7)
    if barstate.islast 
        table.cell(iBox, 0, 0, "N intrabars used now: "+str.tostring(nIBs))
        table.cell(iBox, 0, 1, "N intrabars used on average: "+str.tostring(avgIBs,'#.##'))
//

// --- Error messages ---
if barstate.islast and avgIBs < THRS
    runtime.error('Insufficient intrabar data available at the given timeframe')
//