//@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() var setups = array.new() 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.")