Range Projections [TFO]
// This source code is subject to the terms of the Mozilla Public License 2.0 at https://mozilla.org/MPL/2.0/
// © tradeforopp

//@version=5
indicator("Range Projections [TFO]", "Range Projections [TFO]", true, max_boxes_count = 500, max_lines_count = 500, max_labels_count = 500)

// -------------------------------------------------- Inputs --------------------------------------------------
var g_SET = "Settings"
session         = input.session("1600-2000", "Session", inline = "X", tooltip = "The time window whose range will be utilized for projections", group = g_SET)
range_type      = input.string('Wick', "Range Type", options = ['Wick', 'Body'], tooltip = "Wick will consider the range from high to low, whereas Body will ignore the wicks (on the current chart timeframe)", group = g_SET)
sd_limit        = input.float(1.5, "Standard Deviation Filter", tooltip = "Ranges will be filtered out/ignored if their size exceeds this number of standard deviations (measured from a history of all prior range sizes)", group = g_SET)

var g_SD = "Standard Deviations"
use_sd_1        = input.bool(true, "SD 1", inline = "SD1", tooltip = "The difference between the range high and low will be multiplied by this factor, added to the range high for the upper projection, and subtracted from the range low for the lower projection", group = g_SD)
sd_1            = input.float(1, "", 0, inline = "SD1", group = g_SD)
use_sd_2        = input.bool(true, "SD 2", inline = "SD2", tooltip = "The difference between the range high and low will be multiplied by this factor, added to the range high for the upper projection, and subtracted from the range low for the lower projection", group = g_SD)
sd_2            = input.float(2, "", 0, inline = "SD2", group = g_SD)
use_sd_3        = input.bool(true, "SD 3", inline = "SD3", tooltip = "The difference between the range high and low will be multiplied by this factor, added to the range high for the upper projection, and subtracted from the range low for the lower projection", group = g_SD)
sd_3            = input.float(3, "", 0, inline = "SD3", group = g_SD)
use_sd_4        = input.bool(true, "SD 4", inline = "SD4", tooltip = "The difference between the range high and low will be multiplied by this factor, added to the range high for the upper projection, and subtracted from the range low for the lower projection", group = g_SD)
sd_4            = input.float(4, "", 0, inline = "SD4", group = g_SD)

var g_DRW = "Drawings"
show_data       = input.bool(true, "Data Table", tooltip = "Shows statistics on how often price exceeds X number of standard deviations from the range", group = g_DRW)
vert_lines      = input.bool(true, "Session Delineations", tooltip = "Draws vertical lines delineating the session beginning and ending times", group = g_DRW)
show_labels     = input.bool(true, "SD Labels", tooltip = "Shows the numerical value of each projection level", group = g_DRW)
horiz_lines     = input.bool(true, "SD Projection Lines", tooltip = "Extends the projections out in time", group = g_DRW)
show_exceed     = input.bool(true, "SD Projections Exceeded", tooltip = "Shows when price exceeds any of the projection levels", group = g_DRW)

var g_STY = "Style"
range_box_color  = input.color(color.new(color.blue, 70), "Range Box Color", group = g_STY)
range_line_color = input.color(color.black, "Range Delineation Color", group = g_STY)
label_color     = input.color(#ffffff00, "SD Label Color", group = g_STY)
label_text      = input.color(color.black, "SD Label Text", group = g_STY)
proj_color      = input.color(color.new(color.gray, 70), "SD Projection Box Color", group = g_STY)
proj_style      = input.string('Dotted', "SD Projection Line Style", options = ['Solid', 'Dotted', 'Dashed'], group = g_STY)
line_color      = input.color(color.black, "SD Projection Line Color", group = g_STY)
exceed_color    = input.color(color.blue, "SD Exceeded Color", group = g_STY)

var g_TABLE = "Table"
table_position      = input.string('Top Right', "Table Position", options = ['Bottom Center', 'Bottom Left', 'Bottom Right', 'Middle Center', 'Middle Left', 'Middle Right', 'Top Center', 'Top Left', 'Top Right'], group = g_TABLE)
table_size          = input.string('Auto', "Text Size", options = ['Auto', 'Tiny', 'Small', 'Normal', 'Large', 'Huge'], group = g_TABLE)
table_text          = input.color(color.black, "Text", group = g_TABLE)
table_bg            = input.color(color.white, "Table Background", group = g_TABLE)
table_frame         = input.color(color.black, "Table Frame", group = g_TABLE)
table_border        = input.color(color.black, "Table Border", group = g_TABLE)
table_border_width  = input.int(1, "Table Border Width", group = g_TABLE)
table_frame_width   = input.int(2, "Table Frame Width", group = g_TABLE)
// -------------------------------------------------- Inputs --------------------------------------------------


// -------------------------------------------------- Constants & Variables --------------------------------------------------
t = not na(time("", session, "America/New_York"))
range_wick = range_type == 'Wick'
transparent = #ffffff00
range_high = (range_wick ? high : math.max(open, close))
range_low = (range_wick ? low : math.min(open, close))

var range_size = array.new_float()

var box range_box = na

var box proj_p1 = na
var box proj_m1 = na

var box proj_p2 = na
var box proj_m2 = na

var box proj_p3 = na
var box proj_m3 = na

var box proj_p4 = na
var box proj_m4 = na
    
var int start = 0
var int sessions = 0

var bool valid_range = false

var bool passed_p1 = false
var bool passed_p2 = false
var bool passed_p3 = false
var bool passed_p4 = false

var bool passed_m1 = false
var bool passed_m2 = false
var bool passed_m3 = false
var bool passed_m4 = false

var int count_p1 = 0
var int count_p2 = 0
var int count_p3 = 0
var int count_p4 = 0

var int count_m1 = 0
var int count_m2 = 0
var int count_m3 = 0
var int count_m4 = 0

var int count_a1 = 0
var int count_a2 = 0
var int count_a3 = 0
var int count_a4 = 0

var line line_p1 = na
var line line_p2 = na
var line line_p3 = na
var line line_p4 = na

var line line_m1 = na
var line line_m2 = na
var line line_m3 = na
var line line_m4 = na
// -------------------------------------------------- Constants & Variables --------------------------------------------------


// -------------------------------------------------- Functions --------------------------------------------------
manual_stdev() =>
    mean = range_size.avg()
    accum = 0.0
    size = range_size.size()
    if size > 0
        for i = 0 to size - 1
            accum += math.pow((range_size.get(i) - mean), 2)
    sd = math.sqrt(accum / size)

get_line_style(i) =>
    result = switch i
        'Solid' => line.style_solid
        'Dashed' => line.style_dashed
        'Dotted' => line.style_dotted
proj_style := get_line_style(proj_style)

get_table_position(pos) =>
    result = switch pos
        "Bottom Center" => position.bottom_center
        "Bottom Left" => position.bottom_left
        "Bottom Right" => position.bottom_right
        "Middle Center" => position.middle_center
        "Middle Left" => position.middle_left
        "Middle Right" => position.middle_right
        "Top Center" => position.top_center
        "Top Left" => position.top_left
        "Top Right" => position.top_right

get_table_text_size(size) =>
    result = switch size
        'Auto' => size.auto
        'Tiny' => size.tiny
        'Small' => size.small
        'Normal' => size.normal
        'Large' => size.large
        'Huge' => size.huge
// -------------------------------------------------- Functions --------------------------------------------------


// -------------------------------------------------- Core Logic --------------------------------------------------
if t and not t[1]
    range_box := box.new(bar_index, range_high, bar_index, range_low, border_color = range_box_color, bgcolor = range_box_color)
    start := bar_index
    valid_range := false
    if vert_lines
        line.new(bar_index, high, bar_index, low, extend = extend.both, color = range_line_color)
else if t
    range_box.set_right(bar_index)
    if range_high > range_box.get_top()
        range_box.set_top(range_high)
    if range_low < range_box.get_bottom()
        range_box.set_bottom(range_low)
else if not t and t[1]
    top = range_box.get_top()
    bot = range_box.get_bottom()
    dif = top - bot
    range_size.push(dif)    
    range_box := na
    line_p1 := na
    if vert_lines
        line.new(bar_index - 1, high, bar_index - 1, low, extend = extend.both, color = range_line_color)

    if dif <= manual_stdev() * sd_limit
        valid_range := true
        sessions += 1
        if use_sd_1
            proj_p1 := box.new(start, top + dif * sd_1, bar_index - 1, top, border_color = proj_color, bgcolor = proj_color)
            if show_labels
                label.new(bar_index, top + dif * sd_1, str.tostring(sd_1), textcolor = label_text, color = label_color, style = label.style_label_lower_left)
            if horiz_lines
                line_p1 := line.new(bar_index - 1, top + dif * sd_1, bar_index + 1, top + dif * sd_1, color = line_color, style = proj_style)
        if use_sd_2
            proj_p2 := box.new(start, top + dif * sd_2, bar_index - 1, top + dif * sd_1, border_color = proj_color, bgcolor = proj_color)
            if show_labels
                label.new(bar_index, top + dif * sd_2, str.tostring(sd_2), textcolor = label_text, color = label_color, style = label.style_label_lower_left)
            if horiz_lines
                line_p2 := line.new(bar_index - 1, top + dif * sd_2, bar_index + 1, top + dif * sd_2, color = line_color, style = proj_style)
        if use_sd_3
            proj_p3 := box.new(start, top + dif * sd_3, bar_index - 1, top + dif * sd_2, border_color = proj_color, bgcolor = proj_color)
            if show_labels
                label.new(bar_index, top + dif * sd_3, str.tostring(sd_3), textcolor = label_text, color = label_color, style = label.style_label_lower_left)
            if horiz_lines
                line_p3 := line.new(bar_index - 1, top + dif * sd_3, bar_index + 1, top + dif * sd_3, color = line_color, style = proj_style)
        if use_sd_4
            proj_p4 := box.new(start, top + dif * sd_4, bar_index - 1, top + dif * sd_3, border_color = proj_color, bgcolor = proj_color)
            if show_labels
                label.new(bar_index, top + dif * sd_4, str.tostring(sd_4), textcolor = label_text, color = label_color, style = label.style_label_lower_left)
            if horiz_lines
                line_p4 := line.new(bar_index - 1, top + dif * sd_4, bar_index + 1, top + dif * sd_4, color = line_color, style = proj_style)
        
        if use_sd_1
            proj_m1 := box.new(start, bot, bar_index - 1, bot - dif * sd_1, border_color = proj_color, bgcolor = proj_color)
            if show_labels
                label.new(bar_index, bot - dif * sd_1, "-" + str.tostring(sd_1), textcolor = label_text, color = label_color, style = label.style_label_upper_left)
            if horiz_lines
                line_m1 := line.new(bar_index - 1, bot - dif * sd_1, bar_index + 1, bot - dif * sd_1, color = line_color, style = proj_style)
        if use_sd_2
            proj_m2 := box.new(start, bot - dif * sd_1, bar_index - 1, bot - dif * sd_2, border_color = proj_color, bgcolor = proj_color)
            if show_labels
                label.new(bar_index, bot - dif * sd_2, "-" + str.tostring(sd_2), textcolor = label_text, color = label_color, style = label.style_label_upper_left)
            if horiz_lines
                line_m2 := line.new(bar_index - 1, bot - dif * sd_2, bar_index + 1, bot - dif * sd_2, color = line_color, style = proj_style)
        if use_sd_3
            proj_m3 := box.new(start, bot - dif * sd_2, bar_index - 1, bot - dif * sd_3, border_color = proj_color, bgcolor = proj_color)
            if show_labels
                label.new(bar_index, bot - dif * sd_3, "-" + str.tostring(sd_3), textcolor = label_text, color = label_color, style = label.style_label_upper_left)
            if horiz_lines
                line_m3 := line.new(bar_index - 1, bot - dif * sd_3, bar_index + 1, bot - dif * sd_3, color = line_color, style = proj_style)
        if use_sd_4
            proj_m4 := box.new(start, bot - dif * sd_3, bar_index - 1, bot - dif * sd_4, border_color = proj_color, bgcolor = proj_color)
            if show_labels
                label.new(bar_index, bot - dif * sd_4, "-" + str.tostring(sd_4), textcolor = label_text, color = label_color, style = label.style_label_upper_left)
            if horiz_lines
                line_m4 := line.new(bar_index - 1, bot - dif * sd_4, bar_index + 1, bot - dif * sd_4, color = line_color, style = proj_style)
else if not t    
    if horiz_lines and not na(line_p1)
        line_p1.set_x2(bar_index + 1)
        line_p2.set_x2(bar_index + 1)
        line_p3.set_x2(bar_index + 1)
        line_p4.set_x2(bar_index + 1)
        
        line_m1.set_x2(bar_index + 1)
        line_m2.set_x2(bar_index + 1)
        line_m3.set_x2(bar_index + 1)
        line_m4.set_x2(bar_index + 1)

if not t and valid_range
    if high > proj_p1.get_top() and not passed_p1
        passed_p1 := true
        if show_exceed
            label.new(bar_index, proj_p1.get_top(), str.tostring(sd_1), style = label.style_label_down, color = exceed_color, textcolor = label_text)
    if high > proj_p2.get_top() and not passed_p2
        passed_p2 := true
        if show_exceed
            label.new(bar_index, proj_p2.get_top(), str.tostring(sd_2), style = label.style_label_down, color = exceed_color, textcolor = label_text)
    if high > proj_p3.get_top() and not passed_p3
        passed_p3 := true
        if show_exceed
            label.new(bar_index, proj_p3.get_top(), str.tostring(sd_3), style = label.style_label_down, color = exceed_color, textcolor = label_text)
    if high > proj_p4.get_top() and not passed_p4
        passed_p4 := true
        if show_exceed
            label.new(bar_index, proj_p4.get_top(), str.tostring(sd_4), style = label.style_label_down, color = exceed_color, textcolor = label_text)

    if low < proj_m1.get_bottom() and not passed_m1
        passed_m1 := true
        if show_exceed
            label.new(bar_index, proj_m1.get_bottom(), "-" + str.tostring(sd_1), style = label.style_label_up, color = exceed_color, textcolor = label_text)
    if low < proj_m2.get_bottom() and not passed_m2
        passed_m2 := true
        if show_exceed
            label.new(bar_index, proj_m2.get_bottom(), "-" + str.tostring(sd_2), style = label.style_label_up, color = exceed_color, textcolor = label_text)
    if low < proj_m3.get_bottom() and not passed_m3
        passed_m3 := true
        if show_exceed
            label.new(bar_index, proj_m3.get_bottom(), "-" + str.tostring(sd_3), style = label.style_label_up, color = exceed_color, textcolor = label_text)
    if low < proj_m4.get_bottom() and not passed_m4
        passed_m4 := true
        if show_exceed
            label.new(bar_index, proj_m4.get_bottom(), "-" + str.tostring(sd_4), style = label.style_label_up, color = exceed_color, textcolor = label_text)

else if t and not t[1]
    if passed_p1 or passed_m1
        count_a1 += 1
    if passed_p2 or passed_m2
        count_a2 += 1
    if passed_p3 or passed_m3
        count_a3 += 1
    if passed_p4 or passed_m4
        count_a4 += 1

    passed_p1 := false
    passed_p2 := false
    passed_p3 := false
    passed_p4 := false
    
    passed_m1 := false
    passed_m2 := false
    passed_m3 := false
    passed_m4 := false
// -------------------------------------------------- Core Logic --------------------------------------------------


// -------------------------------------------------- Data Table --------------------------------------------------
if show_data
    var stats = table.new(get_table_position(table_position), 10, 10, bgcolor = table_bg, frame_color = table_frame, frame_width = table_frame_width, border_color = table_border, border_width = table_border_width)

    if barstate.islast
        ts = get_table_text_size(table_size)

        table.cell(stats, 0, 1, "Valid Sessions", text_size = ts)
        table.cell(stats, 1, 1, str.tostring(sessions), text_size = ts)
        table.cell(stats, 2, 1, "-", text_size = ts)
        
        if use_sd_1
            table.cell(stats, 0, 2, "Exceeded " + str.tostring(sd_1) + " SD", text_size = ts)
            table.cell(stats, 1, 2, str.tostring(count_a1), text_size = ts)
            table.cell(stats, 2, 2, str.tostring(math.round(count_a1 / sessions * 1000) / 10) + "%", text_size = ts)
        
        if use_sd_2
            table.cell(stats, 0, 3, "Exceeded " + str.tostring(sd_2) + " SD", text_size = ts)
            table.cell(stats, 1, 3, str.tostring(count_a2), text_size = ts)
            table.cell(stats, 2, 3, str.tostring(math.round(count_a2 / sessions * 1000) / 10) + "%", text_size = ts)
        
        if use_sd_3
            table.cell(stats, 0, 4, "Exceeded " + str.tostring(sd_3) + " SD", text_size = ts)
            table.cell(stats, 1, 4, str.tostring(count_a3), text_size = ts)
            table.cell(stats, 2, 4, str.tostring(math.round(count_a3 / sessions * 1000) / 10) + "%", text_size = ts)
        
        if use_sd_4
            table.cell(stats, 0, 5, "Exceeded " + str.tostring(sd_4) + " SD", text_size = ts)
            table.cell(stats, 1, 5, str.tostring(count_a4), text_size = ts)
            table.cell(stats, 2, 5, str.tostring(math.round(count_a4 / sessions * 1000) / 10) + "%", text_size = ts)
// -------------------------------------------------- Data Table --------------------------------------------------