Skip to content

Instantly share code, notes, and snippets.

@dp21g
Created October 13, 2025 18:31
Show Gist options
  • Select an option

  • Save dp21g/12e3073e551ef97cca2f2fd173b679b8 to your computer and use it in GitHub Desktop.

Select an option

Save dp21g/12e3073e551ef97cca2f2fd173b679b8 to your computer and use it in GitHub Desktop.

Revisions

  1. dp21g created this gist Oct 13, 2025.
    197 changes: 197 additions & 0 deletions gistfile1.txt
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,197 @@
    //@version=5
    indicator("Hourly Open Untouched Zones", overlay=true, max_boxes_count=500, max_lines_count=500, max_labels_count=500)

    // —————————————————————————————————————————————————————————————————————————————
    // Part 3: User Customization (Inputs)
    // —————————————————————————————————————————————————————————————————————————————

    var string G_COLORS = "Colors & Transparency"
    c_box_a_base = input.color(color.blue, "Box A Color", group=G_COLORS, inline="box_a")
    c_box_a_transp = input.int(80, "Transparency", minval=0, maxval=100, group=G_COLORS, inline="box_a")
    c_box_b_base = input.color(color.orange, "Box B Color", group=G_COLORS, inline="box_b")
    c_box_b_transp = input.int(85, "Transparency", minval=0, maxval=100, group=G_COLORS, inline="box_b")
    c_untouched_box_base = input.color(color.fuchsia, "Untouched 'Pink' Box Color", group=G_COLORS, inline="untouched_box")
    c_untouched_box_transp = input.int(75, "Transparency", minval=0, maxval=100, group=G_COLORS, inline="untouched_box")
    c_hourly_line_base = input.color(color.red, "Hourly Open Line Color", group=G_COLORS, inline="hourly_line")
    c_half_hour_line_base = input.color(color.blue, "Half-Hour Line Color", group=G_COLORS, inline="half_hour_line")
    c_breakout_label_base = input.color(color.purple, "Breakout Label Color", group=G_COLORS, inline="breakout_label")

    var string G_LINES = "Line Styles"
    s_hourly_line = input.string("Solid", "Hourly Open Line Style", options=["Solid", "Dashed", "Dotted"], group=G_LINES)
    w_hourly_line = input.int(2, "Hourly Open Line Width", minval=1, group=G_LINES)
    s_half_hour_line = input.string("Dashed", "Half-Hour Open Line Style", options=["Solid", "Dashed", "Dotted"], group=G_LINES)
    w_half_hour_line = input.int(1, "Half-Hour Open Line Width", minval=1, group=G_LINES)

    var string G_LABEL = "Breakout Label/Symbol"
    s_breakout_symbol = input.string("▲", "Breakout Symbol", options=["▲", "▶", "◆", "⭐"], group=G_LABEL)
    s_breakout_size = input.string(size.small, "Breakout Symbol Size", options=[size.tiny, size.small, size.normal, size.large, size.huge], group=G_LABEL)

    c_box_a = color.new(c_box_a_base, c_box_a_transp)
    c_box_b = color.new(c_box_b_base, c_box_b_transp)
    c_untouched_box = color.new(c_untouched_box_base, c_untouched_box_transp)

    // —————————————————————————————————————————————————————————————————————————————
    // Helper Functions & Global Variables
    // —————————————————————————————————————————————————————————————————————————————

    f_get_line_style(style_string) =>
    style_string == "Solid" ? line.style_solid : style_string == "Dashed" ? line.style_dashed : line.style_dotted

    var string TZ = "America/New_York"

    type HourlySession
    int start_time
    float hourly_open
    float high_h1
    float low_h1
    float high_h2
    float low_h2
    bool second_half_touched_open
    bool is_untouched_setup
    bool line_mitigated
    box box_h1
    box box_h2
    line line_open

    type BreakoutSetup
    float setup_high
    int setup_time
    bool triggered

    var sessions = array.new<HourlySession>()
    var setups = array.new<BreakoutSetup>()
    var breakout_alert_fired_this_bar = false

    // —————————————————————————————————————————————————————————————————————————————
    // Data Fetching (Global Scope)
    // —————————————————————————————————————————————————————————————————————————————

    [time_30m, open_30m] = request.security(syminfo.tickerid, "30", [time, open], lookahead=barmerge.lookahead_off)
    [time_30m_p1, open_30m_p1, high_30m_p1, close_30m_p1] = request.security(syminfo.tickerid, "30", [time[1], open[1], high[1], close[1]], lookahead=barmerge.lookahead_off)

    // —————————————————————————————————————————————————————————————————————————————
    // Core Logic
    // —————————————————————————————————————————————————————————————————————————————

    breakout_alert_fired_this_bar := false
    ny_hour = hour(time, TZ)
    ny_minute = minute(time, TZ)
    is_first_half = ny_minute < 30
    is_second_half = not is_first_half
    is_new_hour_bar = ta.change(ny_hour)

    // When a new hour starts, finalize the previous session AND create the new one.
    if is_new_hour_bar
    // --- Part 1: Finalize the session that just ended ---
    if array.size(sessions) > 0
    completed_session = array.last(sessions)

    if not completed_session.second_half_touched_open
    completed_session.is_untouched_setup := true
    // Delete the old box and create a new pink box with priority
    if not na(completed_session.box_h1) and not na(completed_session.high_h1) and not na(completed_session.low_h1)
    start_time_h1 = completed_session.start_time
    end_time_h1 = start_time_h1 + 30 * 60 * 1000
    box.delete(completed_session.box_h1)
    // Create pink box with the stored high/low from first half
    completed_session.box_h1 := box.new(start_time_h1, completed_session.high_h1, end_time_h1, completed_session.low_h1, bgcolor=c_untouched_box, border_color=na, xloc=xloc.bar_time)
    log.info("✅ PINK BOX CREATED for untouched zone at {0}. High: {1}, Low: {2}, Hourly Open: {3}", str.format_time(start_time_h1, "HH:mm", TZ), completed_session.high_h1, completed_session.low_h1, completed_session.hourly_open)

    if close_30m_p1 > open_30m_p1
    array.push(setups, BreakoutSetup.new(high_30m_p1, time_30m_p1, false))
    else
    if not na(completed_session.line_open)
    time_h2_end = completed_session.start_time + 60 * 60 * 1000
    line.set_x2(completed_session.line_open, time_h2_end)

    // --- Part 2: Create the NEW session for the hour that is just starting ---
    new_hourly_open = open
    hour_start_ts = timestamp(TZ, year(time, TZ), month(time, TZ), dayofmonth(time, TZ), ny_hour, 0, 0)

    hourly_line = line.new(hour_start_ts, new_hourly_open, hour_start_ts + 60 * 60 * 1000, new_hourly_open, xloc=xloc.bar_time, color=c_hourly_line_base, style=f_get_line_style(s_hourly_line), width=w_hourly_line)
    array.push(sessions, HourlySession.new(hour_start_ts, new_hourly_open, na, na, na, na, false, false, false, na, na, hourly_line))


    // Bar-by-bar logic for drawing boxes dynamically
    if array.size(sessions) > 0
    current_session = array.last(sessions)
    if is_first_half
    if na(current_session.box_h1)
    // Create box on the first bar of the session
    start_time_h1 = current_session.start_time
    current_session.high_h1 := high
    current_session.low_h1 := low
    current_session.box_h1 := box.new(start_time_h1, high, time, low, bgcolor=c_box_a, border_color=na, xloc=xloc.bar_time)
    else
    // Update box on subsequent bars of the first half
    current_session.high_h1 := math.max(nz(current_session.high_h1), high)
    current_session.low_h1 := math.min(nz(current_session.low_h1, low), low)
    box.set_top(current_session.box_h1, current_session.high_h1)
    box.set_bottom(current_session.box_h1, current_session.low_h1)
    box.set_right(current_session.box_h1, time)
    else // is_second_half
    if na(current_session.box_h2)
    // Create box on the first bar of the second half
    start_time_h2 = current_session.start_time + 30 * 60 * 1000
    current_session.high_h2 := high
    current_session.low_h2 := low
    current_session.box_h2 := box.new(start_time_h2, high, time, low, bgcolor=c_box_b, border_color=na, xloc=xloc.bar_time)
    log.info("Second half started at {0}:{1}. Hourly open: {2}, Current High: {3}, Current Low: {4}", str.tostring(ny_hour, "00"), str.tostring(ny_minute, "00"), current_session.hourly_open, high, low)
    else
    // Update box on subsequent bars of the second half
    current_session.high_h2 := math.max(nz(current_session.high_h2), high)
    current_session.low_h2 := math.min(nz(current_session.low_h2, low), low)
    box.set_top(current_session.box_h2, current_session.high_h2)
    box.set_bottom(current_session.box_h2, current_session.low_h2)
    box.set_right(current_session.box_h2, time)

    // Check for touch condition
    if not current_session.second_half_touched_open
    touched = (high >= current_session.hourly_open and low <= current_session.hourly_open)
    if touched
    current_session.second_half_touched_open := true
    log.info("❌ Touch detected at {0}:{1}. High: {2}, Low: {3}, Hourly Open: {4}", str.tostring(ny_hour, "00"), str.tostring(ny_minute, "00"), high, low, current_session.hourly_open)


    // Extend/Mitigate hourly open lines
    for session_obj in sessions
    if session_obj.is_untouched_setup and not session_obj.line_mitigated
    // Check if current bar crosses the hourly open line
    if high >= session_obj.hourly_open and low <= session_obj.hourly_open
    session_obj.line_mitigated := true
    line.set_x2(session_obj.line_open, time)
    line.set_color(session_obj.line_open, color.new(c_hourly_line_base, 80))
    log.info("✂️ Line mitigated at {0}:{1} for hourly open at {2}", str.tostring(ny_hour, "00"), str.tostring(ny_minute, "00"), str.format_time(session_obj.start_time, "HH:mm", TZ))
    else
    line.set_x2(session_obj.line_open, time + 60 * 1000)

    // Check for breakout triggers
    if is_second_half and array.size(setups) > 0
    for setup in setups
    if not setup.triggered and high > setup.setup_high
    setup.triggered := true
    breakout_alert_fired_this_bar := true
    label.new(bar_index, setup.setup_high, s_breakout_symbol, style=label.style_label_up, color=c_breakout_label_base, textcolor=color.white, size=s_breakout_size, yloc=yloc.price, tooltip="Breakout of " + str.tostring(setup.setup_high, format.mintick) + " from " + str.format_time(setup.setup_time, "HH:mm", TZ))
    break

    // Draw half-hour line
    is_new_30m_bar = ta.change(time_30m)
    if is_new_30m_bar and is_second_half and not na(time_30m)
    line.new(time_30m, open_30m, time_30m + 30 * 60 * 1000, open_30m, xloc=xloc.bar_time, color=c_half_hour_line_base, style=f_get_line_style(s_half_hour_line), width=w_half_hour_line)

    // —————————————————————————————————————————————————————————————————————————————
    // Housekeeping & Alerts
    // —————————————————————————————————————————————————————————————————————————————

    if array.size(sessions) > 20
    old_session = array.shift(sessions)
    if not na(old_session.box_h1)
    box.delete(old_session.box_h1)
    if not na(old_session.box_h2)
    box.delete(old_session.box_h2)
    if not na(old_session.line_open)
    line.delete(old_session.line_open)
    if array.size(setups) > 20
    array.shift(setups)

    alertcondition(breakout_alert_fired_this_bar, "Hourly Open Untouched Zone Breakout", "Valid breakout of a setup high has occurred in the second half of the hour.")