Ultimate Oscillator + Realtime Divergences
// This source code is subject to the terms of the Mozilla Public License 2.0 at https://mozilla.org/MPL/2.0/
// © tvenn

//@version=5
indicator("Ultimate Oscillator + Realtime Divergences", shorttitle="UO+", overlay=false, max_bars_back = 1000, max_lines_count = 400, max_labels_count = 400, precision=3)

pulldatafromtimeframe = input.string("Chart", title="Select alternate timeframe in mins", options=["Chart", "1", "2", "3", "4", "5", "10", "15", "30", "45", "60", "120", "240"])

green   = color.new(#95BD5F, 30)
red     = color.new(#EA1889, 30)
transp  = color.new(#FFFFFF, 100)

 
haTicker = ticker.heikinashi(syminfo.tickerid)
[haO, haH, haL, haC] = request.security(haTicker, timeframe.period, [open, high, low, close])

//Divergence Settings
grp_DIVS    = "Divergence Settings"
showlines   = input(defval = true, title = "Show Divergence Lines", group=grp_DIVS)
showlast    = input(defval = true, title = "Show Only Last Divergence", group=grp_DIVS)
dontconfirm = input(defval = true, title = "Don't Wait for Confirmation", group=grp_DIVS)
searchdiv = input.string(defval = "Regular/Hidden", title = "Divergence Type", options = ["Regular", "Hidden", "Regular/Hidden"], group=grp_DIVS)


//Detail
grp_Detail          = "Details"
showCenterBand      = input(true, title="Centerline", group=grp_Detail, inline="0", tooltip="To add a solid centerline that stays behind the oscillator, simply adjust the color of the 'Alt centerline' within the styles section below and disable this centerline")
showBands           = input(false, title="Range bands", group=grp_Detail, inline="1", tooltip="The color and style of the range bands can be further customised in the 'Style' tab of this settings menu.")
convertToHAbands    = input(false, title="Heikin Ashi range band levels", group=grp_Detail, inline="1", tooltip="This will adjust the overbought and oversold levels to 35 and 65.\n\nIf you are working on a Heikin Ashi chart, select this option and it will increase the height of the oscillator in the panel.")
fadeOutOsc          = input(false, title="Fade out oscillator", group=grp_Detail, tooltip="Fade out the oscillator leaving only the most recent periods prominent for a clearer chart.")
flipOsc             = input.bool(false, title="Flip Oscillator", group=grp_Detail, tooltip="This will flip the oscillator upside down. The purpose is for use with the flip chart feature of Tradingview (Alt+i), which does not also flip the oscillator. This may help those with a particular long/short bias to see the other side of things. Divergence lines will not be drawn.")


grp_COND            = "Conditional styles"
backgroundColor     = input(color.new(color.white, 97), title = "", group=grp_COND, inline='clc')
showBackground      = input(false, title="Background", group=grp_COND, inline='clc')
obosHighlightColor  = input(color.new(red, 0), title = "", group=grp_COND, inline='obos')
obosHighlightEnabled= input.bool(false, title="Highlight overbought & oversold levels", group=grp_COND, inline="obos")
centerlineXBgColor  = input(color.new(red, 90), title='', group=grp_COND, inline='clc1')
backgroundSignalOn  = input(false, title="Centerline crossunder background color", group=grp_COND, tooltip="This will colour the background according to whether the RSI is above or below the 50 level.", inline='clc1')
centerlineXOscColor = input(color.new(red, 10), title='', group=grp_COND, inline='clc2')
oscSignalOn         = input(false, title="Centerline crossunder oscillator color", group=grp_COND, tooltip="This will colour the oscillator according to whether the RSI is above or below the 50 level.", inline='clc2')


convertToHA         = input(false, title="Enable Heikin Ashi mode for non HA charts", group="Heikin Ashi mode", tooltip="This will use Heikin Ashi formula Open and Close values. The purpose is for use on standard chart types, in order to produce a smoother less volatile line.\n\nEnabling this setting will also adjust the Range Band overbought and oversold levels to 35 and 65.")
haCheckHigh         = convertToHA ? haH : high
haCheckLow          = convertToHA ? haL : low
haCheckClose        = convertToHA ? haC : close



//UO settings
grp_UOL = "UO settings"
oscLineColor = input.color(color.blue, title="Color", group=grp_UOL)
length1 = input.int(7, minval=1, title = "Fast Length", group=grp_UOL)
length2 = input.int(14, minval=1, title = "Middle Length", group=grp_UOL)
length3 = input.int(28, minval=1, title = "Slow Length", group=grp_UOL)


//Bands
centerlineupper = hline(50.3, color=color.new(#000000, 100), title="Alt centerline", linestyle=hline.style_solid, linewidth=1)
centerlinelower = hline(49.7, color=color.new(#000000, 100), title="Alt centerline", linestyle=hline.style_solid, linewidth=1)

bandHigh = hline(70, color=color.new(color.gray, 30), title="Band level", linestyle=hline.style_dotted, linewidth=1, display=(showBands and not (convertToHAbands or convertToHA) ? display.all : display.none))
bandHighHA = hline(65, color=color.new(color.gray, 30), title="HA Band level", linestyle=hline.style_dotted, linewidth=1, display=((showBands and convertToHAbands) or (showBands and convertToHA) ? display.all : display.none))
bandCenter = hline(50, color=(showCenterBand ? color.gray : color.new(#777777, 100)), title="Centerline", linestyle=hline.style_dotted, linewidth=1)
bandLowHA = hline(35, color=color.new(color.gray, 30), title="HA Band level", linestyle=hline.style_dotted, linewidth=1, display=((showBands and convertToHAbands) or (showBands and convertToHA) ? display.all : display.none))
bandLow = hline(30, color=color.new(color.gray, 30), title="Band level", linestyle=hline.style_dotted, linewidth=1, display=(showBands and not (convertToHAbands or convertToHA) ? display.all : display.none))

//UO calculations
average(bp, tr_, length) => math.sum(bp, length) / math.sum(tr_, length)
high_ = math.max(haCheckHigh, convertToHA ? haC[1] : close[1])
low_ = math.min(haCheckLow, convertToHA ? haC[1] : close[1])
 
bp = (haCheckClose) - low_
tr_ = high_ - low_
avg7 = average(bp, tr_, length1)
avg14 = average(bp, tr_, length2)
avg28 = average(bp, tr_, length3)
osc = flipOsc ? 100-(100 * (4*avg7 + 2*avg14 + avg28)/7) : (100 * (4*avg7 + 2*avg14 + avg28)/7)
//UO end

//Plot oscillator
distanceTransparency = (bar_index > (last_bar_index - 30) ? 10 : (bar_index > last_bar_index - 60 ? 20 : (bar_index > last_bar_index - 80 ? 30 : (bar_index > last_bar_index - 100 ? 40 : (bar_index > last_bar_index - 120 ? 50 : (bar_index > last_bar_index - 140 ? 60 : (bar_index > last_bar_index - 160 ? 70 : 80)))))))

ultimate_value = request.security(syminfo.tickerid, pulldatafromtimeframe == "Chart" ? "" : pulldatafromtimeframe, osc, barmerge.gaps_on)
flippedOsc = (ultimate_value * -1)
oscColorHA = ((obosHighlightEnabled and osc > 65) ? obosHighlightColor : ((obosHighlightEnabled and osc < 35) ? obosHighlightColor : (oscSignalOn and osc > 50 ? oscLineColor : (oscSignalOn and osc < 50 ? centerlineXOscColor : (fadeOutOsc ? color.new(oscLineColor, distanceTransparency) : oscLineColor)))))
oscColor = ((obosHighlightEnabled and osc > 70) ? obosHighlightColor : ((obosHighlightEnabled and osc < 30) ? obosHighlightColor : (oscSignalOn and osc > 50 ? oscLineColor : (oscSignalOn and osc < 50 ? centerlineXOscColor : (fadeOutOsc ? color.new(oscLineColor, distanceTransparency) : oscLineColor)))))



plot(ultimate_value, color=convertToHA ? oscColorHA : oscColor, linewidth=2, title="UO")


//background fill options
fill(bandHighHA, bandLowHA, backgroundSignalOn and ultimate_value > 50 ? na : backgroundSignalOn and ultimate_value < 50 ? centerlineXBgColor : na, title="Centerline crossover colors", editable=1)

fill(bandHighHA, bandLowHA, showBackground and (convertToHA or convertToHAbands) ? backgroundColor : na, title="HA Background", editable=1)
fill(bandHigh, bandLow, showBackground and not (convertToHA or convertToHAbands) ? backgroundColor : na, title="Background", editable=1)


//Pivot settings
grp_PPS = "Pivot Point Settings"
pp      = input.int(defval = 12, title = "Pivot period", minval = 1, maxval = 50, group=grp_PPS)
maxpp   = input.int(defval = 5, title = "Maximum Pivot periods to check for divs", minval = 1, maxval = 100, group=grp_PPS)
maxbars = input.int(defval = 100, title = "Maximum Bars to Check", minval = 1, maxval = 300, group=grp_PPS)
source  = "Close"

prd = pp


//Styles
grp_STY         = "Styles"
altCenterLineColor = input.color(color.new(color.orange, 100), title="Alt centerline color", group=grp_STY)
pos_reg_div_col = input(defval = green, title = "Positive Regular Divergence", group=grp_STY)
neg_reg_div_col = input(defval = red, title = "Negative Regular Divergence", group=grp_STY)
pos_hid_div_col = input(defval = green, title = "Positive Hidden Divergence", group=grp_STY)
neg_hid_div_col = input(defval = red, title = "Negative Hidden Divergence", group=grp_STY)
reg_div_l_style_= input.string(defval = "Solid", title = "Regular Divergence Line Style", options = ["Solid", "Dashed", "Dotted"], group=grp_STY)
hid_div_l_style_= input.string(defval = "Dotted", title = "Hdden Divergence Line Style", options = ["Solid", "Dashed", "Dotted"], group=grp_STY)
reg_div_l_width = input.int(defval = 2, title = "Regular Divergence Line Width", minval = 1, maxval = 2, group=grp_STY)
hid_div_l_width = input.int(defval = 2, title = "Hidden Divergence Line Width", minval = 1, maxval = 2, group=grp_STY)

fill(centerlineupper, centerlinelower, altCenterLineColor, title="Alt centerline fill", display=display.all, editable=1)

// set line styles
var reg_div_l_style = reg_div_l_style_ == "Solid" ? line.style_solid : 
                       reg_div_l_style_ == "Dashed" ? line.style_dashed :
                       line.style_dotted
var hid_div_l_style = hid_div_l_style_ == "Solid" ? line.style_solid : 
                       hid_div_l_style_ == "Dashed" ? line.style_dashed :
                       line.style_dotted

// get indicators
uo = ultimate_value

// keep indicator colors in arrays
var indicators_name = array.new_string(11)
var div_colors = array.new_color(4)
if barstate.isfirst
    //colors
    array.set(div_colors, 0, pos_reg_div_col)
    array.set(div_colors, 1, neg_reg_div_col)
    array.set(div_colors, 2, pos_hid_div_col)
    array.set(div_colors, 3, neg_hid_div_col)

// Check if we get new Pivot High Or Pivot Low
float ph = ta.pivothigh(haCheckClose, prd, prd)
float pl = ta.pivotlow(haCheckClose, prd, prd)

// keep values and positions of Pivot Highs/Lows in the arrays
var int maxarraysize = 20
var ph_positions = array.new_int(maxarraysize, 0)
var pl_positions = array.new_int(maxarraysize, 0)
var ph_vals = array.new_float(maxarraysize, 0.)
var pl_vals = array.new_float(maxarraysize, 0.)

// add PHs to the array
if ph
    array.unshift(ph_positions, bar_index)
    array.unshift(ph_vals, ph)
    if array.size(ph_positions) > maxarraysize
        array.pop(ph_positions)
        array.pop(ph_vals)

// add PLs to the array
if pl
    array.unshift(pl_positions, bar_index)
    array.unshift(pl_vals, pl)
    if array.size(pl_positions) > maxarraysize
        array.pop(pl_positions)
        array.pop(pl_vals)

// functions to check Regular Divergences and Hidden Divergences

// function to check positive regular or negative hidden divergence
// cond == 1 => positive_regular, cond == 2=> negative_hidden
positive_regular_positive_hidden_divergence(src, cond)=>
    divlen = 0
    prsc = haCheckClose
    // if indicators higher than last value and close price is higher than las close 
    if dontconfirm or src > src[1] or haCheckClose > haCheckClose[1]
        startpoint = dontconfirm ? 0 : 1 // don't check last candle
        // we search last 15 PPs
        for x = 0 to maxpp - 1
            len = bar_index - array.get(pl_positions, x) + prd
            // if we reach non valued array element or arrived 101. or previous bars then we don't search more
            if array.get(pl_positions, x) == 0 or len > maxbars
                break
            if len > 5 and 
               ((cond == 1 and src[startpoint] > src[len] and prsc[startpoint] < nz(array.get(pl_vals, x))) or
               (cond == 2 and src[startpoint] < src[len] and prsc[startpoint] > nz(array.get(pl_vals, x))))
                slope1 = (src[startpoint] - src[len]) / (len - startpoint)
                virtual_line1 = src[startpoint] - slope1
                slope2 = (haCheckClose[startpoint] - haCheckClose[len]) / (len - startpoint)
                virtual_line2 = haCheckClose[startpoint] - slope2
                arrived = true
                for y = 1 + startpoint to len - 1
                    if src[y] < virtual_line1 or nz(haCheckClose[y]) < virtual_line2
                        arrived := false
                        break
                    virtual_line1 := virtual_line1 - slope1
                    virtual_line2 := virtual_line2 - slope2
                
                if arrived
                    divlen := len
                    break
    divlen

// function to check negative regular or positive hidden divergence
// cond == 1 => negative_regular, cond == 2=> positive_hidden
negative_regular_negative_hidden_divergence(src, cond)=>
    divlen = 0
    prsc = haCheckClose
    // if indicators higher than last value and close price is higher than las close 
    if dontconfirm or src < src[1] or haCheckClose < haCheckClose[1]
        startpoint = dontconfirm ? 0 : 1 // don't check last candle
        // we search last 15 PPs
        for x = 0 to maxpp - 1
            len = bar_index - array.get(ph_positions, x) + prd
            // if we reach non valued array element or arrived 101. or previous bars then we don't search more
            if array.get(ph_positions, x) == 0 or len > maxbars
                break
            if len > 5 and 
               ((cond == 1 and src[startpoint] < src[len] and prsc[startpoint] > nz(array.get(ph_vals, x))) or 
               (cond == 2 and src[startpoint] > src[len] and prsc[startpoint] < nz(array.get(ph_vals, x))))
                slope1 = (src[startpoint] - src[len]) / (len - startpoint)
                virtual_line1 = src[startpoint] - slope1
                slope2 = (haCheckClose[startpoint] - nz(haCheckClose[len])) / (len - startpoint)
                virtual_line2 = haCheckClose[startpoint] - slope2
                arrived = true
                for y = 1 + startpoint to len - 1
                    if src[y] > virtual_line1 or nz(haCheckClose[y]) > virtual_line2
                        arrived := false
                        break
                    virtual_line1 := virtual_line1 - slope1
                    virtual_line2 := virtual_line2 - slope2
                
                if arrived
                    divlen := len
                    break
    divlen

// calculate 4 types of divergence if enabled in the options and return divergences in an array
calculate_divs(cond, indicator)=>
    divs = array.new_int(4, 0)
    array.set(divs, 0, cond and (searchdiv == "Regular" or searchdiv == "Regular/Hidden") ? positive_regular_positive_hidden_divergence(indicator, 1) : 0)
    array.set(divs, 1, cond and (searchdiv == "Regular" or searchdiv == "Regular/Hidden") ? negative_regular_negative_hidden_divergence(indicator, 1) : 0)
    array.set(divs, 2, cond and (searchdiv == "Hidden" or searchdiv == "Regular/Hidden")  ? positive_regular_positive_hidden_divergence(indicator, 2) : 0)
    array.set(divs, 3, cond and (searchdiv == "Hidden" or searchdiv == "Regular/Hidden")  ? negative_regular_negative_hidden_divergence(indicator, 2) : 0)
    divs

// array to keep all divergences
var all_divergences = array.new_int(4) // 1 indicator * 4 divergence = 4 elements
// set related array elements
array_set_divs(div_pointer, index)=>
    for x = 0 to 3
        array.set(all_divergences, index * 4 + x, array.get(div_pointer, x))

// set divergences array 
array_set_divs(calculate_divs(true, uo), 0)

// keep line in an array
var pos_div_lines = array.new_line(0)
var neg_div_lines = array.new_line(0)
var pos_div_labels = array.new_label(0)
var neg_div_labels = array.new_label(0) 

// remove old lines and labels if showlast option is enabled
delete_old_pos_div_lines()=>
    if array.size(pos_div_lines) > 0    
        for j = 0 to array.size(pos_div_lines) - 1 
            line.delete(array.get(pos_div_lines, j))
        array.clear(pos_div_lines)

delete_old_neg_div_lines()=>
    if array.size(neg_div_lines) > 0    
        for j = 0 to array.size(neg_div_lines) - 1 
            line.delete(array.get(neg_div_lines, j))
        array.clear(neg_div_lines)

delete_old_pos_div_labels()=>
    if array.size(pos_div_labels) > 0 
        for j = 0 to array.size(pos_div_labels) - 1 
            label.delete(array.get(pos_div_labels, j))
        array.clear(pos_div_labels)

delete_old_neg_div_labels()=>
    if array.size(neg_div_labels) > 0    
        for j = 0 to array.size(neg_div_labels) - 1 
            label.delete(array.get(neg_div_labels, j))
        array.clear(neg_div_labels)

// delete last creted lines and labels until we met new PH/PV 
delete_last_pos_div_lines_label(n)=>
    if n > 0 and array.size(pos_div_lines) >= n    
        asz = array.size(pos_div_lines)
        for j = 1 to n
            line.delete(array.get(pos_div_lines, asz - j))
            array.pop(pos_div_lines)
        if array.size(pos_div_labels) > 0  
            label.delete(array.get(pos_div_labels, array.size(pos_div_labels) - 1))
            array.pop(pos_div_labels)

delete_last_neg_div_lines_label(n)=>
    if n > 0 and array.size(neg_div_lines) >= n    
        asz = array.size(neg_div_lines)
        for j = 1 to n
            line.delete(array.get(neg_div_lines, asz - j))
            array.pop(neg_div_lines)
        if array.size(neg_div_labels) > 0  
            label.delete(array.get(neg_div_labels, array.size(neg_div_labels) - 1))
            array.pop(neg_div_labels)
            
// variables for Alerts
pos_reg_div_detected = false
neg_reg_div_detected = false
pos_hid_div_detected = false
neg_hid_div_detected = false

// to remove lines/labels until we met new // PH/PL
var last_pos_div_lines = 0
var last_neg_div_lines = 0
var remove_last_pos_divs = false 
var remove_last_neg_divs = false
if pl
    remove_last_pos_divs := false
    last_pos_div_lines := 0
if ph
    remove_last_neg_divs := false
    last_neg_div_lines := 0

// draw divergences lines and labels
divergence_text_top = ""
divergence_text_bottom = ""
distances = array.new_int(0)
dnumdiv_top = 0
dnumdiv_bottom = 0
top_label_col = color.white
bottom_label_col = color.white
old_pos_divs_can_be_removed = true
old_neg_divs_can_be_removed = true
startpoint = dontconfirm ? 0 : 1 // used for don't confirm option

for x = 0 to 0
    div_type = -1
    for y = 0 to 3
        if array.get(all_divergences, x * 4 + y) > 0 // any divergence?
            div_type := y
            if (y % 2) == 1 
                dnumdiv_top := dnumdiv_top + 1
                top_label_col := array.get(div_colors, y)
            if (y % 2) == 0
                dnumdiv_bottom := dnumdiv_bottom + 1
                bottom_label_col := array.get(div_colors, y)
            if not array.includes(distances, array.get(all_divergences, x * 4 + y))  // line not exist ?
                array.push(distances, array.get(all_divergences, x * 4 + y))
                new_line = (showlines and not flipOsc) ? line.new(x1 = bar_index - array.get(all_divergences, x * 4 + y), 
                          y1 = (source == "Close" ? uo[array.get(all_divergences, x * 4 + y)] : 
                                           (y % 2) == 0 ? haCheckLow[array.get(all_divergences, x * 4 + y)] : 
                                                          haCheckHigh[array.get(all_divergences, x * 4 + y)]),
                          x2 = bar_index - startpoint,
                          y2 = (source == "Close" ? uo[startpoint] : 
                                           (y % 2) == 0 ? haCheckLow[startpoint] : 
                                                          haCheckHigh[startpoint]),
                          color = array.get(div_colors, y),
                          style = y < 2 ? reg_div_l_style : hid_div_l_style,
                          width = y < 2 ? reg_div_l_width : hid_div_l_width
                          )
                          : na
                if (y % 2) == 0
                    if old_pos_divs_can_be_removed
                        old_pos_divs_can_be_removed := false
                        if not showlast and remove_last_pos_divs
                            delete_last_pos_div_lines_label(last_pos_div_lines)
                            last_pos_div_lines := 0
                        if showlast
                            delete_old_pos_div_lines()
                    array.push(pos_div_lines, new_line)
                    last_pos_div_lines := last_pos_div_lines + 1
                    remove_last_pos_divs := true
                    
                if (y % 2) == 1
                    if old_neg_divs_can_be_removed
                        old_neg_divs_can_be_removed := false
                        if not showlast and remove_last_neg_divs
                            delete_last_neg_div_lines_label(last_neg_div_lines)
                            last_neg_div_lines := 0
                        if showlast
                            delete_old_neg_div_lines()
                    array.push(neg_div_lines, new_line)
                    last_neg_div_lines := last_neg_div_lines + 1
                    remove_last_neg_divs := true
                    
            // set variables for alerts
            if y == 0
                pos_reg_div_detected := true
            if y == 1
                neg_reg_div_detected := true
            if y == 2
                pos_hid_div_detected := true
            if y == 3
                neg_hid_div_detected := true



                                 
    
alertcondition(pos_reg_div_detected and not pos_reg_div_detected[1], title='Regular Bullish Divergence in UO', message='Regular Bullish Divergence in UO')
alertcondition(neg_reg_div_detected and not neg_reg_div_detected[1], title='Regular Bearish Divergence in UO', message='Regular Bearish Divergence in UO')
alertcondition(pos_hid_div_detected and not pos_hid_div_detected[1], title='Hidden Bullish Divergence in UO', message='Hidden Bullish Divergence in UO')
alertcondition(neg_hid_div_detected and not neg_hid_div_detected[1], title='Hidden Bearish Divergence in UO', message='Hidden Bearish Divergence in UO')

alertcondition((pos_reg_div_detected and not pos_reg_div_detected[1]) or (pos_hid_div_detected and not pos_hid_div_detected[1]), title='Bullish Divergence in UO', message='Bullish Divergence in UO')
alertcondition((neg_reg_div_detected and not neg_reg_div_detected[1]) or (neg_hid_div_detected and not neg_hid_div_detected[1]), title='Bearish Divergence Detected in UO', message='Bearish Divergence Detected in UO')

alertcondition((pos_reg_div_detected and not pos_reg_div_detected[1]) or (pos_hid_div_detected and not pos_hid_div_detected[1]) or (neg_reg_div_detected and not neg_reg_div_detected[1]) or (neg_hid_div_detected and not neg_hid_div_detected[1]), title='Divergence Detected in UO', message='Divergence Detected in UO')


//Oscillator label
var table isFlippedLabel = table.new(position.bottom_left, 1, 1, bgcolor = color.new(color.black, 100), frame_width = 0, frame_color = color.new(#000000, 100))

if barstate.islast and flipOsc
    // We only populate the table on the last bar.
    table.cell(isFlippedLabel, 0, 0, text="Flipped", text_color=color.gray, text_halign=text.align_left, text_size=size.small, bgcolor=color.new(#000000, 100))