Seasonality [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("Seasonality [TFO]", "Seasonality v1.0 [TFO]", false, max_lines_count = 500, max_boxes_count = 500, max_labels_count = 500)


// -------------------- Inputs --------------------
var g_ANALYSIS = "Analysis"
show_table          = input.bool(true, "Statistics Table", tooltip = "Summarized table describing seasonal data for the selected timeframe", group = g_ANALYSIS)
show_analysis       = input.bool(true, "Performance Analysis", inline = "ANALYSIS", tooltip = "Histogram showing the average percentage performance for the selected timeframe", group = g_ANALYSIS)
analysis_period     = input.string('Daily', "", ['Daily', 'Monthly', 'Quarterly'], inline = "ANALYSIS", group = g_ANALYSIS)
szn_pivots          = input.bool(false, "Seasonal Pivots", tooltip = "Shows when the current asset tends to make highs and lows throughout the year, based on an aggregation of daily performance", group = g_ANALYSIS)
pivot_strength      = input.int(10, "Pivot Strength", tooltip = "Using a value of 10 for example, a high must be greater than the 10 bars to the left and 10 bars to the right of it in order to be a valid pivot, and vice versa for lows", group = g_ANALYSIS)

var g_EXTRAS = "Extras"
highlight_today     = input.bool(true, "Highlight Current Trading Day", tooltip = "Easily find where the current trading day meets up with seasonal averages", group = g_EXTRAS)
show_labels         = input.bool(true, "Performance Labels", tooltip = "Labels will be shown in the Performance Analysis to describe the average percent move over the specified period of time", group = g_EXTRAS)
track_change        = input.bool(false, "Track Changes", tooltip = "Follows the changes in average performance throughout the year", group = g_EXTRAS)

var g_INDEX = "Indexing"
anchor_boy          = input.bool(true, "Anchor to Beginning of Year", tooltip = "All drawings will start anchored to the first trading day of the current year, to easily overlay past performance on the current Daily chart", group = g_INDEX)
use_manual_offset   = input.bool(false, "Manual Offset", inline = "OFFSET", tooltip = "Offers a secondary offset so users can overlay these drawings on previous years (with an offset of ~252 * number of years, for example)", group = g_INDEX)
manual_offset       = input.int(252, "", inline = "OFFSET", group = g_INDEX)

var g_DIV = "Dividers"
month_div           = input.bool(false, "Month", inline = "MDIV", group = g_DIV)
mdiv_style          = input.string('Dotted', "", ['Dotted', 'Dashed', 'Solid'], inline = "MDIV", group = g_DIV)
mdiv_color          = input.color(color.white, "", inline = "MDIV", group = g_DIV)
qr_div              = input.bool(false, "Quarter", inline = "QDIV", group = g_DIV)
qdiv_style          = input.string('Dotted', "", ['Dotted', 'Dashed', 'Solid'], inline = "QDIV", group = g_DIV)
qdiv_color          = input.color(color.white, "", inline = "QDIV", group = g_DIV)

var g_STYLE = "Style"
main_color          = input.color(color.white, "Main Color", group = g_STYLE)
track_color         = input.color(color.yellow, "Track Changes", group = g_STYLE)
upper_color         = input.color(#08998180, "Performance", inline = "PERFORMANCE", group = g_STYLE)
lower_color         = input.color(#f2364580, "", inline = "PERFORMANCE", group = g_STYLE)
high_color          = input.color(#08998180, "Seasonal Pivots", inline = "PIVOTS", group = g_STYLE)
low_color           = input.color(#f2364580, "", inline = "PIVOTS", group = g_STYLE)
pivot_width         = input.int(2, "Seasonal Pivot Width", group = g_STYLE)
divider_width       = input.int(1, "Divider Width", group = g_STYLE)
text_size           = input.string('Normal', "Text Size", options = ['Auto', 'Tiny', 'Small', 'Normal', 'Large', 'Huge'], group = g_STYLE)

var g_TABLE = "Table"
table_pos           = input.string('Top Right', "Position", options = ['Bottom Center', 'Bottom Left', 'Bottom Right', 'Middle Center', 'Middle Left', 'Middle Right', 'Top Center', 'Top Left', 'Top Right'], group = g_TABLE)
table_text          = input.color(color.black, "Text Color", group = g_TABLE)
table_frame         = input.color(color.black, "Frame Color", group = g_TABLE)
table_border        = input.color(color.black, "Border Color", group = g_TABLE)
table_bg            = input.color(color.white, "Background Color", group = g_TABLE)
// -------------------- Inputs --------------------


// -------------------- Basic Functions --------------------
get_table_pos(i) =>
    result = switch i
        "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
    result

get_text_size(i) =>
    result = switch i
        'Tiny' => size.tiny
        'Small' => size.small
        'Normal' => size.normal
        'Large' => size.large
        'Huge' => size.huge
        'Auto' => size.auto
    result

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

get_month(i) =>
    result = switch i
        1 => 'JAN'
        2 => 'FEB'
        3 => 'MAR'
        4 => 'APR'
        5 => 'MAY'
        6 => 'JUN'
        7 => 'JUL'
        8 => 'AUG'
        9 => 'SEP'
        10 => 'OCT'
        11 => 'NOV'
        12 => 'DEC'
        
get_qr(i) =>
    result = switch i
        1 => 'Q1'
        2 => 'Q2'
        3 => 'Q3'
        4 => 'Q4'
        
get_period() =>
    result = switch analysis_period
        'Daily' => 'D'
        'Monthly' => 'M'
        'Quarterly' => 'Q'
// -------------------- Basic Functions --------------------


// -------------------- Variables & Constants --------------------
[d_open, d_close] = request.security(syminfo.tickerid, "D", [open, close], barmerge.gaps_off, barmerge.lookahead_on)
[m_open, m_close] = request.security(syminfo.tickerid, "M", [open, close], barmerge.gaps_off, barmerge.lookahead_on)
[q_open, q_close] = request.security(syminfo.tickerid, "3M", [open, close], barmerge.gaps_off, barmerge.lookahead_on)

//

var arr_days = array.new_int()
var d_pct = array.new_float()
var d_points = array.new_float()
var d_count = array.new_int()
var d_day = array.new_string()

var d_sorted_pct = array.new_float()
var d_sorted_points = array.new_float()
var d_sorted_count = array.new_int()
var d_sorted_day = array.new_string()

var m_pct_avg = array.new_float()
var q_pct_avg = array.new_float()

//

var int doy = na
var int first_year = na
var int first_xoy = na
var float first_yoy = na

//

if month == 1 and month[1] != 1
    first_xoy := bar_index
    first_yoy := close[1]
    doy := 0
    if na(first_year)
        first_year := year

if not na(doy)
    doy += 1

//

offset = use_manual_offset ? manual_offset : 0
start_idx = anchor_boy ? first_xoy - offset : bar_index

highlight_offset = 20
days_in_year = 252
days_in_month = 21
days_in_qr = 63

period = get_period()
text_size := get_text_size(text_size)
mdiv_style := get_line_style(mdiv_style)
qdiv_style := get_line_style(qdiv_style)
table_pos := get_table_pos(table_pos)

//

szn_idx = switch period
    'D' => doy - 1
    'M' => month - 1
    'Q' => month % 3 - 1
    
szn_arr = switch period
    'D' => d_sorted_pct
    'M' => m_pct_avg
    'Q' => q_pct_avg
    
current_pct = switch period
    'D' => (d_close - d_open) / d_open
    'M' => (m_close - m_open) / m_open
    'Q' => (q_close - q_open) / q_open
    
current_pts = switch period
    'D' => (d_close - d_open)
    'M' => (m_close - m_open)
    'Q' => (q_close - q_open)

num_days = switch period
    'D' => days_in_year
    'M' => days_in_month
    'Q' => days_in_qr
// -------------------- Variables & Constants --------------------


// -------------------- Saving Performance Data --------------------
if not na(doy) and doy <= days_in_year
    idx = arr_days.indexof(doy)
    pct = (d_close - d_open) / d_open
    points = (d_close - d_open)

    if idx == -1
        arr_days.push(doy)
        d_pct.push(pct)
        d_points.push(points)
        d_count.push(1)
        d_day.push(str.tostring(month) + "/" + str.tostring(dayofmonth))
    else
        d_pct.set(idx, d_pct.get(idx) + pct)
        d_points.set(idx, d_points.get(idx) + points)
        d_count.set(idx, d_count.get(idx) + 1)
// -------------------- Saving Performance Data --------------------


// -------------------- Plotting Functions --------------------
dividers(base) =>
    if month_div
        for i = 0 to m_pct_avg.size()
            left = start_idx + i * days_in_month
            line.new(left, base, left, base, color = mdiv_color, style = mdiv_style, extend = extend.both, width = divider_width)

    if qr_div
        for i = 0 to q_pct_avg.size()
            left = start_idx + i * days_in_qr
            line.new(left, base, left, base, color = qdiv_color, style = qdiv_style, extend = extend.both, width = divider_width)
    
plot_analysis(period, szn_arr, szn_idx, num_days, base) =>
    for i = 0 to szn_arr.size() - 1
        not_daily = period != 'D'
        left = start_idx + i * (not_daily ? num_days : 1)
        right = left + (not_daily ?  num_days - 1 : 1)
        
        label_x = math.floor(math.avg(left, right))
        box_text = switch period
            'D' => na
            'M' => get_month(i + 1)
            'Q' => get_qr(i + 1)
            
        perf = szn_arr.get(i) 
        col = perf > 0 ? upper_color : lower_color
        if not_daily
            box.new(left, base + perf, right, base, bgcolor = col, border_color = col, text = box_text, text_color = col)
        else
            line.new(left, base + perf, left, base, color = col, width = 5)
            
        if show_labels
            label.new(label_x, base + perf, str.tostring(perf * 100, format.percent), style = perf > 0 ? label.style_label_down : label.style_label_up, textcolor = main_color, size = text_size, color = #ffffff00)
        
        if track_change and i != szn_arr.size() - 1
            next_perf = szn_arr.get(i + 1)
            line.new(label_x, base + perf, label_x + (not_daily ? num_days : 1), base + next_perf, color = track_color, width = 2)
            
        if highlight_today and i == szn_idx
            line.new(left, base + perf, start_idx + days_in_year - 1, base + perf, style = line.style_dotted, color = main_color, width = 2)
            label.new(start_idx + days_in_year - 1 + highlight_offset, base + perf, "Current Avg: " + str.tostring(perf * 100, format.percent), style = label.style_label_center, textcolor = main_color, size = text_size, color = #ffffff00)
    
        if i == szn_arr.size() - 1
            line.new(start_idx, base, start_idx + days_in_year - 1, base, color = main_color, width = 2)
// -------------------- Plotting Functions --------------------


// -------------------- Statistics Table --------------------
var table stats = table.new(table_pos, 50, 50, bgcolor = table_bg, frame_color = table_frame, border_color = table_border, frame_width = 1, border_width = 1)

if barstate.islast
    if timeframe.in_seconds() != timeframe.in_seconds("D")
        table.cell(stats, 0, 0, "Please Use the 'D' Timeframe")
    else
        // Sort daily arrays by day --------------------------------------------------
        temp_arr_days = arr_days.copy()
        for i = temp_arr_days.size() - 1 to 0
            min = arr_days.indexof(temp_arr_days.min())
            
            d_sorted_pct.push(d_pct.get(min))
            d_sorted_points.push(d_points.get(min))
            d_sorted_count.push(d_count.get(min))
            d_sorted_day.push(d_day.get(min))

            temp_arr_days.set(min, na)

        for i = 0 to d_sorted_pct.size() - 1
            d_sorted_pct.set(i, d_sorted_pct.get(i) / d_sorted_count.get(i))
            d_sorted_points.set(i, d_sorted_points.get(i) / d_sorted_count.get(i))
        // Sort daily arrays by day --------------------------------------------------

        
        // Get month performance --------------------------------------------------
        m_pct_avg.clear()
        for i = 0 to 11
            sum = 0.0
            for j = 0 to days_in_month - 1
                sum += d_sorted_pct.get(i * days_in_month + j)
            m_pct_avg.push(sum)
            
        q_pct_avg.clear()
        for i = 0 to 3
            sum = 0.0
            for j = 0 to days_in_qr - 1
                sum += d_sorted_pct.get(i * days_in_qr + j)
            q_pct_avg.push(sum)
        // Get month performance --------------------------------------------------


        // Seasonal highs and lows --------------------------------------------------
        szn_pct = szn_arr.get(szn_idx)
        base = 1.0

        var price_projection = array.new_float()
        var szn_lows = array.new_string()
        var szn_highs = array.new_string()

        if szn_pivots
            x1 = start_idx
            y1 = base

            x2 = x1 + 1
            for i = 0 to d_pct.size() - 1
                price_projection.push(y1)
                y2 = y1 * (1 + d_sorted_pct.get(i))
                y1 := y2
                x1 += 1
                x2 += 1

            left_bound = pivot_strength
            right_bound = price_projection.size() - pivot_strength - 1
            for i = left_bound to right_bound
                new_low = true
                new_high = true
                for j = -pivot_strength to pivot_strength
                    if i != j
                        if price_projection.get(i) > price_projection.get(i + j)
                            new_low := false
                            break
                for j = -pivot_strength to pivot_strength
                    if i != j
                        if price_projection.get(i) < price_projection.get(i + j)
                            new_high := false
                            break
                if new_low
                    new_low_month = get_month(math.ceil(i / days_in_month))
                    if szn_lows.indexof(new_low_month) == -1
                        szn_lows.push(new_low_month)
                    line.new(start_idx + i, base, start_idx + i, base, extend = extend.both, color = low_color, width = pivot_width)
                    label.new(start_idx + i, math.min(base, base + szn_arr.min()), "SEASONAL\nLOW", color = color.new(low_color, 0), textcolor = main_color, size = text_size, style = label.style_label_up)
                if new_high
                    new_high_month = get_month(math.ceil(i / days_in_month))
                    if szn_highs.indexof(new_high_month) == -1
                        szn_highs.push(new_high_month)
                    line.new(start_idx + i, base, start_idx + i, base, extend = extend.both, color = high_color, width = pivot_width)
                    label.new(start_idx + i, math.max(base, base + szn_arr.max()), "SEASONAL\nHIGH", color = color.new(high_color, 0), textcolor = main_color, size = text_size, style = label.style_label_down)
        // Seasonal highs and lows --------------------------------------------------
        

        // Plotting analysis --------------------------------------------------
        dividers(base)
        if show_analysis
            plot_analysis(period, szn_arr, szn_idx, num_days, base)
        // Plotting analysis --------------------------------------------------

        
        // Table --------------------------------------------------
        if show_table
            curr_month_pct = (m_close - m_open) / m_open
            curr_month_points = (m_close - m_open)

            table.cell(stats, 0, 0, "Timeframe: ", text_size = text_size, text_color = table_text)
            table.cell(stats, 1, 0, str.tostring(period), text_size = text_size, text_color = table_text)

            table.cell(stats, 0, 1, "Collected Over: ", text_size = text_size, text_color = table_text)
            table.cell(stats, 1, 1, str.tostring(year - first_year) + " Years", text_size = text_size, text_color = table_text)

            table.cell(stats, 0, 2, "Seasonality: ", text_size = text_size, text_color = table_text)
            table.cell(stats, 1, 2, str.tostring(szn_pct > 0 ? 'BULLISH' : 'BEARISH'), bgcolor = szn_pct > 0 ? upper_color : lower_color, text_size = text_size, text_color = table_text)

            table.cell(stats, 0, 3, "Average % Change: ", text_size = text_size, text_color = table_text)
            table.cell(stats, 1, 3, str.tostring(math.round(szn_pct * 10000) / 100, format.percent), bgcolor = szn_pct > 0 ? upper_color : lower_color, text_size = text_size, text_color = table_text)

            table.cell(stats, 0, 4, "Current % Change: ", text_size = text_size, text_color = table_text)
            table.cell(stats, 1, 4, str.tostring(math.round(current_pct * 10000) / 100, format.percent), bgcolor = current_pct > 0 ? upper_color : lower_color, text_size = text_size, text_color = table_text)

            table.cell(stats, 0, 5, "Current Point Change: ", text_size = text_size, text_color = table_text)
            table.cell(stats, 1, 5, str.tostring(current_pts), bgcolor = current_pts > 0 ? upper_color : lower_color, text_size = text_size, text_color = table_text)

            if szn_pivots
                table.cell(stats, 0, 6, "Major Lows Made in: ", text_size = text_size, text_color = table_text)
                table.cell(stats, 1, 6, str.tostring(szn_lows), text_size = text_size, text_color = table_text)

                table.cell(stats, 0, 7, "Major Highs Made in: ", text_size = text_size, text_color = table_text)
                table.cell(stats, 1, 7, str.tostring(szn_highs), text_size = text_size, text_color = table_text)
        // Table --------------------------------------------------
// -------------------- Statistics Table --------------------