Time & volume point of control / quantifytools
// This source code is subject to the terms of the Mozilla Public License 2.0 at https://mozilla.org/MPL/2.0/
// © quantifytools

//@version=5
indicator("Time & volume point of control (TPOC & VPOC)", overlay=true, max_labels_count = 500, max_lines_count = 500, max_boxes_count = 500)

//                      Inputs

//POC inputs
groupPoc = "VPOC & TPOC settings"

i_source = input.source(close, "Source for TPOC/VPOC price", group=groupPoc, tooltip="E.g. With close as source, a point of control is calculated using LTF close.")
i_tpocThreshold = input.int(50, "Threshold % for TPOC highlight", maxval=100, minval=1, group=groupPoc, tooltip="E.g. With 50% threshold, a highlight will occur when >= 50% of total time was traded at point of control.")
i_vpocThreshold = input.int(50, "Threshold % for VPOC highlight", maxval=100, minval=1, group=groupPoc)

i_deltaRule = input.string("Standard VPOC", "Show VPOC as", options=["Standard VPOC", "VPOC with delta indication", "VPOCs separately for each side"], group=groupPoc,  tooltip="Standard VPOC = volume point of control. VPOC with delta indication = volume point of control with direction indicated as color (buy/sell). VPOCs separately for each side = volume point of control for each side separately with direction indicated as color.")
i_buySellPoc = i_deltaRule == "VPOC with delta indication" or i_deltaRule == "VPOCs separately for each side"
i_addPoc = i_deltaRule == "VPOCs separately for each side"

//Timeframe inputs
groupTf = "Timeframe settings"

i_tf1 = input.timeframe("1", "LTF for charts <= 30 min", group=groupTf, tooltip="E.g. Lower timeframe data is fetched from 1 minute timeframe when viewing charts at or below 30 min. The lower the chosen timeframe, the more precision you get, but with the cost of less historical data and slower loading time.")
i_tf2 = input.timeframe("5", "LTF for charts > 30 min & <= 3 hours", group=groupTf)
i_tf3 = input.timeframe("15", "LTF for charts > 3 hours & <= 8 hours", group=groupTf)
i_tf4 = input.timeframe("60", "LTF for charts > 8 hours & <= 1D", group=groupTf)
i_tf5 = input.timeframe("120", "LTF for charts > 1D & <= 3D", group=groupTf)
i_tf6 = input.timeframe("240", "LTF for charts > 3D", group=groupTf)

//TPOC/VPOC visual inputs
groupVisuals = "TPOC/VPOC visuals"

i_hideChart = input.bool(true, "Hide chart", group=groupVisuals, tooltip="")
i_lineCol = input.color(color.gray, "Line", group=groupVisuals, inline="line")
i_lineColThreshold = input.color(color.orange, "Threshold exceeded", group=groupVisuals, inline="line")
i_vpocCol = input.color(color.yellow, "VPOC", group=groupVisuals, inline="vpoc")
i_vpocThresholdCol = input.color(color.orange, "Threshold exceeded", group=groupVisuals, inline="vpoc")
i_tpocCol = input.color(color.yellow, "TPOC", group=groupVisuals, inline="tpoc")
i_tpocThresholdCol = input.color(color.orange, "Threshold exceeded", group=groupVisuals, inline="tpoc")
i_vpocBuyCol = input.color(color.rgb(31, 97, 249), "Buy side POC", group=groupVisuals, inline="vpocdelta")
i_vpocSellCol = input.color(color.rgb(247, 62, 139), "Sell side POC", group=groupVisuals, inline="vpocdelta")



//TPOC/VPOC anomaly visual inputs
groupAnomalyVisuals = "Anomaly visuals"

i_hideAnomalies = input.bool(true, "Hide anomalies", tooltip="", group=groupAnomalyVisuals)
i_trappedTpocVpoc = input.color(color.rgb(232, 74, 74), "Trapped TPOC/VPOC", group=groupAnomalyVisuals)
i_trendInitiationCol = input.color(color.rgb(232, 74, 74), "Trend initiation", group=groupAnomalyVisuals)

//                      LTF data


//Delta rules
upVolRule = i_source >= i_source[1] 
downVolRule = i_source < i_source[1] 

//Current timeframe in minutes
currentTimeframe = timeframe.in_seconds(timeframe.period) / 60

//Dynamic timeframe
dynTf = currentTimeframe <= 30 ? i_tf1 : currentTimeframe > 30 and currentTimeframe <= 180 ? i_tf2 :
 currentTimeframe > 180 and currentTimeframe <= 480 ? i_tf3 : currentTimeframe > 480 and currentTimeframe <= 1440 ? i_tf4 :
  currentTimeframe > 1440 and currentTimeframe <= 4320 ? i_tf5 : currentTimeframe > 4320 ? i_tf6 : na

//Function to fetch LTF data
ltfStats() =>


    vol = na(volume) == false ? volume : 0
    volDelta = upVolRule and na(volume) == false ? volume : downVolRule and na(volume) == false ? 0 - volume : na

    [i_source, vol, volDelta]

[ltfSrc, ltfVolume, ltfDelta]
 = request.security_lower_tf(syminfo.tickerid, dynTf, ltfStats())


//                      TPOC

//Dividing range into 10 blocks/lots
divBlock = (high - low) / 10

//Blocks separately
tpoBlock1 = low + (divBlock * 1)
tpoBlock2 = low + (divBlock * 2)
tpoBlock3 = low + (divBlock * 3)
tpoBlock4 = low + (divBlock * 4)
tpoBlock5 = low + (divBlock * 5)
tpoBlock6 = low + (divBlock * 6)
tpoBlock7 = low + (divBlock * 7)
tpoBlock8 = low + (divBlock * 8)
tpoBlock9 = low + (divBlock * 9)
tpoBlock10 = low + (divBlock * 10)

//Time counters for blocks
int block1Count = 0
int block2Count = 0
int block3Count = 0
int block4Count = 0
int block5Count = 0
int block6Count = 0
int block7Count = 0
int block8Count = 0
int block9Count = 0
int block10Count = 0

//TPO array size
tpoArrSize = array.size(ltfSrc)

//Looping through TPO array and assigning counts to blocks
for i = 0 to (tpoArrSize > 0 ? tpoArrSize - 1 : na)

    recentTpo = array.get(ltfSrc, i)

    if recentTpo < tpoBlock1
        block1Count += 1 
    else
        if recentTpo > tpoBlock1 and recentTpo <= tpoBlock2
            block2Count += 1 
        else   
            if recentTpo > tpoBlock2 and recentTpo <= tpoBlock3
                block3Count += 1 
            else   
                if recentTpo > tpoBlock3 and recentTpo <= tpoBlock4
                    block4Count += 1 
                else   
                    if recentTpo > tpoBlock4 and recentTpo <= tpoBlock5
                        block5Count += 1 
        

    if recentTpo > tpoBlock5 and recentTpo <= tpoBlock6
        block6Count += 1 
    else   
        if recentTpo > tpoBlock6 and recentTpo <= tpoBlock7
            block7Count += 1 
        else   
            if recentTpo > tpoBlock7 and recentTpo <= tpoBlock8
                block8Count += 1 
            else   
                if recentTpo > tpoBlock8 and recentTpo <= tpoBlock9
                    block9Count += 1 
                else
                    if recentTpo > tpoBlock9 and recentTpo <= tpoBlock10
                        block10Count += 1 

//TPOC Array
tpoCountArray = array.new_int(0, 0)

//Populating TPOC array with time counts
array.push(tpoCountArray, block1Count)
array.push(tpoCountArray, block2Count)
array.push(tpoCountArray, block3Count)
array.push(tpoCountArray, block4Count)
array.push(tpoCountArray, block5Count)
array.push(tpoCountArray, block6Count)
array.push(tpoCountArray, block7Count)
array.push(tpoCountArray, block8Count)
array.push(tpoCountArray, block9Count)
array.push(tpoCountArray, block10Count)

//Highest time count (TPOC)
maxTpo = array.max(tpoCountArray)

//Array index of TPOC
indexOfMaxTpo = array.indexof(tpoCountArray, maxTpo)

//Finding TPOC block
relevantTpoBlock = indexOfMaxTpo == 0 ? tpoBlock1 : indexOfMaxTpo == 1 ? tpoBlock2 : indexOfMaxTpo == 2 ? tpoBlock3 : indexOfMaxTpo == 3 ? tpoBlock4 : 
 indexOfMaxTpo == 4 ? tpoBlock5 : indexOfMaxTpo == 5 ? tpoBlock6 : indexOfMaxTpo == 6 ? tpoBlock7 : indexOfMaxTpo == 7 ? tpoBlock8 : 
  indexOfMaxTpo == 8 ? tpoBlock9 : tpoBlock10

//Forming a plot location for TPOC mark (middle point of block)
tpoBlockPos = relevantTpoBlock == tpoBlock1 ? (low + tpoBlock1) / 2 : relevantTpoBlock == tpoBlock2 ? (tpoBlock1 + tpoBlock2) / 2 : relevantTpoBlock == tpoBlock3 ? (tpoBlock2 + tpoBlock3) / 2  : 
 relevantTpoBlock == tpoBlock4 ? (tpoBlock3 + tpoBlock4) / 2  : relevantTpoBlock == tpoBlock5 ? (tpoBlock4 + tpoBlock5) / 2  : relevantTpoBlock == tpoBlock6 ? (tpoBlock5 + tpoBlock6) / 2  : 
  relevantTpoBlock == tpoBlock7 ? (tpoBlock6 + tpoBlock7) / 2  : relevantTpoBlock == tpoBlock8 ? (tpoBlock7 + tpoBlock8) / 2  : relevantTpoBlock == tpoBlock9 ? (tpoBlock8 + tpoBlock9) / 2 : (tpoBlock9 + high) / 2


//                      VPOC

//Max volume from LTF volume/up volume array
maxVolume = array.max(ltfVolume)

//Array index of max volume
indexOfMaxVolume = array.indexof(ltfVolume, maxVolume)

//Price at max volume (VPOC/bVPOC)
maxVol = array.size(ltfSrc) > 0 ? array.get(ltfSrc, indexOfMaxVolume) : na


//Max volume from LTF down volume array
maxVolumeDelta = array.max(ltfDelta)
minVolumeDelta = array.min(ltfDelta)

//Determining additional POC
addVpoc = maxVolumeDelta > math.abs(minVolumeDelta) ? minVolumeDelta : maxVolumeDelta

//Array index of max sell volume
indexOfAddVpoc = array.indexof(ltfDelta, addVpoc)

//Price at max sell volume (sVPOC)
addVpocPrice = array.size(ltfSrc) > 0 ? array.get(ltfSrc, indexOfAddVpoc) : na



//                      TPOC/VPOC anomalies

//Trapped time/volume

//Ensuring sufficient distance between close and TPOC/VPOC, equal to or greater than half of bar range.
vpocCloseSpread = close > maxVol ? close - maxVol : maxVol - close
vpocCloseSpreadCond = vpocCloseSpread >= (hl2 - low)
tpocCloseSpread = close > tpoBlockPos ? close - tpoBlockPos : tpoBlockPos - close
tpocCloseSpreadCond = tpocCloseSpread >= (hl2 - low)

//Trapped volume down scenarios
trappedVpocDown = (close > low[1] and maxVol < low[1]) or (close < low[1] and maxVol < close and vpocCloseSpreadCond)

//Trapped volume up scenarios
trappedVpocUp = (close < high[1] and maxVol > high[1]) or (close > high[1] and maxVol > close and vpocCloseSpreadCond)

//Trapped time down scenarios
trappedTpocDown = (close > low[1] and tpoBlockPos < low[1]) or (close < low[1] and tpoBlockPos < close and tpocCloseSpreadCond)

//Trapped time up scenarios
trappedTpocUp = (close < high[1] and tpoBlockPos > high[1]) or (close > high[1] and tpoBlockPos > close and tpocCloseSpreadCond)

//Trapped volume
trappedVolumeDown = trappedVpocDown and maxVol < hl2 and barstate.isconfirmed
trappedVolumeUp = trappedVpocUp and maxVol > hl2 and barstate.isconfirmed

trappedVolumeAny = trappedVolumeDown or trappedVolumeUp

//Trapped time
trappedTimeDown = trappedTpocDown and tpoBlockPos < hl2 and barstate.isconfirmed
trappedTimeUp = trappedTpocUp and tpoBlockPos > hl2 and barstate.isconfirmed

trappedTimeAny = trappedTimeDown or trappedTimeUp

//Trend initiation

//Defining volatility
volatility = high - low

//Ensuring distance between TPOC and VPOC is sufficient, equal to or greater than half of bar range.
tpocVpocSpread = tpoBlockPos > maxVol ? tpoBlockPos - maxVol : maxVol - tpoBlockPos
tpocVpocSpreadCond = tpocVpocSpread >= (hl2 - low)

//Trend initiation up/down
trendInitiationUp = close > high[1] and tpoBlockPos < hl2 and maxVol > hl2 and volatility > volatility[1] * 1.5 and tpocVpocSpreadCond and barstate.isconfirmed
trendInitiationDown = close < low[1] and (maxVol < hl2 and tpoBlockPos > hl2) and volatility > volatility[1] * 1.5 and tpocVpocSpreadCond and barstate.isconfirmed

trendInitiationAny = trendInitiationUp or trendInitiationDown


//                      Alerts

alertcondition(trappedTimeUp, "Trapped TPOC above", "Potential trapped TPOC above detected.")
alertcondition(trappedTimeDown, "Trapped TPOC below", "Potential trapped TPOC below detected.")

alertcondition(trappedVolumeUp, "Trapped VPOC above", "Potential trapped VPOC above detected.")
alertcondition(trappedVolumeDown, "Trapped VPOC below", "Potential trapped VPOC below detected.")

alertcondition(trappedTimeAny, "Trapped TPOC above/below", "Potential trapped TPOC above/below detected.")
alertcondition(trappedVolumeAny, "Trapped VPOC above/below", "Potential trapped VPOC above/below detected.")
alertcondition(trappedTimeAny or trappedVolumeAny, "Trapped POC (either) above/below", "Potential trapped POC (either) above/below detected.")

alertcondition(trendInitiationUp, "Trend initiation up", "Potential trend initiation up detected.")
alertcondition(trendInitiationDown, "Trend initiation down", "Potential trend initiation down detected.")
alertcondition(trendInitiationAny, "Trend initiation any", "Potential trend initiation up/down detected.")


//                      Colors

vpocThresholdMet = maxVolume >= (volume * (i_vpocThreshold / 100)) and barstate.isconfirmed
tpocThresholdMet = maxTpo >= (tpoArrSize * (i_tpocThreshold / 100)) and barstate.isconfirmed

vpocColor = (trappedVolumeUp or trappedVolumeDown) and i_hideAnomalies == false ? i_trappedTpocVpoc : vpocThresholdMet ? i_vpocThresholdCol : i_vpocCol
tpocColor = (trappedTimeUp or trappedTimeDown) and i_hideAnomalies == false ? i_trappedTpocVpoc : tpocThresholdMet ? i_tpocThresholdCol : i_tpocCol
lineColor = vpocThresholdMet or tpocThresholdMet ? i_lineColThreshold : i_lineCol
boxColor = vpocThresholdMet[500] or tpocThresholdMet[500] ? i_lineColThreshold : i_lineCol


//                      Plots

plotVpocCond = tpoArrSize > 0 and na(volume) == false and volume > 0 
mainVpocCol = i_buySellPoc ? addVpoc == maxVolumeDelta ? i_vpocSellCol : i_vpocBuyCol : vpocColor
addVpocCond = addVpoc == maxVolumeDelta ? i_vpocBuyCol : i_vpocSellCol 

//TPOC and VPOC plots
plotshape(tpoArrSize > 0 ? tpoBlockPos : na, title="TPOC", style=shape.cross, text="", color=tpocColor, location=location.absolute, size=size.small, textcolor=color.white)
plotshape(plotVpocCond ? maxVol : na, title="VPOC #1", style=shape.circle, text="", color=mainVpocCol, location=location.absolute, size=size.tiny, textcolor=color.white)
plotshape(i_buySellPoc and i_addPoc ? addVpocPrice : na, title="VPOC #2", style=shape.cross, text="", color=addVpocCond, location=location.absolute, size=size.small, textcolor=color.white)


plotshape(i_hideAnomalies == false and trendInitiationUp ? low : na, title="Trend initiation up", style=shape.triangleup, text="", color=i_trendInitiationCol, location=location.belowbar, size=size.tiny, textcolor=color.white)
plotshape(i_hideAnomalies == false and trendInitiationDown ? high : na, title="Trend initiation down", style=shape.triangledown, text="", color=i_trendInitiationCol, location=location.abovebar, size=size.tiny, textcolor=color.white)

//Mid-bar line
line.new(bar_index, high, bar_index, low, xloc=xloc.bar_index, color=lineColor, width=3)
box.new(left=bar_index[500], top=high[500], right=bar_index[500], bottom=low[500], bgcolor=boxColor, border_color=boxColor, border_width=2)

//Hiding chart
barcolor(i_hideChart == true ? color.new(color.white, 100) : na)

