Indicator · Pine Script v5 · On the chart

NQ Hourly Retracement Levels

by TamaraKGreen· code credited to npg

NQ Hourly Retracement Levels highlights NY, London, and Asia trading sessions directly on the price chart.

View on GitHub

GitHub stars46on the repository
Pine versionv5runs as is
Lines of code947
Settings19inputs
LicenceNone statedfile: MPL-2.0
Last update7 weeks ago4 Sep 2026

Add It to Your TradingView Chart

  1. 1Copy the scriptPress Copy script above, or the copy button on the code below.
  2. 2Open the Pine EditorOpen a TradingView chart and choose Pine Editor at the bottom of the screen.
  3. 3Paste over the templateCreate a new indicator, select all of the template text and paste. Press Save and give it a name.
  4. 4Add it to the chartPress Add to chart, then adjust the settings with the gear icon next to its name.

Source Code

947 lines of Pine Script v5 from NQ Hourly Retracement Levels.txt, published under MPL-2.0 (file header). Keep the author's credit and licence lines when you reuse it.

NQ Hourly Retracement Levels.txt
// This Pine Script® code is subject to the terms of the Mozilla Public License 2.0 at https://mozilla.org/MPL/2.0/
// © npg

//@version=5
indicator("NQ Hourly Retracement Levels", "Hourly Levels", true, max_lines_count=500, max_labels_count=500)

// ---------------------------------------------- Inputs --------------------------------------------------

var g_SESS = "========= Trading Sessions =========="
show_sesa = input(true, 'Show NY Session', inline='sesa', group=g_SESS)
sesa_css = input.color(color.rgb(255, 0, 55), '', inline='sesa', group=g_SESS)

show_sesb = input(true, 'Show London Session', inline='sesb', group=g_SESS)
sesb_css = input.color(color.rgb(33, 87, 243), '', inline='sesb', group=g_SESS)

show_sesc = input(true, 'Show Asia Session', inline='sesc', group=g_SESS)
sesc_css = input.color(color.rgb(255, 152, 0), '', inline='sesc', group=g_SESS)

bg_transp = input.float(90, 'Range Area Transparency', group=g_SESS)
show_outline = input(true, 'Range Outline', group=g_SESS)

var g_LEVELS = "========= Hourly Levels =========="
show_hourly_levels = input(true, "Show Hourly Levels at NY Open", group=g_LEVELS)
show_opens = input(true, "Show Hourly Opens", group=g_LEVELS)
show_midpoints = input(true, "Show Hourly Midpoints", group=g_LEVELS)
min_hit_rate = input.float(75.0, "Minimum Hit Rate % to Display", minval=0, maxval=100, step=5, group=g_LEVELS)
line_width = input.int(1, "Line Width", minval=1, maxval=5, group=g_LEVELS)
line_style_input = input.string("Dashed", "Retracement Line Style", options=["Solid", "Dashed", "Dotted"], group=g_LEVELS)
label_size_input = input.string("small", "Label Size", options=["tiny", "small", "normal", "large"], group=g_LEVELS)
remove_on_hit = input(true, "Remove Level When Hit", group=g_LEVELS)

var g_TABLES = "========= Table Settings ========="
show_stats_table = input(true, "Show Statistics Table", group=g_TABLES)
stats_table_pos = input.string("Bottom Right", "Stats Table Position", 
     options=["Bottom Center", "Bottom Left", "Bottom Right", "Middle Center", 
              "Middle Left", "Middle Right", "Top Center", "Top Left", "Top Right"], group=g_TABLES)
stats_table_size = input.string("Small", "Stats Table Size", 
     options=["Auto", "Tiny", "Small", "Normal", "Large", "Huge"], group=g_TABLES)

// ---------------------------------------------- Helper Functions --------------------------------------------------

get_line_style(style_string) =>
    switch style_string
        "Solid" => line.style_solid
        "Dashed" => line.style_dashed
        "Dotted" => line.style_dotted
        => line.style_solid

get_label_size(size_string) =>
    switch size_string
        "tiny" => size.tiny
        "small" => size.small
        "normal" => size.normal
        "large" => size.large
        => size.small

get_table_position(pos) =>
    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) =>
    switch size
        'Auto' => size.auto
        'Tiny' => size.tiny
        'Small' => size.small
        'Normal' => size.normal
        'Large' => size.large
        'Huge' => size.huge

// Get color based on MAE percentage
get_color_for_mae(mae_pct) =>
    color result = color.gray
    
    if mae_pct <= 0.25
        result := color.rgb(33, 150, 243)      // Blue
    else if mae_pct <= 0.30
        result := color.rgb(156, 39, 176)      // Purple
    else if mae_pct <= 0.35
        result := color.rgb(255, 87, 34)       // Deep Orange
    else if mae_pct <= 0.40
        result := color.rgb(0, 150, 136)       // Teal
    else if mae_pct <= 0.45
        result := color.rgb(255, 152, 0)       // Orange
    else if mae_pct <= 0.50
        result := color.rgb(233, 30, 99)       // Pink
    else
        result := color.rgb(96, 125, 139)      // Blue Grey
    
    result

// Get color for the live distance cell
// Green  = price is within MAE (level very much in play)
// Yellow = price is within 2× MAE (approaching, worth watching)
// Orange = price is within 3× MAE (possible but stretched)
// Gray   = price is beyond 3× MAE (unlikely / far away)
get_distance_color(dist_pct, mae_pct) =>
    color result = color.rgb(180, 180, 180)  // default gray
    if dist_pct <= mae_pct
        result := color.rgb(76, 175, 80)     // Green  – within MAE
    else if dist_pct <= mae_pct * 2
        result := color.rgb(255, 235, 59)    // Yellow – within 2× MAE
    else if dist_pct <= mae_pct * 3
        result := color.rgb(255, 152, 0)     // Orange – within 3× MAE
    result

get_distance_text_color(dist_pct, mae_pct) =>
    color result = color.rgb(64, 64, 64)
    if dist_pct <= mae_pct
        result := color.white
    else if dist_pct <= mae_pct * 2
        result := color.rgb(64, 64, 64)
    else if dist_pct <= mae_pct * 3
        result := color.rgb(64, 64, 64)
    result

// ---------------------------------------------- Session Detection --------------------------------------------------

tf = timeframe.period
var tz = "America/New_York"
is_sesa = math.sign(nz(time(tf, "0800-1600", tz)))
is_sesb = math.sign(nz(time(tf, "0200-0800", tz)))
is_sesc = math.sign(nz(time(tf, "2000-0200", tz)))

// Hourly tracking
is_6pm = math.sign(nz(time(tf, "1800-1900", tz)))
is_7pm = math.sign(nz(time(tf, "1900-2000", tz)))
is_8pm = math.sign(nz(time(tf, "2000-2100", tz)))
is_9pm = math.sign(nz(time(tf, "2100-2200", tz)))
is_10pm = math.sign(nz(time(tf, "2200-2300", tz)))
is_11pm = math.sign(nz(time(tf, "2300-0000", tz)))
is_12am = math.sign(nz(time(tf, "0000-0100", tz)))
is_1am = math.sign(nz(time(tf, "0100-0200", tz)))
is_2am = math.sign(nz(time(tf, "0200-0300", tz)))
is_3am = math.sign(nz(time(tf, "0300-0400", tz)))
is_4am = math.sign(nz(time(tf, "0400-0500", tz)))
is_5am = math.sign(nz(time(tf, "0500-0600", tz)))
is_6am = math.sign(nz(time(tf, "0600-0700", tz)))
is_7am = math.sign(nz(time(tf, "0700-0800", tz)))

// ---------------------------------------------- Session Ranges --------------------------------------------------

get_range(session, session_css)=>
    var max = high
    var min = low
    var sess_open = open
    var box bx = na
    
    if session > session[1]
        max := high
        min := low
        sess_open := open
        bx := box.new(bar_index, max, bar_index, min, 
             bgcolor=color.new(session_css, bg_transp), 
             border_color=show_outline ? session_css : na, 
             border_style=line.style_dotted)
    
    if session and session == session[1]
        max := math.max(high, max)
        min := math.min(low, min)
        box.set_top(bx, max)
        box.set_rightbottom(bx, bar_index, min)
    
    [session ? na : max, session ? na : min, sess_open]

// Session variables
var float max_sesa = na
var float min_sesa = na
var float open_sesa = na

var float max_sesb = na
var float min_sesb = na
var float open_sesb = na

var float max_sesc = na
var float min_sesc = na
var float open_sesc = na

// Track hourly opens and ranges
track_hourly(is_hour, prev_is_hour) =>
    var float hour_open = na
    var float hour_max = na
    var float hour_min = na
    
    if is_hour and is_hour > prev_is_hour
        hour_open := open
        hour_max := high
        hour_min := low
    else if is_hour
        hour_max := math.max(high, hour_max)
        hour_min := math.min(low, hour_min)
    
    [hour_open, hour_max, hour_min]

// Call track_hourly for each hour
[open_6pm, max_6pm, min_6pm] = track_hourly(is_6pm, is_6pm[1])
[open_7pm, max_7pm, min_7pm] = track_hourly(is_7pm, is_7pm[1])
[open_8pm, max_8pm, min_8pm] = track_hourly(is_8pm, is_8pm[1])
[open_9pm, max_9pm, min_9pm] = track_hourly(is_9pm, is_9pm[1])
[open_10pm, max_10pm, min_10pm] = track_hourly(is_10pm, is_10pm[1])
[open_11pm, max_11pm, min_11pm] = track_hourly(is_11pm, is_11pm[1])
[open_12am, max_12am, min_12am] = track_hourly(is_12am, is_12am[1])
[open_1am, max_1am, min_1am] = track_hourly(is_1am, is_1am[1])
[open_2am, max_2am, min_2am] = track_hourly(is_2am, is_2am[1])
[open_3am, max_3am, min_3am] = track_hourly(is_3am, is_3am[1])
[open_4am, max_4am, min_4am] = track_hourly(is_4am, is_4am[1])
[open_5am, max_5am, min_5am] = track_hourly(is_5am, is_5am[1])
[open_6am, max_6am, min_6am] = track_hourly(is_6am, is_6am[1])
[open_7am, max_7am, min_7am] = track_hourly(is_7am, is_7am[1])

// Track which levels have been hit during NY session
var bool hit_7to8_mid = false
var bool hit_7am_open = false
var bool hit_6to7_mid = false
var bool hit_6am_open = false
var bool hit_5to6_mid = false
var bool hit_5am_open = false
var bool hit_4to5_mid = false
var bool hit_4am_open = false
var bool hit_3to4_mid = false
var bool hit_3am_open = false
var bool hit_2to3_mid = false
var bool hit_2am_open = false
var bool hit_1to2_mid = false
var bool hit_1am_open = false
var bool hit_12to1_mid = false
var bool hit_12am_open = false

// Session ranges
if show_sesa
    [max, min, sess_open] = get_range(is_sesa, sesa_css)
    max_sesa := max
    min_sesa := min
    open_sesa := sess_open

if show_sesb
    [max, min, sess_open] = get_range(is_sesb, sesb_css)
    max_sesb := max
    min_sesb := min
    open_sesb := sess_open

if show_sesc
    [max, min, sess_open] = get_range(is_sesc, sesc_css)
    max_sesc := max
    min_sesc := min
    open_sesc := sess_open

// ---------------------------------------------- Pattern Detection --------------------------------------------------

get_session_pattern() =>
    var string pattern = "unknown"
    if not na(max_sesb) and not na(min_sesb) and not na(max_sesc) and not na(min_sesc)
        if max_sesb > max_sesc and min_sesb < min_sesc
            pattern := "london_engulfs_asia"
        else if max_sesc >= max_sesb and min_sesc <= min_sesb
            pattern := "asia_engulfs_london"
        else if max_sesb > max_sesc and min_sesb >= min_sesc
            pattern := "london_partial_up"
        else if max_sesb <= max_sesc and min_sesb < min_sesc
            pattern := "london_partial_down"
        else
            pattern := "none"
    pattern

var string current_pattern = "unknown"

if not na(max_sesb) and not na(min_sesb) and not na(max_sesc) and not na(min_sesc)
    current_pattern := get_session_pattern()

// ---------------------------------------------- Hit Rate Data --------------------------------------------------

get_overall_hit_rate(level_name) =>
    rate = 0.0
    rate := switch level_name
        "07:00am_midpoint" => 94.19
        "07:00am_open"     => 88.46
        "06:00am_midpoint" => 87.25
        "06:00am_open"     => 84.42
        "05:00am_midpoint" => 84.35
        "05:00am_open"     => 82.71
        "04:00am_midpoint" => 81.73
        "04:00am_open"     => 79.58
        "03:00am_midpoint" => 78.85
        "03:00am_open"     => 77.14
        "02:00am_midpoint" => 76.68
        "02:00am_open"     => 75.65
        "01:00am_midpoint" => 75.21
        "01:00am_open"     => 74.52
        "12:00am_midpoint" => 74.09
        "12:00am_open"     => 73.67
        => 0.0
    rate

get_pattern_hit_rate(level_name, pattern) =>
    rate = 0.0
    if pattern == "london_engulfs_asia"
        rate := switch level_name
            "07:00am_midpoint" => 94.14
            "07:00am_open"     => 87.31
            "06:00am_midpoint" => 86.50
            "06:00am_open"     => 83.94
            "05:00am_midpoint" => 83.27
            "05:00am_open"     => 80.95
            "04:00am_midpoint" => 78.27
            "04:00am_open"     => 75.81
            "03:00am_midpoint" => 75.30
            "03:00am_open"     => 76.25
            "02:00am_midpoint" => 75.71
            "02:00am_open"     => 78.38
            "01:00am_midpoint" => 78.68
            "01:00am_open"     => 79.09
            "12:00am_midpoint" => 79.08
            "12:00am_open"     => 79.53
            => 0.0
    else if pattern == "asia_engulfs_london"
        rate := switch level_name
            "07:00am_midpoint" => 97.52
            "07:00am_open"     => 91.54
            "06:00am_midpoint" => 92.08
            "06:00am_open"     => 90.55
            "05:00am_midpoint" => 90.10
            "05:00am_open"     => 91.50
            "04:00am_midpoint" => 89.60
            "04:00am_open"     => 88.12
            "03:00am_midpoint" => 87.62
            "03:00am_open"     => 89.05
            "02:00am_midpoint" => 90.10
            "02:00am_open"     => 89.05
            "01:00am_midpoint" => 87.13
            "01:00am_open"     => 84.65
            "12:00am_midpoint" => 86.14
            "12:00am_open"     => 85.57
            => 0.0
    else if pattern == "london_partial_up"
        rate := switch level_name
            "07:00am_midpoint" => 94.31
            "07:00am_open"     => 89.01
            "06:00am_midpoint" => 87.82
            "06:00am_open"     => 85.16
            "05:00am_midpoint" => 85.10
            "05:00am_open"     => 83.17
            "04:00am_midpoint" => 82.45
            "04:00am_open"     => 79.97
            "03:00am_midpoint" => 78.45
            "03:00am_open"     => 76.58
            "02:00am_midpoint" => 75.32
            "02:00am_open"     => 73.11
            "01:00am_midpoint" => 71.71
            "01:00am_open"     => 70.86
            "12:00am_midpoint" => 69.95
            "12:00am_open"     => 69.46
            => 0.0
    else if pattern == "london_partial_down"
        rate := switch level_name
            "07:00am_midpoint" => 93.40
            "07:00am_open"     => 87.95
            "06:00am_midpoint" => 86.05
            "06:00am_open"     => 82.48
            "05:00am_midpoint" => 82.96
            "05:00am_open"     => 81.62
            "04:00am_midpoint" => 81.79
            "04:00am_open"     => 80.19
            "03:00am_midpoint" => 80.30
            "03:00am_open"     => 76.04
            "02:00am_midpoint" => 76.36
            "02:00am_open"     => 73.99
            "01:00am_midpoint" => 74.55
            "01:00am_open"     => 73.59
            "12:00am_midpoint" => 73.06
            "12:00am_open"     => 72.13
            => 0.0
    rate

get_mae_pct(level_name) =>
    mae = 0.0
    mae := switch level_name
        "07:00am_midpoint" => 0.25
        "07:00am_open"     => 0.25
        "06:00am_midpoint" => 0.25
        "06:00am_open"     => 0.30
        "05:00am_midpoint" => 0.30
        "05:00am_open"     => 0.35
        "04:00am_midpoint" => 0.35
        "04:00am_open"     => 0.40
        "03:00am_midpoint" => 0.40
        "03:00am_open"     => 0.45
        "02:00am_midpoint" => 0.45
        "02:00am_open"     => 0.45
        "01:00am_midpoint" => 0.50
        "01:00am_open"     => 0.50
        "12:00am_midpoint" => 0.50
        "12:00am_open"     => 0.50
        => 0.50
    mae

// ---------------------------------------------- Draw Hourly Levels at NY Open --------------------------------------------------

var array<line>  hourly_lines  = array.new<line>()
var array<label> hourly_labels = array.new<label>()
var array<float> hourly_prices = array.new<float>()

// Clear old lines and labels
clear_hourly_levels() =>
    if array.size(hourly_lines) > 0
        for i = 0 to array.size(hourly_lines) - 1
            line.delete(array.get(hourly_lines, i))
        array.clear(hourly_lines)
    if array.size(hourly_labels) > 0
        for i = 0 to array.size(hourly_labels) - 1
            label.delete(array.get(hourly_labels, i))
        array.clear(hourly_labels)
    if array.size(hourly_prices) > 0
        array.clear(hourly_prices)

// Draw a level
draw_level(price, level_name, hit_rate, mae_pct, ny_open_bar, ny_open_price, is_midpoint) =>
    if hit_rate >= min_hit_rate
        style      = get_line_style(line_style_input)
        lbl_size   = get_label_size(label_size_input)
        level_color = get_color_for_mae(mae_pct)

        new_line = line.new(ny_open_bar, price, ny_open_bar, price,
             color=level_color, style=style, width=line_width)
        array.push(hourly_lines, new_line)

        label_text = str.format("{0}: {1,number,#.#}%", level_name, hit_rate)
        new_label  = label.new(ny_open_bar, price, label_text,
             style=label.style_none, textcolor=level_color, size=lbl_size,
             textalign=text.align_left, text_font_family=font.family_monospace)
        array.push(hourly_labels, new_label)
        array.push(hourly_prices, price)

// Check if levels have been hit and remove them
check_and_remove_hit_levels() =>
    if remove_on_hit and array.size(hourly_prices) > 0
        i = array.size(hourly_prices) - 1
        while i >= 0
            level_price = array.get(hourly_prices, i)
            if low <= level_price and level_price <= high
                line.delete(array.get(hourly_lines, i))
                label.delete(array.get(hourly_labels, i))
                array.remove(hourly_lines, i)
                array.remove(hourly_labels, i)
                array.remove(hourly_prices, i)
            i := i - 1

// At NY open (8am), draw all relevant levels
if show_hourly_levels and is_sesa and is_sesa > is_sesa[1]
    clear_hourly_levels()

    hit_7to8_mid  := false
    hit_7am_open  := false
    hit_6to7_mid  := false
    hit_6am_open  := false
    hit_5to6_mid  := false
    hit_5am_open  := false
    hit_4to5_mid  := false
    hit_4am_open  := false
    hit_3to4_mid  := false
    hit_3am_open  := false
    hit_2to3_mid  := false
    hit_2am_open  := false
    hit_1to2_mid  := false
    hit_1am_open  := false
    hit_12to1_mid := false
    hit_12am_open := false

    ny_open_bar   = bar_index
    ny_open_price = open
    pattern       = current_pattern

    if not na(max_7am) and not na(min_7am) and show_midpoints
        mid  = (max_7am + min_7am) / 2
        rate = pattern != "unknown" ? get_pattern_hit_rate("07:00am_midpoint", pattern) : get_overall_hit_rate("07:00am_midpoint")
        mae  = get_mae_pct("07:00am_midpoint")
        draw_level(mid, "7-8am Mid", rate, mae, ny_open_bar, ny_open_price, true)

    if not na(open_7am) and show_opens
        rate = pattern != "unknown" ? get_pattern_hit_rate("07:00am_open", pattern) : get_overall_hit_rate("07:00am_open")
        mae  = get_mae_pct("07:00am_open")
        draw_level(open_7am, "7am Open", rate, mae, ny_open_bar, ny_open_price, false)

    if not na(max_6am) and not na(min_6am) and show_midpoints
        mid  = (max_6am + min_6am) / 2
        rate = pattern != "unknown" ? get_pattern_hit_rate("06:00am_midpoint", pattern) : get_overall_hit_rate("06:00am_midpoint")
        mae  = get_mae_pct("06:00am_midpoint")
        draw_level(mid, "6-7am Mid", rate, mae, ny_open_bar, ny_open_price, true)

    if not na(open_6am) and show_opens
        rate = pattern != "unknown" ? get_pattern_hit_rate("06:00am_open", pattern) : get_overall_hit_rate("06:00am_open")
        mae  = get_mae_pct("06:00am_open")
        draw_level(open_6am, "6am Open", rate, mae, ny_open_bar, ny_open_price, false)

    if not na(max_5am) and not na(min_5am) and show_midpoints
        mid  = (max_5am + min_5am) / 2
        rate = pattern != "unknown" ? get_pattern_hit_rate("05:00am_midpoint", pattern) : get_overall_hit_rate("05:00am_midpoint")
        mae  = get_mae_pct("05:00am_midpoint")
        draw_level(mid, "5-6am Mid", rate, mae, ny_open_bar, ny_open_price, true)

    if not na(open_5am) and show_opens
        rate = pattern != "unknown" ? get_pattern_hit_rate("05:00am_open", pattern) : get_overall_hit_rate("05:00am_open")
        mae  = get_mae_pct("05:00am_open")
        draw_level(open_5am, "5am Open", rate, mae, ny_open_bar, ny_open_price, false)

    if not na(max_4am) and not na(min_4am) and show_midpoints
        mid  = (max_4am + min_4am) / 2
        rate = pattern != "unknown" ? get_pattern_hit_rate("04:00am_midpoint", pattern) : get_overall_hit_rate("04:00am_midpoint")
        mae  = get_mae_pct("04:00am_midpoint")
        draw_level(mid, "4-5am Mid", rate, mae, ny_open_bar, ny_open_price, true)

    if not na(open_4am) and show_opens
        rate = pattern != "unknown" ? get_pattern_hit_rate("04:00am_open", pattern) : get_overall_hit_rate("04:00am_open")
        mae  = get_mae_pct("04:00am_open")
        draw_level(open_4am, "4am Open", rate, mae, ny_open_bar, ny_open_price, false)

    if not na(max_3am) and not na(min_3am) and show_midpoints
        mid  = (max_3am + min_3am) / 2
        rate = pattern != "unknown" ? get_pattern_hit_rate("03:00am_midpoint", pattern) : get_overall_hit_rate("03:00am_midpoint")
        mae  = get_mae_pct("03:00am_midpoint")
        draw_level(mid, "3-4am Mid", rate, mae, ny_open_bar, ny_open_price, true)

    if not na(open_3am) and show_opens
        rate = pattern != "unknown" ? get_pattern_hit_rate("03:00am_open", pattern) : get_overall_hit_rate("03:00am_open")
        mae  = get_mae_pct("03:00am_open")
        draw_level(open_3am, "3am Open", rate, mae, ny_open_bar, ny_open_price, false)

    if not na(max_2am) and not na(min_2am) and show_midpoints
        mid  = (max_2am + min_2am) / 2
        rate = pattern != "unknown" ? get_pattern_hit_rate("02:00am_midpoint", pattern) : get_overall_hit_rate("02:00am_midpoint")
        mae  = get_mae_pct("02:00am_midpoint")
        draw_level(mid, "2-3am Mid", rate, mae, ny_open_bar, ny_open_price, true)

    if not na(open_2am) and show_opens
        rate = pattern != "unknown" ? get_pattern_hit_rate("02:00am_open", pattern) : get_overall_hit_rate("02:00am_open")
        mae  = get_mae_pct("02:00am_open")
        draw_level(open_2am, "2am Open", rate, mae, ny_open_bar, ny_open_price, false)

    if not na(max_1am) and not na(min_1am) and show_midpoints
        mid  = (max_1am + min_1am) / 2
        rate = pattern != "unknown" ? get_pattern_hit_rate("01:00am_midpoint", pattern) : get_overall_hit_rate("01:00am_midpoint")
        mae  = get_mae_pct("01:00am_midpoint")
        draw_level(mid, "1-2am Mid", rate, mae, ny_open_bar, ny_open_price, true)

    if not na(open_1am) and show_opens
        rate = pattern != "unknown" ? get_pattern_hit_rate("01:00am_open", pattern) : get_overall_hit_rate("01:00am_open")
        mae  = get_mae_pct("01:00am_open")
        draw_level(open_1am, "1am Open", rate, mae, ny_open_bar, ny_open_price, false)

    if not na(max_12am) and not na(min_12am) and show_midpoints
        mid  = (max_12am + min_12am) / 2
        rate = pattern != "unknown" ? get_pattern_hit_rate("12:00am_midpoint", pattern) : get_overall_hit_rate("12:00am_midpoint")
        mae  = get_mae_pct("12:00am_midpoint")
        draw_level(mid, "12-1am Mid", rate, mae, ny_open_bar, ny_open_price, true)

    if not na(open_12am) and show_opens
        rate = pattern != "unknown" ? get_pattern_hit_rate("12:00am_open", pattern) : get_overall_hit_rate("12:00am_open")
        mae  = get_mae_pct("12:00am_open")
        draw_level(open_12am, "12am Open", rate, mae, ny_open_bar, ny_open_price, false)

// Track hits during NY session
if is_sesa
    if not na(max_7am) and not na(min_7am)
        mid = (max_7am + min_7am) / 2
        if low <= mid and mid <= high
            hit_7to8_mid := true
    if not na(open_7am) and low <= open_7am and open_7am <= high
        hit_7am_open := true
    if not na(max_6am) and not na(min_6am)
        mid = (max_6am + min_6am) / 2
        if low <= mid and mid <= high
            hit_6to7_mid := true
    if not na(open_6am) and low <= open_6am and open_6am <= high
        hit_6am_open := true
    if not na(max_5am) and not na(min_5am)
        mid = (max_5am + min_5am) / 2
        if low <= mid and mid <= high
            hit_5to6_mid := true
    if not na(open_5am) and low <= open_5am and open_5am <= high
        hit_5am_open := true
    if not na(max_4am) and not na(min_4am)
        mid = (max_4am + min_4am) / 2
        if low <= mid and mid <= high
            hit_4to5_mid := true
    if not na(open_4am) and low <= open_4am and open_4am <= high
        hit_4am_open := true
    if not na(max_3am) and not na(min_3am)
        mid = (max_3am + min_3am) / 2
        if low <= mid and mid <= high
            hit_3to4_mid := true
    if not na(open_3am) and low <= open_3am and open_3am <= high
        hit_3am_open := true
    if not na(max_2am) and not na(min_2am)
        mid = (max_2am + min_2am) / 2
        if low <= mid and mid <= high
            hit_2to3_mid := true
    if not na(open_2am) and low <= open_2am and open_2am <= high
        hit_2am_open := true
    if not na(max_1am) and not na(min_1am)
        mid = (max_1am + min_1am) / 2
        if low <= mid and mid <= high
            hit_1to2_mid := true
    if not na(open_1am) and low <= open_1am and open_1am <= high
        hit_1am_open := true
    if not na(max_12am) and not na(min_12am)
        mid = (max_12am + min_12am) / 2
        if low <= mid and mid <= high
            hit_12to1_mid := true
    if not na(open_12am) and low <= open_12am and open_12am <= high
        hit_12am_open := true

// Extend lines during NY session and check for hits
if is_sesa and array.size(hourly_lines) > 0
    curr_bar = bar_index
    check_and_remove_hit_levels()
    remaining = array.size(hourly_lines)
    if remaining > 0
        for i = 0 to remaining - 1
            line.set_x2(array.get(hourly_lines, i), curr_bar)
            label.set_x(array.get(hourly_labels, i), curr_bar)

// ---------------------------------------------- Statistics Table --------------------------------------------------

get_pattern_label(pattern) =>
    switch pattern
        "london_engulfs_asia" => "London Engulfs Asia"
        "asia_engulfs_london" => "Asia Engulfs London"
        "london_partial_up"   => "London Partial Up"
        "london_partial_down" => "London Partial Down"
        => "Unknown"

// Helper: compute live distance % from current close to a level price
// Returns na if level price is na
live_dist(level_price) =>
    not na(level_price) ? math.abs(close - level_price) / close * 100 : float(na)

// Helper: format distance for table display
// Shows "Hit ✓" if already hit, the % distance if pending, or "—" if level not available
fmt_dist(dist_pct, was_hit) =>
    if was_hit
        "Hit ✓"
    else if na(dist_pct)
        "—"
    else
        str.format("{0,number,#.##}%", dist_pct)

var table stats_table = na

if show_stats_table
    table.delete(stats_table)

    table_pos  = get_table_position(stats_table_pos)
    table_size = get_table_text_size(stats_table_size)

    header_bg    = color.rgb(64, 64, 64)
    subheader_bg = color.rgb(96, 96, 96)
    header_text  = color.white
    cell_bg      = color.white
    cell_text    = color.rgb(64, 64, 64)
    hit_bg       = color.teal
    hit_text     = color.white
    pending_bg   = color.yellow
    pending_text = color.rgb(64, 64, 64)

    // ---- WAITING STATE ----
    if not is_sesa
        stats_table := table.new(table_pos, 5, 6, border_width=1, border_color=color.rgb(64, 64, 64), bgcolor=cell_bg)
        table.cell(stats_table, 0, 0, "━━━ NY SESSION STATS ━━━", bgcolor=header_bg, text_color=header_text,
             text_size=table_size, text_font_family=font.family_monospace)
        table.merge_cells(stats_table, 0, 0, 4, 0)
        table.cell(stats_table, 0, 1, "Active Session", bgcolor=cell_bg, text_color=cell_text,
             text_size=table_size, text_font_family=font.family_monospace)
        table.cell(stats_table, 1, 1, "Closed", bgcolor=color.rgb(96, 96, 96), text_color=color.white,
             text_size=table_size, text_font_family=font.family_monospace)
        table.merge_cells(stats_table, 1, 1, 4, 1)
        table.cell(stats_table, 0, 2, "Pattern", bgcolor=cell_bg, text_color=cell_text,
             text_size=table_size, text_font_family=font.family_monospace)
        table.cell(stats_table, 1, 2, "Waiting for next NY session", bgcolor=cell_bg, text_color=color.rgb(150, 150, 150),
             text_size=table_size, text_font_family=font.family_monospace)
        table.merge_cells(stats_table, 1, 2, 4, 2)
        table.cell(stats_table, 0, 3, "━━━ TOP 11 LEVELS ━━━", bgcolor=header_bg, text_color=header_text,
             text_size=table_size, text_font_family=font.family_monospace)
        table.merge_cells(stats_table, 0, 3, 4, 3)
        table.cell(stats_table, 0, 4, "Level",    bgcolor=subheader_bg, text_color=header_text, text_size=table_size, text_font_family=font.family_monospace)
        table.cell(stats_table, 1, 4, "Hit %",    bgcolor=subheader_bg, text_color=header_text, text_size=table_size, text_font_family=font.family_monospace)
        table.cell(stats_table, 2, 4, "MAE %",    bgcolor=subheader_bg, text_color=header_text, text_size=table_size, text_font_family=font.family_monospace)
        table.cell(stats_table, 3, 4, "Dist Now", bgcolor=subheader_bg, text_color=header_text, text_size=table_size, text_font_family=font.family_monospace)
        table.cell(stats_table, 4, 4, "Status",   bgcolor=subheader_bg, text_color=header_text, text_size=table_size, text_font_family=font.family_monospace)
        table.cell(stats_table, 0, 5, "Levels reset at 8am NY", bgcolor=cell_bg, text_color=color.rgb(150, 150, 150),
             text_size=table_size, text_font_family=font.family_monospace)
        table.merge_cells(stats_table, 0, 5, 4, 5)

    // ---- ACTIVE STATE ----
    if is_sesa
        // Pre-calculate all level prices for live distance
        mid_7to8   = (not na(max_7am)  and not na(min_7am))  ? (max_7am  + min_7am)  / 2 : float(na)
        mid_6to7   = (not na(max_6am)  and not na(min_6am))  ? (max_6am  + min_6am)  / 2 : float(na)
        mid_5to6   = (not na(max_5am)  and not na(min_5am))  ? (max_5am  + min_5am)  / 2 : float(na)
        mid_4to5   = (not na(max_4am)  and not na(min_4am))  ? (max_4am  + min_4am)  / 2 : float(na)
        mid_3to4   = (not na(max_3am)  and not na(min_3am))  ? (max_3am  + min_3am)  / 2 : float(na)
        mid_2to3   = (not na(max_2am)  and not na(min_2am))  ? (max_2am  + min_2am)  / 2 : float(na)
        mid_1to2   = (not na(max_1am)  and not na(min_1am))  ? (max_1am  + min_1am)  / 2 : float(na)
        mid_12to1  = (not na(max_12am) and not na(min_12am)) ? (max_12am + min_12am) / 2 : float(na)

        // Live distances
        d_7to8_mid  = live_dist(mid_7to8)
        d_7am_open  = live_dist(open_7am)
        d_6to7_mid  = live_dist(mid_6to7)
        d_6am_open  = live_dist(open_6am)
        d_5to6_mid  = live_dist(mid_5to6)
        d_5am_open  = live_dist(open_5am)
        d_4to5_mid  = live_dist(mid_4to5)
        d_4am_open  = live_dist(open_4am)
        d_3to4_mid  = live_dist(mid_3to4)
        d_3am_open  = live_dist(open_3am)
        d_2to3_mid  = live_dist(mid_2to3)
        d_2am_open  = live_dist(open_2am)
        d_1to2_mid  = live_dist(mid_1to2)
        d_1am_open  = live_dist(open_1am)
        d_12to1_mid = live_dist(mid_12to1)
        d_12am_open = live_dist(open_12am)

        stats_table := table.new(table_pos, 5, 27, border_width=1, border_color=color.rgb(64, 64, 64), bgcolor=cell_bg)

        row = 0

        // Header
        table.cell(stats_table, 0, row, "━━━ NY SESSION STATS ━━━", bgcolor=header_bg, text_color=header_text,
             text_size=table_size, text_font_family=font.family_monospace)
        table.merge_cells(stats_table, 0, row, 4, row)
        row := row + 1

        // Session active
        table.cell(stats_table, 0, row, "Active Session", bgcolor=cell_bg, text_color=cell_text,
             text_size=table_size, text_font_family=font.family_monospace)
        table.cell(stats_table, 1, row, "New York", bgcolor=sesa_css, text_color=color.white,
             text_size=table_size, text_font_family=font.family_monospace)
        table.merge_cells(stats_table, 1, row, 4, row)
        row := row + 1

        // Pattern
        pattern_label = get_pattern_label(current_pattern)
        table.cell(stats_table, 0, row, "Pattern", bgcolor=cell_bg, text_color=cell_text,
             text_size=table_size, text_font_family=font.family_monospace)
        table.cell(stats_table, 1, row, pattern_label, bgcolor=cell_bg, text_color=cell_text,
             text_size=table_size, text_font_family=font.family_monospace)
        table.merge_cells(stats_table, 1, row, 4, row)
        row := row + 1

        // Active levels count
        active_count = array.size(hourly_lines)
        table.cell(stats_table, 0, row, "Active Levels", bgcolor=cell_bg, text_color=cell_text,
             text_size=table_size, text_font_family=font.family_monospace)
        active_bg  = active_count > 0 ? pending_bg : cell_bg
        active_txt = active_count > 0 ? pending_text : cell_text
        table.cell(stats_table, 1, row, str.tostring(active_count), bgcolor=active_bg, text_color=active_txt,
             text_size=table_size, text_font_family=font.family_monospace)
        table.merge_cells(stats_table, 1, row, 4, row)
        row := row + 1

        if current_pattern != "unknown"
            // Section header
            table.cell(stats_table, 0, row, "━━━ TOP 11 LEVELS ━━━", bgcolor=header_bg, text_color=header_text,
                 text_size=table_size, text_font_family=font.family_monospace)
            table.merge_cells(stats_table, 0, row, 4, row)
            row := row + 1

            // Column headers
            table.cell(stats_table, 0, row, "Level",    bgcolor=subheader_bg, text_color=header_text, text_size=table_size, text_font_family=font.family_monospace)
            table.cell(stats_table, 1, row, "Hit %",    bgcolor=subheader_bg, text_color=header_text, text_size=table_size, text_font_family=font.family_monospace)
            table.cell(stats_table, 2, row, "MAE %",    bgcolor=subheader_bg, text_color=header_text, text_size=table_size, text_font_family=font.family_monospace)
            table.cell(stats_table, 3, row, "Distance from level", bgcolor=subheader_bg, text_color=header_text, text_size=table_size, text_font_family=font.family_monospace)
            table.cell(stats_table, 4, row, "Status",   bgcolor=subheader_bg, text_color=header_text, text_size=table_size, text_font_family=font.family_monospace)
            row := row + 1

            // ---- Macro to write one level row ----
            // (Pine doesn't have macros so we inline each row)

            // Level 1: 7-8am Mid
            rate = get_pattern_hit_rate("07:00am_midpoint", current_pattern)
            mae  = get_mae_pct("07:00am_midpoint")
            dist = hit_7to8_mid ? float(na) : d_7to8_mid
            dist_bg   = hit_7to8_mid ? hit_bg   : get_distance_color(nz(dist, 999.0), mae)
            dist_txt  = hit_7to8_mid ? hit_text  : get_distance_text_color(nz(dist, 999.0), mae)
            table.cell(stats_table, 0, row, "7-8am Mid",  bgcolor=cell_bg,  text_color=cell_text, text_size=table_size, text_font_family=font.family_monospace)
            table.cell(stats_table, 1, row, str.format("{0,number,#.#}%", rate), bgcolor=cell_bg, text_color=cell_text, text_size=table_size, text_font_family=font.family_monospace)
            table.cell(stats_table, 2, row, str.format("{0,number,#.##}%", mae), bgcolor=cell_bg, text_color=cell_text, text_size=table_size, text_font_family=font.family_monospace)
            table.cell(stats_table, 3, row, fmt_dist(dist, hit_7to8_mid), bgcolor=dist_bg, text_color=dist_txt, text_size=table_size, text_font_family=font.family_monospace)
            val_bg = hit_7to8_mid ? hit_bg : pending_bg
            val_txt = hit_7to8_mid ? hit_text : pending_text
            table.cell(stats_table, 4, row, hit_7to8_mid ? "Hit ✓" : "Pending", bgcolor=val_bg, text_color=val_txt, text_size=table_size, text_font_family=font.family_monospace)
            row := row + 1

            // Level 2: 7am Open
            rate := get_pattern_hit_rate("07:00am_open", current_pattern)
            mae  := get_mae_pct("07:00am_open")
            dist := hit_7am_open ? float(na) : d_7am_open
            dist_bg   := hit_7am_open ? hit_bg  : get_distance_color(nz(dist, 999.0), mae)
            dist_txt  := hit_7am_open ? hit_text : get_distance_text_color(nz(dist, 999.0), mae)
            table.cell(stats_table, 0, row, "7am Open",   bgcolor=cell_bg, text_color=cell_text, text_size=table_size, text_font_family=font.family_monospace)
            table.cell(stats_table, 1, row, str.format("{0,number,#.#}%", rate), bgcolor=cell_bg, text_color=cell_text, text_size=table_size, text_font_family=font.family_monospace)
            table.cell(stats_table, 2, row, str.format("{0,number,#.##}%", mae), bgcolor=cell_bg, text_color=cell_text, text_size=table_size, text_font_family=font.family_monospace)
            table.cell(stats_table, 3, row, fmt_dist(dist, hit_7am_open), bgcolor=dist_bg, text_color=dist_txt, text_size=table_size, text_font_family=font.family_monospace)
            val_bg := hit_7am_open ? hit_bg : pending_bg
            val_txt := hit_7am_open ? hit_text : pending_text
            table.cell(stats_table, 4, row, hit_7am_open ? "Hit ✓" : "Pending", bgcolor=val_bg, text_color=val_txt, text_size=table_size, text_font_family=font.family_monospace)
            row := row + 1

            // Level 3: 6-7am Mid
            rate := get_pattern_hit_rate("06:00am_midpoint", current_pattern)
            mae  := get_mae_pct("06:00am_midpoint")
            dist := hit_6to7_mid ? float(na) : d_6to7_mid
            dist_bg   := hit_6to7_mid ? hit_bg  : get_distance_color(nz(dist, 999.0), mae)
            dist_txt  := hit_6to7_mid ? hit_text : get_distance_text_color(nz(dist, 999.0), mae)
            table.cell(stats_table, 0, row, "6-7am Mid",  bgcolor=cell_bg, text_color=cell_text, text_size=table_size, text_font_family=font.family_monospace)
            table.cell(stats_table, 1, row, str.format("{0,number,#.#}%", rate), bgcolor=cell_bg, text_color=cell_text, text_size=table_size, text_font_family=font.family_monospace)
            table.cell(stats_table, 2, row, str.format("{0,number,#.##}%", mae), bgcolor=cell_bg, text_color=cell_text, text_size=table_size, text_font_family=font.family_monospace)
            table.cell(stats_table, 3, row, fmt_dist(dist, hit_6to7_mid), bgcolor=dist_bg, text_color=dist_txt, text_size=table_size, text_font_family=font.family_monospace)
            val_bg := hit_6to7_mid ? hit_bg : pending_bg
            val_txt := hit_6to7_mid ? hit_text : pending_text
            table.cell(stats_table, 4, row, hit_6to7_mid ? "Hit ✓" : "Pending", bgcolor=val_bg, text_color=val_txt, text_size=table_size, text_font_family=font.family_monospace)
            row := row + 1

            // Level 4: 6am Open
            rate := get_pattern_hit_rate("06:00am_open", current_pattern)
            mae  := get_mae_pct("06:00am_open")
            dist := hit_6am_open ? float(na) : d_6am_open
            dist_bg   := hit_6am_open ? hit_bg  : get_distance_color(nz(dist, 999.0), mae)
            dist_txt  := hit_6am_open ? hit_text : get_distance_text_color(nz(dist, 999.0), mae)
            table.cell(stats_table, 0, row, "6am Open",   bgcolor=cell_bg, text_color=cell_text, text_size=table_size, text_font_family=font.family_monospace)
            table.cell(stats_table, 1, row, str.format("{0,number,#.#}%", rate), bgcolor=cell_bg, text_color=cell_text, text_size=table_size, text_font_family=font.family_monospace)
            table.cell(stats_table, 2, row, str.format("{0,number,#.##}%", mae), bgcolor=cell_bg, text_color=cell_text, text_size=table_size, text_font_family=font.family_monospace)
            table.cell(stats_table, 3, row, fmt_dist(dist, hit_6am_open), bgcolor=dist_bg, text_color=dist_txt, text_size=table_size, text_font_family=font.family_monospace)
            val_bg := hit_6am_open ? hit_bg : pending_bg
            val_txt := hit_6am_open ? hit_text : pending_text
            table.cell(stats_table, 4, row, hit_6am_open ? "Hit ✓" : "Pending", bgcolor=val_bg, text_color=val_txt, text_size=table_size, text_font_family=font.family_monospace)
            row := row + 1

            // Level 5: 5-6am Mid
            rate := get_pattern_hit_rate("05:00am_midpoint", current_pattern)
            mae  := get_mae_pct("05:00am_midpoint")
            dist := hit_5to6_mid ? float(na) : d_5to6_mid
            dist_bg   := hit_5to6_mid ? hit_bg  : get_distance_color(nz(dist, 999.0), mae)
            dist_txt  := hit_5to6_mid ? hit_text : get_distance_text_color(nz(dist, 999.0), mae)
            table.cell(stats_table, 0, row, "5-6am Mid",  bgcolor=cell_bg, text_color=cell_text, text_size=table_size, text_font_family=font.family_monospace)
            table.cell(stats_table, 1, row, str.format("{0,number,#.#}%", rate), bgcolor=cell_bg, text_color=cell_text, text_size=table_size, text_font_family=font.family_monospace)
            table.cell(stats_table, 2, row, str.format("{0,number,#.##}%", mae), bgcolor=cell_bg, text_color=cell_text, text_size=table_size, text_font_family=font.family_monospace)
            table.cell(stats_table, 3, row, fmt_dist(dist, hit_5to6_mid), bgcolor=dist_bg, text_color=dist_txt, text_size=table_size, text_font_family=font.family_monospace)
            val_bg := hit_5to6_mid ? hit_bg : pending_bg
            val_txt := hit_5to6_mid ? hit_text : pending_text
            table.cell(stats_table, 4, row, hit_5to6_mid ? "Hit ✓" : "Pending", bgcolor=val_bg, text_color=val_txt, text_size=table_size, text_font_family=font.family_monospace)
            row := row + 1

            // Level 6: 5am Open
            rate := get_pattern_hit_rate("05:00am_open", current_pattern)
            mae  := get_mae_pct("05:00am_open")
            dist := hit_5am_open ? float(na) : d_5am_open
            dist_bg   := hit_5am_open ? hit_bg  : get_distance_color(nz(dist, 999.0), mae)
            dist_txt  := hit_5am_open ? hit_text : get_distance_text_color(nz(dist, 999.0), mae)
            table.cell(stats_table, 0, row, "5am Open",   bgcolor=cell_bg, text_color=cell_text, text_size=table_size, text_font_family=font.family_monospace)
            table.cell(stats_table, 1, row, str.format("{0,number,#.#}%", rate), bgcolor=cell_bg, text_color=cell_text, text_size=table_size, text_font_family=font.family_monospace)
            table.cell(stats_table, 2, row, str.format("{0,number,#.##}%", mae), bgcolor=cell_bg, text_color=cell_text, text_size=table_size, text_font_family=font.family_monospace)
            table.cell(stats_table, 3, row, fmt_dist(dist, hit_5am_open), bgcolor=dist_bg, text_color=dist_txt, text_size=table_size, text_font_family=font.family_monospace)
            val_bg := hit_5am_open ? hit_bg : pending_bg
            val_txt := hit_5am_open ? hit_text : pending_text
            table.cell(stats_table, 4, row, hit_5am_open ? "Hit ✓" : "Pending", bgcolor=val_bg, text_color=val_txt, text_size=table_size, text_font_family=font.family_monospace)
            row := row + 1

            // Level 7: 4-5am Mid
            rate := get_pattern_hit_rate("04:00am_midpoint", current_pattern)
            mae  := get_mae_pct("04:00am_midpoint")
            dist := hit_4to5_mid ? float(na) : d_4to5_mid
            dist_bg   := hit_4to5_mid ? hit_bg  : get_distance_color(nz(dist, 999.0), mae)
            dist_txt  := hit_4to5_mid ? hit_text : get_distance_text_color(nz(dist, 999.0), mae)
            table.cell(stats_table, 0, row, "4-5am Mid",  bgcolor=cell_bg, text_color=cell_text, text_size=table_size, text_font_family=font.family_monospace)
            table.cell(stats_table, 1, row, str.format("{0,number,#.#}%", rate), bgcolor=cell_bg, text_color=cell_text, text_size=table_size, text_font_family=font.family_monospace)
            table.cell(stats_table, 2, row, str.format("{0,number,#.##}%", mae), bgcolor=cell_bg, text_color=cell_text, text_size=table_size, text_font_family=font.family_monospace)
            table.cell(stats_table, 3, row, fmt_dist(dist, hit_4to5_mid), bgcolor=dist_bg, text_color=dist_txt, text_size=table_size, text_font_family=font.family_monospace)
            val_bg := hit_4to5_mid ? hit_bg : pending_bg
            val_txt := hit_4to5_mid ? hit_text : pending_text
            table.cell(stats_table, 4, row, hit_4to5_mid ? "Hit ✓" : "Pending", bgcolor=val_bg, text_color=val_txt, text_size=table_size, text_font_family=font.family_monospace)
            row := row + 1

            // Level 8: 4am Open
            rate := get_pattern_hit_rate("04:00am_open", current_pattern)
            mae  := get_mae_pct("04:00am_open")
            dist := hit_4am_open ? float(na) : d_4am_open
            dist_bg   := hit_4am_open ? hit_bg  : get_distance_color(nz(dist, 999.0), mae)
            dist_txt  := hit_4am_open ? hit_text : get_distance_text_color(nz(dist, 999.0), mae)
            table.cell(stats_table, 0, row, "4am Open",   bgcolor=cell_bg, text_color=cell_text, text_size=table_size, text_font_family=font.family_monospace)
            table.cell(stats_table, 1, row, str.format("{0,number,#.#}%", rate), bgcolor=cell_bg, text_color=cell_text, text_size=table_size, text_font_family=font.family_monospace)
            table.cell(stats_table, 2, row, str.format("{0,number,#.##}%", mae), bgcolor=cell_bg, text_color=cell_text, text_size=table_size, text_font_family=font.family_monospace)
            table.cell(stats_table, 3, row, fmt_dist(dist, hit_4am_open), bgcolor=dist_bg, text_color=dist_txt, text_size=table_size, text_font_family=font.family_monospace)
            val_bg := hit_4am_open ? hit_bg : pending_bg
            val_txt := hit_4am_open ? hit_text : pending_text
            table.cell(stats_table, 4, row, hit_4am_open ? "Hit ✓" : "Pending", bgcolor=val_bg, text_color=val_txt, text_size=table_size, text_font_family=font.family_monospace)
            row := row + 1

            // Level 9: 3-4am Mid
            rate := get_pattern_hit_rate("03:00am_midpoint", current_pattern)
            mae  := get_mae_pct("03:00am_midpoint")
            dist := hit_3to4_mid ? float(na) : d_3to4_mid
            dist_bg   := hit_3to4_mid ? hit_bg  : get_distance_color(nz(dist, 999.0), mae)
            dist_txt  := hit_3to4_mid ? hit_text : get_distance_text_color(nz(dist, 999.0), mae)
            table.cell(stats_table, 0, row, "3-4am Mid",  bgcolor=cell_bg, text_color=cell_text, text_size=table_size, text_font_family=font.family_monospace)
            table.cell(stats_table, 1, row, str.format("{0,number,#.#}%", rate), bgcolor=cell_bg, text_color=cell_text, text_size=table_size, text_font_family=font.family_monospace)
            table.cell(stats_table, 2, row, str.format("{0,number,#.##}%", mae), bgcolor=cell_bg, text_color=cell_text, text_size=table_size, text_font_family=font.family_monospace)
            table.cell(stats_table, 3, row, fmt_dist(dist, hit_3to4_mid), bgcolor=dist_bg, text_color=dist_txt, text_size=table_size, text_font_family=font.family_monospace)
            val_bg := hit_3to4_mid ? hit_bg : pending_bg
            val_txt := hit_3to4_mid ? hit_text : pending_text
            table.cell(stats_table, 4, row, hit_3to4_mid ? "Hit ✓" : "Pending", bgcolor=val_bg, text_color=val_txt, text_size=table_size, text_font_family=font.family_monospace)
            row := row + 1

            // Level 10: 3am Open
            rate := get_pattern_hit_rate("03:00am_open", current_pattern)
            mae  := get_mae_pct("03:00am_open")
            dist := hit_3am_open ? float(na) : d_3am_open
            dist_bg   := hit_3am_open ? hit_bg  : get_distance_color(nz(dist, 999.0), mae)
            dist_txt  := hit_3am_open ? hit_text : get_distance_text_color(nz(dist, 999.0), mae)
            table.cell(stats_table, 0, row, "3am Open",   bgcolor=cell_bg, text_color=cell_text, text_size=table_size, text_font_family=font.family_monospace)
            table.cell(stats_table, 1, row, str.format("{0,number,#.#}%", rate), bgcolor=cell_bg, text_color=cell_text, text_size=table_size, text_font_family=font.family_monospace)
            table.cell(stats_table, 2, row, str.format("{0,number,#.##}%", mae), bgcolor=cell_bg, text_color=cell_text, text_size=table_size, text_font_family=font.family_monospace)
            table.cell(stats_table, 3, row, fmt_dist(dist, hit_3am_open), bgcolor=dist_bg, text_color=dist_txt, text_size=table_size, text_font_family=font.family_monospace)
            val_bg := hit_3am_open ? hit_bg : pending_bg
            val_txt := hit_3am_open ? hit_text : pending_text
            table.cell(stats_table, 4, row, hit_3am_open ? "Hit ✓" : "Pending", bgcolor=val_bg, text_color=val_txt, text_size=table_size, text_font_family=font.family_monospace)
            row := row + 1

            // Level 11: 2-3am Mid
            rate := get_pattern_hit_rate("02:00am_midpoint", current_pattern)
            mae  := get_mae_pct("02:00am_midpoint")
            dist := hit_2to3_mid ? float(na) : d_2to3_mid
            dist_bg   := hit_2to3_mid ? hit_bg  : get_distance_color(nz(dist, 999.0), mae)
            dist_txt  := hit_2to3_mid ? hit_text : get_distance_text_color(nz(dist, 999.0), mae)
            table.cell(stats_table, 0, row, "2-3am Mid",  bgcolor=cell_bg, text_color=cell_text, text_size=table_size, text_font_family=font.family_monospace)
            table.cell(stats_table, 1, row, str.format("{0,number,#.#}%", rate), bgcolor=cell_bg, text_color=cell_text, text_size=table_size, text_font_family=font.family_monospace)
            table.cell(stats_table, 2, row, str.format("{0,number,#.##}%", mae), bgcolor=cell_bg, text_color=cell_text, text_size=table_size, text_font_family=font.family_monospace)
            table.cell(stats_table, 3, row, fmt_dist(dist, hit_2to3_mid), bgcolor=dist_bg, text_color=dist_txt, text_size=table_size, text_font_family=font.family_monospace)
            val_bg := hit_2to3_mid ? hit_bg : pending_bg
            val_txt := hit_2to3_mid ? hit_text : pending_text
            table.cell(stats_table, 4, row, hit_2to3_mid ? "Hit ✓" : "Pending", bgcolor=val_bg, text_color=val_txt, text_size=table_size, text_font_family=font.family_monospace)

Overview

NQ Hourly Retracement Levels plots hourly open prices and midpoint retracement levels at the New York session open, filtering levels based on a configurable minimum hit rate threshold. Users can adjust session background colors, line styles, automatically remove levels once price hits them, and display an on-screen statistics table.

Details

Published by

TamaraKGreen on GitHub

Code credit

npg, as written in the script's header

Type

Indicator, Pine Script v5

Draws

On the price chart

Category

Sessions & time, Support & resistance, Screeners & dashboards

Markets

Indices

Timeframes

M1-M5, M15-H1

Risk Notes

No repainting, lookahead or stacked entries were found in its code or README. That says nothing about how well it works: test it first.

Settings

The inputs you can change from the gear icon on the chart. Defaults are the author's.

SettingTypeDefaultVariable
Show NY Sessionbooltrueshow_sesa
sesa_csscolorcolor.rgb(255, 0, 55)sesa_css
Show London Sessionbooltrueshow_sesb
sesb_csscolorcolor.rgb(33, 87, 243)sesb_css
Show Asia Sessionbooltrueshow_sesc
sesc_csscolorcolor.rgb(255, 152, 0)sesc_css
Range Area Transparencyfloat90bg_transp
Range Outlinebooltrueshow_outline
Show Hourly Levels at NY Openbooltrueshow_hourly_levels
Show Hourly Opensbooltrueshow_opens
Show Hourly Midpointsbooltrueshow_midpoints
Minimum Hit Rate % to Displayfloat75.0min_hit_rate
Show all 19 settings
SettingTypeDefaultVariable
Line Widthint1line_width
Retracement Line StylestringDashedline_style_input
Label Sizestringsmalllabel_size_input
Remove Level When Hitbooltrueremove_on_hit
Show Statistics Tablebooltrueshow_stats_table
Stats Table PositionstringBottom Rightstats_table_pos
Stats Table SizestringSmallstats_table_size
Third-party code. ForexR did not write, test or endorse this script, and the summary above was generated automatically from its code and README. Indicators describe the past, and a strategy's backtest can look better than live trading, especially when a script repaints. Check it in the Strategy Tester and on a demo account before you rely on it.

Similar Pine Scripts

575 stars

This indicator builds Market Profile Time Price Opportunity (TPO) structures across defined session timeframes directly on the main price chart. It visualizes the Point of Control (POC) and Value Area boundaries using dynamic box drawings. An integrated multi-

Indicator v5 Support & resistanceScreeners & dashboards
Updated 1 year ago · MPL-2.0 GitHub repository of Market Profile with Screener
257 stars

This indicator overlays an intraday SPX and ES chart with a midnight session range, regular-trading-hours opening levels, and a VWAP projection box. It draws the midnight high, mid, and low, flags edge rejections within an absolute tolerance, and plots optiona

Indicator v6 TrendSupport & resistance
Updated 6 weeks ago · None stated GitHub repository of SPX Midnight Box + Snag Logic

Golden Bullet Strategy

by TradersPost
218 stars

The Golden Bullet Strategy is an overlay signal indicator based on Smart Money Concepts and time-based liquidity models. It identifies liquidity sweeps of a designated reference session range during a specified hunt window. It then confirms Market Structure Sh

Indicator v6 SMC / ICT / price actionSessions & time
Updated 3 months ago · None stated GitHub repository of Golden Bullet Strategy

Session Edge Probabilities

by TraderOracle
61 stars

Session Edge Probabilities tracks eight session-based trading rules using statistical probabilities evaluated on historical chart data. It utilizes a fixed calculation timeframe to evaluate opening ranges, midnight levels, afternoon reversal windows, and price

Indicator v6 Sessions & timeScreeners & dashboards
Updated 9 days ago · None stated GitHub repository of Session Edge Probabilities
56 stars

This indicator marks forex trading sessions and opening ranges on the price chart. It can draw Sydney, Tokyo, Frankfurt, London, and New York session boxes or five- and fifteen-minute opening ranges, plus an optional opening channel and a day-open range. Day r

Indicator v5 Sessions & timeSupport & resistance Repaints
Updated 2 months ago · None stated GitHub repository of Day Trading Booster by DGT

More indicators

Trade Your Charts With a Regulated Broker

Compare brokers that connect to TradingView, with spreads, minimum deposits and leverage side by side.

Compare brokers