Created
October 13, 2025 18:31
-
-
Save dp21g/12e3073e551ef97cca2f2fd173b679b8 to your computer and use it in GitHub Desktop.
untouched opens
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| //@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.") |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment