Tick based chart [DotH]
// This source code is subject to the terms of the Mozilla Public License 2.0 at https://mozilla.org/MPL/2.0/
// © doth4581 (DotH)
// Version 1.0 - 2nd January 2023
// This is an implementation for displaying a tick based chart in Trading View. 
//
// Tick based charts are charts with candles that are rendered in the same way as traditional candles.
// However, instead of rendering a new candle at a specific time period,tick based candles are rendered after
// a set number of ticks have occured. 
// There are benefits to using ticks based candles, as each candle represents the same number of "price moves"
// For more info see this page (not affiliated with me):  
// https://www.centralcharts.com/en/gm/1-learn/7-technical-analysis/29-chart/569-understanding-tick-charts
//
// Limitations/known issues:
//  - Currently the indicator has been restricted to 100 candles. This is for limiting the line and box usage to a max of 300 objects.
//  - On timeframes above 1 minute, the seconds values will always be 0.  In order to be able to see seconds values in the chart scale 
//  you need to be on a second level chart, which requires a premium TradingView subscription.
//  - Changing the parameters in the settings will cause the chart to empty and start redrawing from its first candle again. This is
//  because the tick chart is being drawn from realtime data, unlike the standard TradingView charts. 
//
// TODOs & Bugs:
//  - Add some moving average indicators (SMA, EMA as a minmum)
//  - Add a corresponding tick based volume chart
//  - Create RSI, MACD, BB variations of this indicator
//  if you have any ideas/suggestions or bug reports, please feel free to let me know, however keep in mind
//  that I do not have too much spare time to add things, so updates are going to be sporadic.
//
// CODE REUSE:
// If you plan to use this code to make a derivative indicator or strategy, it would be nice to know,
// so let me know if you like!
// 



//@version=5
indicator(shorttitle = "TBC", title="Tick based chart [DotH]", overlay=false,max_boxes_count = 100,max_lines_count = 200,max_bars_back = 2)
//Input parameters
int ticks_per_candle=input.int(20,"Ticks per candle",1,1000,1)
int max_candles_in_chart=input.int(50,"Max number of candles to render",5,100,1)
int scale_labels_pos_ticksize=input.int(1,"Time scale labels position below ticksize multiplier",0,100,1)

//Initialize our globals
varip openp=open
varip highp=high
varip lowp=low
varip closep=close
varip timep=time
varip tick_count = ticks_per_candle
varip open_prices = array.new_float()
varip high_prices = array.new_float()
varip low_prices = array.new_float()
varip close_prices = array.new_float()
varip close_times = array.new_int()
varip tick_counter = array.new_int(1,ticks_per_candle+1)
varip new_candle = array.new_bool(1,true)
varip openID=0
varip highID=1
varip lowID=2
varip closeID=3
varip timeID=4
varip prices = array.new_float(5,close)

//Reset a candle for first use
reset_candle_prices() =>
    array.set(prices,openID,close)
    array.set(prices,highID,close)
    array.set(prices,lowID,close)
    array.set(prices,closeID,close)
    array.set(prices,timeID,time)

//Calculate OHLC of candle
update_candle_prices(tick_price) =>
    array.set(prices,timeID,time)
    if array.get(new_candle,0)
        array.set(prices,openID,tick_price)
        array.set(prices,highID,tick_price)
        array.set(prices,lowID,tick_price)
        array.set(prices,closeID,tick_price)
        array.set(new_candle,0,false)
    else
        array.set(prices,highID,math.max(array.get(prices,highID), tick_price))
        array.set(prices,lowID,math.min(array.get(prices,lowID), tick_price))
        array.set(prices,closeID,tick_price)

//Update and close the last candle and add a new candle
save_candle() =>
    array.unshift(open_prices,array.get(prices,openID))
    array.unshift(high_prices,array.get(prices,highID))
    array.unshift(low_prices,array.get(prices,lowID))
    array.unshift(close_prices,array.get(prices,closeID))
    array.unshift(close_times,int(array.get(prices,timeID)))
    reset_candle_prices()
    if (array.size(open_prices)>max_candles_in_chart)
        array.pop(open_prices)
        array.pop(high_prices)
        array.pop(low_prices)
        array.pop(close_prices)
        array.pop(close_times)

//Update values of last candle
update_last_candle() =>
    array.set(high_prices,0,array.get(prices,highID))
    array.set(low_prices,0,array.get(prices,lowID))
    array.set(close_prices,0,array.get(prices,closeID))
    array.set(close_times,0,int(array.get(prices,timeID)))

//Calculate candles
updatecandles(tick_price) =>
    update_candle_prices(tick_price)
    
    tick_counter_newval = array.get(tick_counter,0)+1
    array.set(tick_counter,0,tick_counter_newval)
    if array.get(tick_counter,0) >= tick_count
        save_candle()
        array.set(tick_counter,0,0)
        array.set(new_candle,0,false)
    
//If this is the last bar, calculate our tick based candles
if last_bar_index==bar_index 
    updatecandles(close)
    
//Clear all previous boxes
a_allBoxes = box.all
if array.size(a_allBoxes) > 0
	for i = 0 to array.size(a_allBoxes) - 1
		box.delete(array.get(a_allBoxes, i))

//Clear all previous labels
a_allLabels = label.all
if array.size(a_allLabels) > 0
	for i = 0 to array.size(a_allLabels) - 1
		label.delete(array.get(a_allLabels, i))

//Clear all previous lines
a_allLines = line.all
if array.size(a_allLines) > 0
	for i = 0 to array.size(a_allLines) - 1
		line.delete(array.get(a_allLines, i))


// Calculate scale and time labeling minimum and maximum positions
lowPosT=math.min(array.min(low_prices),array.get(prices,lowID))-syminfo.mintick*scale_labels_pos_ticksize
highPosT=math.max(array.max(high_prices),array.get(prices,highID))+syminfo.mintick*scale_labels_pos_ticksize
//Render past candles
if array.size(open_prices)>0
    for i = 0 to array.size(open_prices)-1 by 1
        pos=i*3
        openp:=array.get(open_prices,i)
        highp:=array.get(high_prices,i)
        lowp:=array.get(low_prices,i)
        closep:=array.get(close_prices,i)
        timep:=array.get(close_times,i)
        past_candles_color=openp>closep?color.red:color.rgb(37, 190, 150)
        box.new(bar_index-pos,math.max(openp,closep),bar_index-pos+2,math.min(openp,closep),past_candles_color,1,line.style_solid,extend.none,xloc.bar_index,past_candles_color)
        line.new(bar_index-pos+1,math.max(openp,closep),bar_index-pos+1,highp,color = past_candles_color,width = 2)
        line.new(bar_index-pos+1,math.min(openp,closep),bar_index-pos+1,lowp,color = past_candles_color,width = 2)
        if pos%30==0 and pos>15
            label.new(bar_index-pos+1,lowPosT,str.format_time(timep,'MM/DD-HH:mm:ss'),size = size.tiny,textcolor = color.white,style = label.style_none,yloc = yloc.price,textalign = text.align_center,text_font_family=font.family_monospace)
            line.new(bar_index-pos+1,lowPosT,bar_index-pos+1,highPosT,color = color.gray,width = 1,style = line.style_dotted)
//Render current candle
lpos=-3
current_candle_color=array.get(prices,openID)>array.get(prices,closeID)?color.red:color.rgb(37, 190, 150)
box.new(bar_index-lpos,math.max(array.get(prices,openID),array.get(prices,closeID)),bar_index-lpos+2,math.min(array.get(prices,openID),array.get(prices,closeID)),current_candle_color,1,line.style_solid,extend.none,xloc.bar_index,current_candle_color)
line.new(bar_index-lpos+1,math.max(array.get(prices,openID),array.get(prices,closeID)),bar_index-lpos+1,array.get(prices,highID),color = current_candle_color,width = 2)
line.new(bar_index-lpos+1,math.min(array.get(prices,openID),array.get(prices,closeID)),bar_index-lpos+1,array.get(prices,lowID),color = current_candle_color,width = 2)
label.new(bar_index-lpos+1,lowPosT,str.format_time(int(array.get(prices,timeID)),'MM/DD-HH:mm:ss'),size = size.tiny,textcolor = color.white,style = label.style_none,yloc = yloc.price,textalign = text.align_center,text_font_family=font.family_monospace)
line.new(bar_index-lpos+1,lowPosT,bar_index-lpos+1,highPosT,color = color.gray,width = 1,style = line.style_dotted)
