Indicator · Pine Script v5 · On the chart

Clusterbreak

by Jferheart

This indicator identifies Break of Structure (BOS) and Market Structure Shift (MSS) zones, plotting automated bracket levels on the price chart.

View on GitHub

GitHub stars15on the repository
Pine versionv5runs as is
Lines of code918
Settings40inputs
LicenceNone statedfile: MPL-2.0
Last update4 weeks ago30 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

918 lines of Pine Script v5 from clusterbreak.pine, published under MPL-2.0 (file header). Keep the author's credit and licence lines when you reuse it.

clusterbreak.pine
// This source code is subject to the terms of the Mozilla Public License 2.0 at https://mozilla.org/MPL/2.0/
//
// Code is based on Multi Timeframe Break of Structure(BOS) & Market Structure Shift(MSS) from Lenny_Kiruthu
// Original Source: https://www.tradingview.com/script/1LrVbh1I-MTF-Break-of-Structure-BOS-Market-Structure-Shift-MSS/
//
// BRACKET RULES
//   SHORT: bearish MSS that directly follows a bullish zone (BOS or MSS), and the two are TOGETHER: the swing low
//          it breaks was created INSIDE that bullish zone, or within 'Touch tolerance' bars after it (the zones touch)
//          Entry = market order when the break bar closes (broken swing low = "1")
//          Stop  = broken level + Stop x zone height (1.0 = "0", the highest high inside the zone) or a fixed number of points
//          TP    = broken level - TP x zone height (1.0 = "2", one zone height below "1") or a fixed number of points
//   LONG : bullish MSS that directly follows a bearish zone (BOS or MSS) - mirror image
//          Stop  = broken level - Stop x zone height (1.0 = lowest low inside the zone) or fixed points
//          TP    = broken level + TP x zone height or fixed points
//   Stop mode '% of TP distance': stop distance = TP distance x Stop % (100 = a 1:1 stop, 50 = half of it, 70 = 70% of it).
//   Zone height = distance from the broken level to the far end of the zone (the "0" to "1" distance).
//   Set both multiples to 1.0 for the original 1:1 bracket.
//   EXIT: with 'Exit when an opposite structure break forms' on, a bearish break while long (or a bullish break while
//         short) closes the trade at market (closeposition). If a new opposite signal fires on that same bar, the new
//         entry goes out with flatten_first=true instead of a separate close.
//
// HTF STRUCTURE / LTF ENTRY (optional)
//   With 'Detect structure on a higher timeframe' on, BOS/MSS structure is detected on the timeframe set in 'Structure
//   timeframe' (e.g. 1m) via request.security, while the chart itself can be run on a lower timeframe (e.g. 30s) for a
//   tighter entry. Immediate mode enters at the close of the LTF bar the HTF break is confirmed on (roughly the same
//   price as running the script on the HTF alone, just up to one LTF bar sooner). Retrace mode waits for LTF price to
//   pull back a chosen % into the HTF zone before entering (a limit-style fill), and cancels if that retrace doesn't
//   happen within the bar timeout or if price hits the stop side first. Stop/TP/exit settings above are unchanged and
//   apply to whichever price this module fills at. Requires the chart's own timeframe to be lower than 'Structure
//   timeframe' - if it isn't, this has no effect.
//
// TRADE TIME WINDOWS
//   With 'Only trade inside time windows' on, new entries (and their webhook alerts) are only allowed while the bar is
//   inside an enabled window. Signals outside the window are still drawn but labelled 'skip: outside trade window'.
//   Exits are never blocked: stop, TP and the opposite-break exit keep working outside the windows.

// DAILY P&L TABLE
//   Tracks this script's own signals as virtual trades (entry = signal bar close, exit at stop/TP) and shows
//   trades, W-L, win % and P&L per trading day. The day rolls at 18:00 New York by default.
//
// WEBHOOK ALERTS
//   Stop/TP ride the entry order as a bracket (take_profit / stop_loss). Nothing else is ever sent for exits.
//   Create ONE TradingView alert on this indicator: condition = "Any alert() function call",
//   webhook URL = your webhook URL (Notifications tab). Recreate the alert after every script edit.

// VERSION HISTORY
//   v2.3  2026-09-30  Renamed to 'clusterbreak' (title/shorttitle only) to match the GitHub repo. No logic changes.
//   v2.2  2026-09-28  New defaults: swing length 8, quantity 3, day ends 16:00, Light chart theme, all 3 trade windows on
//                     (05:00-13:30, 13:30-15:30, 03:00-05:00 New York). No logic changes.
//   v2.1  2026-09-28  Fix: request.security can't sit inside an if/else in Pine v5. Now called unconditionally every bar
//                     (as Pine requires) and only used when 'Detect structure on a higher timeframe' is on.
//   v2.0  2026-09-28  Optional HTF structure / LTF entry: detect BOS/MSS on a higher timeframe (e.g. 1m) while running the
//                     script on a lower timeframe (e.g. 30s), and either enter immediately on the HTF break's close or wait
//                     for a retrace into the zone on the LTF candles for a better price. See 'HTF STRUCTURE / LTF ENTRY' below.
//   v1.4  2026-09-28  Saved as a separate script: 'BOS MSS Bracket Webhook'. Renamed; generic webhook wording in the name, settings and labels.
//                     The webhook message format itself is unchanged.
//   v1.4  2026-09-28  Price removed from the Stop/Entry/TP labels (optional 'Show price on Stop / Entry / TP labels' toggle, off by default).
//   v1.3  2026-09-28  New stop mode '% of TP distance' (50 = stop is half of a 1:1 stop, so 2:1 reward-to-risk; 70 = 70% of a 1:1 stop).
//   v1.2  2026-09-28  Adjustable stop and take profit (zone multiple or fixed points each), so it is no longer always 1:1.
//   v1.1  2026-09-28  Trade time windows: up to 3 windows, entries only inside them, optional close at window end, window shading.
//   v1.0  2026-09-28  First version-stamped build: BOS/MSS touching-zone bracket signals, opposite-break exit, daily P&L table,
//                     adjustable look, plus the new Settings table (for sharing) and White/Black chart background toggle.

//@version=5
indicator(title = "clusterbreak v2.3", shorttitle = "clusterbreak v2.3", overlay = true, max_labels_count = 500, max_lines_count = 500, max_polylines_count = 100, max_bars_back = 500)

// =====================================================================
// >>>>>>>>>>  PASTE YOUR WEBHOOK KEY BETWEEN THE QUOTES  <<<<<<<<<<
//   (hardcoded on purpose; a Settings-panel key gets wiped by the mobile UI.
//    A full-file re-paste of this script resets it, so paste the key back in.)
// =====================================================================
var string WEBHOOK_KEY = "PASTE_KEY_HERE"

// =====================================================================
// CONSTANTS
// =====================================================================
KEY_PLACEHOLDER    = "PASTE_KEY_HERE"
SCRIPT_VERSION     = "v2.3"   // keep in sync with the title and the version history above
Use_BOS_Plot       = true
Use_MSS_Plot       = true
MAX_LOOKBACK       = 500   // keep equal to max_bars_back in indicator() above

// =====================================================================
// INPUTS
// =====================================================================
General_Settings_group = "-------General Settings-------"
Fill_Zones_group       = "-------Fill Zones-------"
Swing_Settings_group   = "-------Swing Settings-------"
Trade_group            = "-------Bracket Trade Settings-------"
Webhook_group          = "-------Webhook-------"
Table_group            = "-------Daily P&L Table-------"
Cfg_group              = "-------Settings Table (for sharing)-------"
Window_group           = "-------Trade Time Windows-------"
HTF_group              = "-------HTF Structure / LTF Entry-------"

Swing_Length = input.int(defval = 8, title = "Swing Point Length", minval = 1, maxval = 50, group = Swing_Settings_group,
  tooltip = "Number of bars to the left and right used to identify a swing point. Lower values are more sensitive, higher values are more stable.")

Send_Webhook      = input.bool(defval = false, title = "Send Webhook Alerts", group = Webhook_group,
  tooltip = "OFF by default. Nothing is sent until this is turned on.")
Account           = input.string(defval = "", title = "Account", group = Webhook_group,
  tooltip = "Must exactly match the account name set up for your webhook receiver (case-sensitive).")
Auto_Instrument   = input.bool(defval = true, title = "Auto-Detect Instrument From Chart", group = Webhook_group,
  tooltip = "Converts a dated contract chart (e.g. MNQU2026) to NT8 short form (MNQU6). Does NOT work on a continuous symbol like MNQ1! - use the manual override there.")
Manual_Instrument = input.string(defval = "", title = "Manual Instrument Override", group = Webhook_group,
  tooltip = "If filled in, this is used instead of auto-detect. Enter the exact NT8 instrument name.")
Trade_Qty         = input.int(defval = 3, title = "Quantity", minval = 1, group = Webhook_group)
Reverse_On_Opposite = input.bool(defval = false, title = "Flatten & reverse on opposite signal", group = Webhook_group,
  tooltip = "OFF (default): a new signal is ignored while the previous trade is still open (in_trade guard). ON: an opposite signal sends flatten_first=true and reverses.")
Show_Status       = input.bool(defval = true, title = "Show status box", group = Webhook_group)

Show_PnL_Table  = input.bool(defval = true, title = "Show daily P&L table", group = Table_group)
Table_Days      = input.int(defval = 5, title = "Days to show", minval = 1, maxval = 20, group = Table_group)
Day_End_Hour    = input.int(defval = 16, title = "Trading day ends at (hour, 0-23)", minval = 0, maxval = 23, group = Table_group,
  tooltip = "18 = the day rolls over at 18:00 in the timezone below (CME futures day).")
Day_Timezone    = input.string(defval = "America/New_York", title = "Day timezone", group = Table_group,
  tooltip = "Timezone used for the day rollover, for example America/New_York or America/Chicago.")
Table_Pos_Input = input.string(defval = "Bottom Right", title = "Table position", options = ["Top Left", "Top Right", "Middle Left", "Middle Right", "Bottom Left", "Bottom Right"], group = Table_group)

Show_Cfg_Table = input.bool(defval = true, title = "Show settings table", group = Cfg_group,
  tooltip = "A compact table of the settings that shape the signals, so a screenshot of the chart shows exactly how it was set up. It never shows your account name or webhook key.")
Cfg_Pos_Input  = input.string(defval = "Top Left", title = "Settings table position", options = ["Top Left", "Top Right", "Middle Left", "Middle Right", "Bottom Left", "Bottom Right"], group = Cfg_group)

Use_Windows   = input.bool(defval = true, title = "Only trade inside time windows", group = Window_group,
  tooltip = "ON: new entries (and webhook alerts) are only allowed inside an enabled window below. OFF: trade all day. If this is ON and no window is enabled, nothing will trade. Exits are never blocked by the windows.")
Window_TZ     = input.string(defval = "America/New_York", title = "Window timezone", group = Window_group,
  tooltip = "Timezone the window times are read in, for example America/New_York or America/Chicago.")
W1_On         = input.bool(defval = true, title = "Window 1", group = Window_group, inline = "w1")
W1_Sess       = input.session(defval = "0500-1330", title = "", group = Window_group, inline = "w1")
W2_On         = input.bool(defval = true, title = "Window 2", group = Window_group, inline = "w2")
W2_Sess       = input.session(defval = "1330-1530", title = "", group = Window_group, inline = "w2")
W3_On         = input.bool(defval = true, title = "Window 3", group = Window_group, inline = "w3")
W3_Sess       = input.session(defval = "0300-0500", title = "", group = Window_group, inline = "w3")
Close_At_End  = input.bool(defval = false, title = "Close open trade when the window ends", group = Window_group,
  tooltip = "ON: a trade still open when the last enabled window closes is exited at market (closeposition, which also cancels its stop/TP). OFF: the trade stays on its stop/TP bracket.")
Shade_Windows = input.bool(defval = true, title = "Shade the windows on the chart", group = Window_group, inline = "shade")
Window_Shade  = input.color(defval = color.new(color.teal, 92), title = "", group = Window_group, inline = "shade")

Use_HTF_Entry = input.bool(defval = false, title = "Detect structure on a higher timeframe", group = HTF_group,
  tooltip = "OFF (default): structure is detected on the chart's own timeframe, as before. ON: structure is detected on 'Structure timeframe' below, and this module controls the entry. Run the chart itself on a LOWER timeframe than that (e.g. chart on 30s, structure timeframe 1m).")
HTF_Period    = input.timeframe(defval = "1", title = "Structure timeframe", group = HTF_group,
  tooltip = "The higher timeframe BOS/MSS structure is detected on, e.g. 1 (1 minute), 5, 15.")
Entry_Trigger = input.string(defval = "Retrace into zone (better price)", title = "Entry trigger", options = ["Immediate (HTF break close)", "Retrace into zone (better price)"], group = HTF_group,
  tooltip = "Immediate: enter at the close of the LTF bar the HTF break is confirmed on. Retrace: wait for LTF price to pull back into the HTF zone before entering.")
Retrace_Pct     = input.float(defval = 50.0, title = "Retrace % into the zone", minval = 1.0, maxval = 99.0, step = 5.0, group = HTF_group,
  tooltip = "How far back into the HTF zone price must pull back before entering, as a % of the zone height. 50 = the middle of the zone (same math as the 30/50/70 lines above). Only used in Retrace mode.")
Retrace_Timeout = input.int(defval = 40, title = "Retrace timeout (LTF bars)", minval = 1, group = HTF_group,
  tooltip = "If the retrace target isn't reached within this many bars of the chart's own timeframe, the setup is cancelled. Only used in Retrace mode.")
Show_Retrace_Line = input.bool(defval = true, title = "Show retrace target while waiting", group = HTF_group)

Trade_Shorts = input.bool(defval = true, title = "Trade shorts (bearish MSS after bullish MSS)", group = Trade_group)
Trade_Longs  = input.bool(defval = true, title = "Trade longs (bullish MSS after bearish MSS)", group = Trade_group)
Show_Bracket = input.bool(defval = true, title = "Draw Stop / Entry / TP levels", group = Trade_group)
Show_Bracket_Price = input.bool(defval = false, title = "Show price on Stop / Entry / TP labels", group = Trade_group,
  tooltip = "OFF (default): the labels just say Stop, Entry and TP. ON: the price is added after each name.")
Bracket_Bars = input.int(defval = 15, title = "Bracket line length (bars)", minval = 1, maxval = 100, group = Trade_group)
Touch_Tolerance = input.int(defval = 10, title = "Touch tolerance (bars)", minval = 0, maxval = 50, group = Trade_group,
  tooltip = "The swing level a reversal breaks must have formed inside the previous zone, or at most this many bars AFTER that zone broke (for example a pullback low right after a spike). 0 = strictly inside the previous zone. Bigger = more signals.")
Stop_Mode   = input.string(defval = "Zone multiple", title = "Stop loss mode", options = ["Zone multiple", "Fixed points", "% of TP distance"], group = Trade_group,
  tooltip = "Zone multiple: distance from the broken level = multiple x zone height. 1.0 puts the stop exactly at the far end of the zone (your '0' line). Fixed points: a set number of points from the broken level. % of TP distance: the stop is a percentage of however far the take profit is, so 100 = a 1:1 stop, 50 = half of a 1:1 stop (2:1 reward-to-risk), 70 = 70% of a 1:1 stop.")
Stop_Mult   = input.float(defval = 1.0, title = "Stop multiple (x zone height, used in Zone multiple mode)", minval = 0.1, step = 0.05, group = Trade_group)
Stop_Pct    = input.float(defval = 50.0, title = "Stop % of TP distance (used in % of TP distance mode)", minval = 5.0, maxval = 500.0, step = 5.0, group = Trade_group)
Stop_Points = input.float(defval = 20.0, title = "Stop fixed points", minval = 0.25, step = 0.25, group = Trade_group)
TP_Mode     = input.string(defval = "Zone multiple", title = "Take profit mode", options = ["Zone multiple", "Fixed points"], group = Trade_group,
  tooltip = "Zone multiple: distance from the broken level = multiple x zone height. 1.0 = your '2' line (1:1 with a 1.0 stop). 2.0 = twice the zone height. Fixed points: a set number of points from the broken level.")
TP_Mult     = input.float(defval = 1.0, title = "Take profit multiple (x zone height)", minval = 0.1, step = 0.05, group = Trade_group)
TP_Points   = input.float(defval = 40.0, title = "Take profit fixed points", minval = 0.25, step = 0.25, group = Trade_group)
Exit_On_Break = input.bool(defval = true, title = "Exit when an opposite structure break forms", group = Trade_group,
  tooltip = "While long, a bearish break (BOS or MSS) closes the trade at market; while short, a bullish break does. The stop/TP bracket still applies until then. Turn off to hold every trade to its stop or TP.")

Display_Pivots = input.bool(defval = false, title = "Display Swing Points", group = General_Settings_group, inline = "Swing Point")
Swing_Color    = input.color(defval = color.rgb(255, 255, 255, 50), title = "", group = General_Settings_group, inline = "Swing Point")

Plot_Entry_Levels = input.bool(defval = true, title = "Plot 30-50-70 Entry levels", group = General_Settings_group)
Entry_Text        = input.string(defval = "None", title = "Entry level text", options = ["None", "Percentage", "Price", "Both"], group = General_Settings_group)

Plot_30 = input.bool(defval = false, title = "Plot 30%", tooltip = "Show the 30% entry level line. Its text is controlled by 'Entry level text'.", group = General_Settings_group)
Plot_50 = input.bool(defval = true,  title = "Plot 50%", tooltip = "Show the 50% entry level line. Its text is controlled by 'Entry level text'.", group = General_Settings_group)
Plot_70 = input.bool(defval = false, title = "Plot 70%", tooltip = "Show the 70% entry level line. Its text is controlled by 'Entry level text'.", group = General_Settings_group)

Label_Size_Input = input.string(defval = "default", title = "Entry level text size", options = ["auto", "very small", "small", "default", "large", "very large"], group = General_Settings_group)

Fill_Bull_Zone1 = input.bool(defval = true,  title = "Fill Only Bullish Zone 1", group = Fill_Zones_group)
Fill_Bear_Zone1 = input.bool(defval = true,  title = "Fill Only Bearish Zone 1", group = Fill_Zones_group)
Fill_Bull_Zones = input.bool(defval = false, title = "Fill All Bullish Zones",   group = Fill_Zones_group)
Fill_Bear_Zones = input.bool(defval = false, title = "Fill All Bearish Zones",   group = Fill_Zones_group)
Color_Bull_Zone = input.color(defval = color.rgb(75, 130, 30, 50), title = "Bullish Zone 1 color", group = Fill_Zones_group, inline = "TF 1 Color")
Color_Bear_Zone = input.color(defval = color.rgb(130, 30, 30, 50), title = "Bearish Zone 1 color", group = Fill_Zones_group, inline = "TF 1 Color")

// ---------- LOOK: sizes and colors for chart text, signals and tables ----------
Look_group   = "-------Look: Theme-------"
Text_group   = "-------Look: Chart Text-------"
Signal_group = "-------Look: Signals-------"

Theme_Input = input.string(defval = "Light chart", title = "Theme preset", options = ["Dark chart", "Light chart", "Custom"], group = Look_group,
  tooltip = "Dark chart / Light chart apply readable text, line, swing-point and table colors for that chart background. Choose Custom to use the individual colors marked (Custom), including the swing point color in General Settings. Sizes, widths, bullish/bearish colors and label background always apply.")

BG_Mode = input.string(defval = "Off", title = "Chart background", options = ["Off", "White", "Black"], group = Look_group,
  tooltip = "Paints the chart background white or black behind the candles. White/Black also switches the text, line and table colors to match (unless Theme preset is Custom). Off leaves your chart background alone and uses the Theme preset.")

Bracket_Size_Input = input.string(defval = "small", title = "Stop/Entry/TP label size", options = ["tiny", "small", "normal", "large", "huge"], group = Text_group)
Swing_Size_Input   = input.string(defval = "small", title = "Swing label size (HH/HL/LH/LL)", options = ["tiny", "small", "normal", "large", "huge"], group = Text_group)
Show_Skips         = input.bool(defval = true, title = "Show why a signal was skipped", group = Text_group,
  tooltip = "Puts a small label on every MSS that did not become a trade, with the reason (no touch + gap in bars, previous trade still open, and so on).")
Skip_Size_Input    = input.string(defval = "tiny", title = "Skipped-signal label size", options = ["tiny", "small", "normal", "large", "huge"], group = Text_group)
Sig_Text_In        = input.color(defval = color.new(#26a69a, 0), title = "Stop/Entry/TP text color (Custom)", group = Text_group)
Lbl_BG             = input.color(defval = color.new(color.white, 100), title = "Label background color (all chart labels)", group = Text_group,
  tooltip = "Background box behind Stop/Entry/TP, 30-50-70 and swing labels. Fully transparent by default; make it opaque for a solid box behind the text.")

Bull_Color  = input.color(defval = color.green, title = "Bullish line / level color", group = Signal_group)
Bear_Color  = input.color(defval = color.red, title = "Bearish line / level color", group = Signal_group)
Line_Width  = input.int(defval = 1, title = "BOS / MSS line width", minval = 1, maxval = 5, group = Signal_group)
Sig_Line_In = input.color(defval = color.new(color.gray, 0), title = "Stop/Entry/TP line color (Custom)", group = Signal_group)
Sig_Width   = input.int(defval = 1, title = "Stop/Entry/TP line width", minval = 1, maxval = 5, group = Signal_group)

Tbl_Size_Input = input.string(defval = "small", title = "Table text size (P&L table + status box)", options = ["tiny", "small", "normal", "large", "huge"], group = Table_group)
Tbl_BG_In      = input.color(defval = color.new(color.black, 25), title = "Table background color (Custom)", group = Table_group)
Tbl_Head_In    = input.color(defval = color.new(color.white, 0), title = "Header / total text color (Custom)", group = Table_group)
Tbl_Text_In    = input.color(defval = color.new(color.white, 0), title = "Body text color (Custom)", group = Table_group)
Tbl_Frame_In   = input.color(defval = color.gray, title = "Frame / border color (Custom)", group = Table_group)
Tbl_Profit_In  = input.color(defval = color.lime, title = "Profit color (Custom)", group = Table_group)
Tbl_Loss_In    = input.color(defval = color.red, title = "Loss color (Custom)", group = Table_group)

Entry_Label_Size = switch Label_Size_Input
    "very small" => size.tiny
    "small"      => size.small
    "default"    => size.normal
    "large"      => size.large
    "very large" => size.huge
    "auto"       => size.auto

// =====================================================================
// RESOLVED LOOK (theme preset or Custom colors)
// =====================================================================
bool isDarkTheme  = Theme_Input != "Custom" and (BG_Mode == "Black" or (BG_Mode == "Off" and Theme_Input == "Dark chart"))
bool isLightTheme = Theme_Input != "Custom" and (BG_Mode == "White" or (BG_Mode == "Off" and Theme_Input == "Light chart"))

Tbl_BG     = isDarkTheme ? color.new(#000000, 25) : (isLightTheme ? color.new(#FFFFFF, 8)  : Tbl_BG_In)
Tbl_Head   = isDarkTheme ? color.new(#FFFFFF, 0)  : (isLightTheme ? color.new(#131722, 0)  : Tbl_Head_In)
Tbl_Text   = isDarkTheme ? color.new(#FFFFFF, 0)  : (isLightTheme ? color.new(#131722, 0)  : Tbl_Text_In)
Tbl_Frame  = isDarkTheme ? color.new(#787B86, 0)  : (isLightTheme ? color.new(#9598A1, 0)  : Tbl_Frame_In)
Tbl_Profit = isDarkTheme ? color.new(#00E676, 0)  : (isLightTheme ? color.new(#008A5B, 0)  : Tbl_Profit_In)
Tbl_Loss   = isDarkTheme ? color.new(#FF5252, 0)  : (isLightTheme ? color.new(#C62828, 0)  : Tbl_Loss_In)
Sig_Line   = isDarkTheme ? color.new(#787B86, 0)  : (isLightTheme ? color.new(#5D606B, 0)  : Sig_Line_In)
Sig_Text   = isDarkTheme ? color.new(#26A69A, 0)  : (isLightTheme ? color.new(#00695C, 0)  : Sig_Text_In)
Swing_Txt  = isDarkTheme ? color.new(#FFFFFF, 50) : (isLightTheme ? color.new(#131722, 30) : Swing_Color)

sizeFrom(string _s) =>
    switch _s
        "tiny"   => size.tiny
        "small"  => size.small
        "normal" => size.normal
        "large"  => size.large
        "huge"   => size.huge
        => size.small

Bracket_Size = sizeFrom(Bracket_Size_Input)
Swing_Size   = sizeFrom(Swing_Size_Input)
Tbl_Size     = sizeFrom(Tbl_Size_Input)
Skip_Size    = sizeFrom(Skip_Size_Input)

bgcolor(BG_Mode == "White" ? color.white : (BG_Mode == "Black" ? color.black : na), title = "Chart background")

// =====================================================================
// INSTRUMENT RESOLUTION (dated contract -> NT8 short form, e.g. MNQU2026 -> MNQU6)
// =====================================================================
string _root   = syminfo.root
string _tk     = syminfo.ticker
string _suffix = str.length(_tk) > str.length(_root) ? str.substring(_tk, str.length(_root)) : ""
string _mCode  = str.length(_suffix) >= 3 ? str.substring(_suffix, 0, 1) : ""
string _yrStr  = str.length(_suffix) >= 3 ? str.substring(_suffix, 1) : ""
bool   _monthOk = _mCode != "" and str.contains("FGHJKMNQUVXZ", _mCode)
bool   _yearOk  = _yrStr != "" and not na(str.tonumber(_yrStr)) and (str.length(_yrStr) == 4 or str.length(_yrStr) == 2)
string autoInstrument = (_monthOk and _yearOk) ? _root + _mCode + str.substring(_yrStr, str.length(_yrStr) - 1) : ""

// Manual override wins. Never falls back to a bare continuous ticker.
string instrument = Manual_Instrument != "" ? Manual_Instrument : (Auto_Instrument ? autoInstrument : "")
bool   canSend    = Send_Webhook and instrument != "" and Account != "" and WEBHOOK_KEY != KEY_PLACEHOLDER

// =====================================================================
// HTF ENGINE (pure - no drawing - so it can run inside request.security)
//   Detects the same touching-zone BOS/MSS signal as the main script, on whatever timeframe it is called with.
//   Returns: [sigLong, sigShort, lonLevel, lonZoneLow, shoLevel, shoZoneHigh, sigTime, hBrk, lBrk, breakTime]
//   sigLong/sigShort: a qualifying (touching-zone) signal fired on this call's bar.
//   lonLevel/lonZoneLow, shoLevel/shoZoneHigh: the broken level and the opposite extreme of that zone (for stop/TP/retrace math).
//   sigTime: time of a qualifying signal (na if none). hBrk/lBrk: any break, BOS or MSS. breakTime: time of any break (na if none).
// =====================================================================
f_htfEngine(int swingLen, int touchTol) =>
    var float pHigh    = na
    var float pLow     = na
    var int   pHighIdx = na
    var int   pLowIdx  = na
    var bool  hAct      = false
    var bool  lAct      = false
    var int   lastBrk   = 0
    var int   zStart    = na
    var int   zBreak    = na

    float sh = ta.pivothigh(high, swingLen, swingLen)
    float sl = ta.pivotlow(low, swingLen, swingLen)

    if not na(sh)
        pHigh    := sh
        pHighIdx := bar_index - swingLen
        hAct     := true
    if not na(sl)
        pLow    := sl
        pLowIdx := bar_index - swingLen
        lAct    := true

    bool hBrk = hAct and close > pHigh
    bool lBrk = lAct and close < pLow

    bool  sigL   = false
    bool  sigS   = false
    float lonLvl = na
    float lonLo  = na
    float shoLvl = na
    float shoHi  = na
    int   sTime  = na
    int   bTime  = na

    if hBrk
        hAct := false
        bTime := time
        bool isMSS = lastBrk == -1
        if isMSS and not na(zStart) and pHighIdx >= zStart and pHighIdx <= zBreak + touchTol and (bar_index - pHighIdx) < MAX_LOOKBACK
            float lowest = na
            for i = 0 to (bar_index - pHighIdx)
                lowest := na(lowest) ? low[i] : math.min(lowest, low[i])
            sigL   := true
            lonLvl := pHigh
            lonLo  := lowest
            sTime  := time
        zStart  := pHighIdx
        zBreak  := bar_index
        lastBrk := 1

    if lBrk
        lAct := false
        bTime := time
        bool isMSSb = lastBrk == 1
        if isMSSb and not na(zStart) and pLowIdx >= zStart and pLowIdx <= zBreak + touchTol and (bar_index - pLowIdx) < MAX_LOOKBACK
            float highest = na
            for i = 0 to (bar_index - pLowIdx)
                highest := na(highest) ? high[i] : math.max(highest, high[i])
            sigS   := true
            shoLvl := pLow
            shoHi  := highest
            sTime  := time
        zStart  := pLowIdx
        zBreak  := bar_index
        lastBrk := -1

    [sigL, sigS, lonLvl, lonLo, shoLvl, shoHi, sTime, hBrk, lBrk, bTime]

// =====================================================================
// HELPERS
// =====================================================================
// Draws one entry level (short line + optional text) starting at the current (break) bar.
drawEntryLevel(float _price, string _pct, bool _show, color _col) =>
    if _show
        line.new(bar_index, _price, bar_index + 5, _price, color = _col)
        string _txt = switch Entry_Text
            "Percentage" => _pct
            "Price"      => str.tostring(_price, format.mintick)
            "Both"       => _pct + " (" + str.tostring(_price, format.mintick) + ")"
            => ""
        if _txt != ""
            label.new(bar_index + 6, _price, text = _txt, color = Lbl_BG, style = label.style_label_left, textcolor = _col, textalign = text.align_left, size = Entry_Label_Size)

// Draws one bracket level (line + text) starting at the current (break) bar. Local visual only.
drawBracketLevel(float _price, string _name) =>
    line.new(bar_index, _price, bar_index + Bracket_Bars, _price, color = Sig_Line, width = Sig_Width)
    label.new(bar_index + Bracket_Bars + 1, _price, text = Show_Bracket_Price ? _name + "  " + str.tostring(_price, format.mintick) : _name, color = Lbl_BG, style = label.style_label_left, textcolor = Sig_Text, textalign = text.align_left, size = Bracket_Size)

// =====================================================================
// STATE
// =====================================================================
var float prevHigh    = na     // most recent swing high level
var float prevLow     = na     // most recent swing low level
var int   prevHighIdx = na     // bar_index of that swing high
var int   prevLowIdx  = na     // bar_index of that swing low
var bool  highActive  = false  // swing high not yet broken
var bool  lowActive   = false  // swing low not yet broken
var int   lastBreak   = 0      // 1 = last break was bullish, -1 = bearish, 0 = none yet
var int   lastZoneStartIdx = na  // left edge of the previous break's zone, BOS or MSS (bar_index of the swing it broke)
var int   lastZoneBreakIdx = na  // right edge of the previous break's zone (bar_index of that break)

// Virtual trade (local in_trade guard: assumes the bracket fills at the levels; never sends anything)
var bool  inTrade      = false
var int   tradeDir     = 0
var float tradeEntry   = na
var float tradeStop    = na
var float tradeTP      = na
var int   lastFlattenDay = 0   // day-key of the last entry that carried flatten_first

// Trade signals for the current bar (filled in by the break blocks below)
bool  goShort    = false
bool  goLong     = false
float shortStop  = na
float shortTP    = na
float shortLevel = na
float longStop   = na
float longTP     = na
float longLevel  = na
string skipWhy   = ""   // reason an MSS did not become a trade (drawn as a small label)
float fillPrice  = close   // actual entry price used for tradeEntry; overridden by the HTF/retrace path when it fills

// HTF structure / LTF entry pending state
var bool  pending        = false
var int   pendDir        = 0
var float pendStop       = na
var float pendTP         = na
var float pendLevel      = na
var float pendTarget     = na
var int   pendBars       = 0
var int   lastSigTime    = na    // dedupes qualifying HTF signals
var int   lastBreakTime  = na    // dedupes any HTF break, for highBroken/lowBroken edges and the opposite-break exit

// =====================================================================
// DAILY STATS (one row per trading day; the day rolls at Day_End_Hour in Day_Timezone)
// =====================================================================
var array<int>   dKey    = array.new<int>()
var array<int>   dTrades = array.new<int>()
var array<int>   dWins   = array.new<int>()
var array<int>   dLosses = array.new<int>()
var array<float> dPts    = array.new<float>()

// Adds one closed virtual trade (result in points) to the current trading day.
recordTrade(float _pts) =>
    int _n = array.size(dKey) - 1
    array.set(dTrades, _n, array.get(dTrades, _n) + 1)
    if _pts > 0
        array.set(dWins, _n, array.get(dWins, _n) + 1)
    else if _pts < 0
        array.set(dLosses, _n, array.get(dLosses, _n) + 1)
    array.set(dPts, _n, array.get(dPts, _n) + _pts)

// Shift time so the day boundary lands on midnight, then read the date in the chosen timezone.
int shiftedTime = time + (24 - Day_End_Hour) * 3600000
int tradingDay  = year(shiftedTime, Day_Timezone) * 10000 + month(shiftedTime, Day_Timezone) * 100 + dayofmonth(shiftedTime, Day_Timezone)

bool isNewDay = array.size(dKey) == 0 ? true : array.get(dKey, array.size(dKey) - 1) != tradingDay
if isNewDay
    array.push(dKey, tradingDay)
    array.push(dTrades, 0)
    array.push(dWins, 0)
    array.push(dLosses, 0)
    array.push(dPts, 0.0)

// =====================================================================
// SWING DETECTION
// =====================================================================
// request.security cannot sit inside an if/else in Pine v5, so this runs every bar regardless of
// Use_HTF_Entry; the result is only used below when that setting is on.
[htfSigL, htfSigS, htfLonLvl, htfLonLo, htfShoLvl, htfShoHi, htfSigTime, htfHBrk, htfLBrk, htfBreakTime] =
  request.security(syminfo.tickerid, HTF_Period, f_htfEngine(Swing_Length, Touch_Tolerance), gaps = barmerge.gaps_off, lookahead = barmerge.lookahead_off)

bool highBroken = false
bool lowBroken  = false
if not Use_HTF_Entry
    swingHigh = ta.pivothigh(high, Swing_Length, Swing_Length)
    swingLow  = ta.pivotlow(low, Swing_Length, Swing_Length)

    bool HH = false
    bool LH = false
    bool HL = false
    bool LL = false

    if not na(swingHigh)
        if not na(prevHigh)          // first swing has nothing to compare against
            if swingHigh >= prevHigh
                HH := true
            else
                LH := true
        prevHigh    := swingHigh
        prevHighIdx := bar_index - Swing_Length
        highActive  := true

    if not na(swingLow)
        if not na(prevLow)
            if swingLow >= prevLow
                HL := true
            else
                LL := true
        prevLow    := swingLow
        prevLowIdx := bar_index - Swing_Length
        lowActive  := true

    if Display_Pivots
        if HH
            label.new(prevHighIdx, prevHigh, "HH", color = Lbl_BG, style = label.style_label_down, textcolor = Swing_Txt, size = Swing_Size)
        if LH
            label.new(prevHighIdx, prevHigh, "LH", color = Lbl_BG, style = label.style_label_down, textcolor = Swing_Txt, size = Swing_Size)
        if HL
            label.new(prevLowIdx, prevLow, "HL", color = Lbl_BG, style = label.style_label_up, textcolor = Swing_Txt, size = Swing_Size)
        if LL
            label.new(prevLowIdx, prevLow, "LL", color = Lbl_BG, style = label.style_label_up, textcolor = Swing_Txt, size = Swing_Size)

    // =====================================================================
    // BREAKS (close beyond the swing level)
    // =====================================================================
    highBroken := highActive and close > prevHigh
    lowBroken  := lowActive and close < prevLow

    // ---------- Bullish break: BOS if last break was bullish, MSS if it was bearish ----------
    if highBroken
        highActive := false
        bool isMSS = lastBreak == -1
        bool isBOS = lastBreak == 1
        if (isBOS and Use_BOS_Plot) or (isMSS and Use_MSS_Plot)
            line.new(prevHighIdx, prevHigh, bar_index, prevHigh, color = Bull_Color, style = line.style_dashed, width = Line_Width)

            int cnt = bar_index - prevHighIdx
            if cnt < MAX_LOOKBACK   // zone needs history access; skip the zone if the swing is older than the buffer
                float lowest = na
                array<chart.point> pts = array.new<chart.point>()
                for i = 0 to cnt
                    if i > 0
                        pts.push(chart.point.from_index(bar_index - i, math.max(open[i], close[i])))
                    lowest := na(lowest) ? low[i] : math.min(lowest, low[i])
                pts.push(chart.point.from_index(prevHighIdx, prevHigh))
                pts.push(chart.point.from_index(bar_index, prevHigh))

                if Fill_Bull_Zones or (isMSS and Fill_Bull_Zone1)
                    polyline.new(pts, curved = false, closed = true, line_color = Color_Bull_Zone, fill_color = Color_Bull_Zone, xloc = xloc.bar_index)

                if isMSS and Plot_Entry_Levels
                    float rng = prevHigh - lowest
                    drawEntryLevel(prevHigh - rng * 0.30, "30%", Plot_30, Bull_Color)
                    drawEntryLevel(prevHigh - rng * 0.50, "50%", Plot_50, Bull_Color)
                    drawEntryLevel(prevHigh - rng * 0.70, "70%", Plot_70, Bull_Color)

                // LONG signal: bullish MSS, and the swing high it breaks was created inside the previous (bearish) zone, BOS or MSS,
                // or at most Touch_Tolerance bars after that zone broke
                if isMSS
                    if na(lastZoneStartIdx)
                        skipWhy := "skip: no prior zone"
                    else if prevHighIdx < lastZoneStartIdx
                        skipWhy := "skip: swing older than prior zone"
                    else if prevHighIdx > lastZoneBreakIdx + Touch_Tolerance
                        skipWhy := "skip: no touch (gap " + str.tostring(prevHighIdx - lastZoneBreakIdx) + " bars)"
                    else
                        float zoneH = prevHigh - lowest
                        float tpDistL   = TP_Mode == "Fixed points" ? TP_Points : zoneH * TP_Mult
                        float stopDistL = Stop_Mode == "Fixed points" ? Stop_Points : (Stop_Mode == "% of TP distance" ? tpDistL * Stop_Pct / 100.0 : zoneH * Stop_Mult)
                        float stopL = math.round_to_mintick(prevHigh - stopDistL)
                        float tpL   = math.round_to_mintick(prevHigh + tpDistL)
                        if zoneH > syminfo.mintick and close < tpL and close > stopL
                            goLong    := true
                            longStop  := stopL
                            longTP    := tpL
                            longLevel := prevHigh
                        else
                            skipWhy := close >= tpL ? "skip: price already past TP" : (close <= stopL ? "skip: price already past stop" : "skip: zone too small")
            else if isMSS
                skipWhy := "skip: swing too old"
        lastZoneStartIdx := prevHighIdx
        lastZoneBreakIdx := bar_index
        lastBreak := 1

    // ---------- Bearish break: BOS if last break was bearish, MSS if it was bullish ----------
    if lowBroken
        lowActive := false
        bool isMSSb = lastBreak == 1
        bool isBOSb = lastBreak == -1
        if (isBOSb and Use_BOS_Plot) or (isMSSb and Use_MSS_Plot)
            line.new(prevLowIdx, prevLow, bar_index, prevLow, color = Bear_Color, style = line.style_dashed, width = Line_Width)

            int cntb = bar_index - prevLowIdx
            if cntb < MAX_LOOKBACK
                float highest = na
                array<chart.point> ptsb = array.new<chart.point>()
                for i = 0 to cntb
                    if i > 0
                        ptsb.push(chart.point.from_index(bar_index - i, math.min(open[i], close[i])))
                    highest := na(highest) ? high[i] : math.max(highest, high[i])
                ptsb.push(chart.point.from_index(prevLowIdx, prevLow))
                ptsb.push(chart.point.from_index(bar_index, prevLow))

                if Fill_Bear_Zones or (isMSSb and Fill_Bear_Zone1)
                    polyline.new(ptsb, curved = false, closed = true, line_color = Color_Bear_Zone, fill_color = Color_Bear_Zone, xloc = xloc.bar_index)

                if isMSSb and Plot_Entry_Levels
                    float rngb = highest - prevLow
                    drawEntryLevel(prevLow + rngb * 0.30, "30%", Plot_30, Bear_Color)
                    drawEntryLevel(prevLow + rngb * 0.50, "50%", Plot_50, Bear_Color)
                    drawEntryLevel(prevLow + rngb * 0.70, "70%", Plot_70, Bear_Color)

                // SHORT signal: bearish MSS, and the swing low it breaks was created inside the previous (bullish) zone, BOS or MSS,
                // or at most Touch_Tolerance bars after that zone broke
                if isMSSb
                    if na(lastZoneStartIdx)
                        skipWhy := "skip: no prior zone"
                    else if prevLowIdx < lastZoneStartIdx
                        skipWhy := "skip: swing older than prior zone"
                    else if prevLowIdx > lastZoneBreakIdx + Touch_Tolerance
                        skipWhy := "skip: no touch (gap " + str.tostring(prevLowIdx - lastZoneBreakIdx) + " bars)"
                    else
                        float zoneHb = highest - prevLow
                        float tpDistS   = TP_Mode == "Fixed points" ? TP_Points : zoneHb * TP_Mult
                        float stopDistS = Stop_Mode == "Fixed points" ? Stop_Points : (Stop_Mode == "% of TP distance" ? tpDistS * Stop_Pct / 100.0 : zoneHb * Stop_Mult)
                        float stopS  = math.round_to_mintick(prevLow + stopDistS)
                        float tpS    = math.round_to_mintick(prevLow - tpDistS)
                        if zoneHb > syminfo.mintick and close > tpS and close < stopS
                            goShort    := true
                            shortStop  := stopS
                            shortTP    := tpS
                            shortLevel := prevLow
                        else
                            skipWhy := close <= tpS ? "skip: price already past TP" : (close >= stopS ? "skip: price already past stop" : "skip: zone too small")
            else if isMSSb
                skipWhy := "skip: swing too old"
        lastZoneStartIdx := prevLowIdx
        lastZoneBreakIdx := bar_index
        lastBreak := -1

else
    // highBroken/lowBroken fire once per NEW higher-timeframe break (not held true for every LTF bar inside it)
    bool htfNewBreak = not na(htfBreakTime) and htfBreakTime != lastBreakTime
    if htfNewBreak
        lastBreakTime := htfBreakTime
    highBroken := htfNewBreak and htfHBrk
    lowBroken  := htfNewBreak and htfLBrk

    // Arm a new pending setup (or fill immediately) on a fresh qualifying HTF signal
    if not na(htfSigTime) and htfSigTime != lastSigTime and (htfSigL or htfSigS)
        lastSigTime := htfSigTime
        int   dir   = htfSigL ? 1 : -1
        float lvl   = htfSigL ? htfLonLvl : htfShoLvl
        float zoneH = htfSigL ? htfLonLvl - htfLonLo : htfShoHi - htfShoLvl
        if zoneH > syminfo.mintick
            float tpDist   = TP_Mode == "Fixed points" ? TP_Points : zoneH * TP_Mult
            float stopDist = Stop_Mode == "Fixed points" ? Stop_Points : (Stop_Mode == "% of TP distance" ? tpDist * Stop_Pct / 100.0 : zoneH * Stop_Mult)
            float stopP    = math.round_to_mintick(dir == 1 ? lvl - stopDist : lvl + stopDist)
            float tpP      = math.round_to_mintick(dir == 1 ? lvl + tpDist   : lvl - tpDist)
            if Entry_Trigger == "Immediate (HTF break close)"
                fillPrice := close
                if dir == 1
                    goLong    := true
                    longStop  := stopP
                    longTP    := tpP
                    longLevel := lvl
                else
                    goShort    := true
                    shortStop  := stopP
                    shortTP    := tpP
                    shortLevel := lvl
            else
                pending    := true
                pendDir    := dir
                pendLevel  := lvl
                pendStop   := stopP
                pendTP     := tpP
                pendTarget := math.round_to_mintick(dir == 1 ? lvl - zoneH * Retrace_Pct / 100.0 : lvl + zoneH * Retrace_Pct / 100.0)
                pendBars   := 0
        else
            skipWhy := "skip: HTF zone too small"

    // Manage a pending retrace: fill, invalidate (stop side hit first), or time out
    if pending
        bool reached = pendDir == 1 ? (low <= pendTarget) : (high >= pendTarget)
        bool blownBy = pendDir == 1 ? (low <= pendStop)   : (high >= pendStop)
        if blownBy
            pending := false
            skipWhy := "skip: invalidated before entry"
        else if reached
            pending   := false
            fillPrice := pendTarget
            if pendDir == 1
                goLong    := true
                longStop  := pendStop
                longTP    := pendTP
                longLevel := pendLevel
            else
                goShort    := true
                shortStop  := pendStop
                shortTP    := pendTP
                shortLevel := pendLevel
        else
            pendBars += 1
            if pendBars > Retrace_Timeout
                pending := false
                skipWhy := "skip: retrace timeout"

    if Show_Retrace_Line and pending
        label.new(bar_index, pendTarget, "Retrace target", color = Lbl_BG, style = pendDir == 1 ? label.style_label_up : label.style_label_down, textcolor = Sig_Text, size = Skip_Size)

// =====================================================================
// VIRTUAL TRADE TRACKING (closes the local in_trade flag when stop or TP is touched)
// =====================================================================
bool w1Now     = W1_On and not na(time(timeframe.period, W1_Sess, Window_TZ))
bool w2Now     = W2_On and not na(time(timeframe.period, W2_Sess, Window_TZ))
bool w3Now     = W3_On and not na(time(timeframe.period, W3_Sess, Window_TZ))
bool inWindow  = (not Use_Windows) or w1Now or w2Now or w3Now
bool windowEnded = Use_Windows and inWindow[1] and not inWindow

bgcolor(Shade_Windows and Use_Windows and inWindow ? Window_Shade : na, title = "Trade windows")

bool exitedOnBreak = false   // true on the bar where an opposite structure break closed the trade
bool exitedAtWindowEnd = false   // true on the bar where the trade window closing exited the trade

if inTrade
    bool stopHit = tradeDir == -1 ? high >= tradeStop : low <= tradeStop
    bool tpHit   = tradeDir == -1 ? low <= tradeTP : high >= tradeTP
    if stopHit or tpHit
        float exitPx = stopHit ? tradeStop : tradeTP   // if both are touched in one bar, assume the stop came first
        recordTrade((exitPx - tradeEntry) * tradeDir)
        inTrade := false
    else if Exit_On_Break and ((tradeDir == 1 and lowBroken) or (tradeDir == -1 and highBroken))
        float brkPts = (close - tradeEntry) * tradeDir   // exit at the close of the break bar
        recordTrade(brkPts)
        exitedOnBreak := true
        inTrade := false
        if Show_Bracket
            string exitTxt = "Exit (opposite break) " + (brkPts > 0 ? "+" : "") + str.tostring(brkPts, "0.00")
            if tradeDir == 1
                label.new(bar_index, low, exitTxt, color = Lbl_BG, style = label.style_label_up, textcolor = Sig_Text, size = Bracket_Size)
            else
                label.new(bar_index, high, exitTxt, color = Lbl_BG, style = label.style_label_down, textcolor = Sig_Text, size = Bracket_Size)
    else if Close_At_End and windowEnded
        float endPts = (close - tradeEntry) * tradeDir   // exit at the close of the first bar outside the window
        recordTrade(endPts)
        exitedAtWindowEnd := true
        inTrade := false
        if Show_Bracket
            string endTxt = "Exit (window end) " + (endPts > 0 ? "+" : "") + str.tostring(endPts, "0.00")
            if tradeDir == 1
                label.new(bar_index, low, endTxt, color = Lbl_BG, style = label.style_label_up, textcolor = Sig_Text, size = Bracket_Size)
            else
                label.new(bar_index, high, endTxt, color = Lbl_BG, style = label.style_label_down, textcolor = Sig_Text, size = Bracket_Size)

// =====================================================================
// ENTRY + WEBHOOK (bracket rides the entry order only)
// =====================================================================
int  sigDir     = (goShort and Trade_Shorts) ? -1 : ((goLong and Trade_Longs) ? 1 : 0)
bool isReversal = inTrade and sigDir != 0 and sigDir != tradeDir
bool takeIt     = sigDir != 0 and inWindow and ((not inTrade) or (isReversal and Reverse_On_Opposite))

if goShort and not Trade_Shorts
    skipWhy := "skip: shorts are off"
if goLong and not Trade_Longs
    skipWhy := "skip: longs are off"
if sigDir != 0 and not takeIt
    skipWhy := inWindow ? "skip: previous trade still open" : "skip: outside trade window"

if takeIt
    float sStop  = sigDir == -1 ? shortStop  : longStop
    float sTP    = sigDir == -1 ? shortTP    : longTP
    float sLevel = sigDir == -1 ? shortLevel : longLevel

    if Show_Bracket
        drawBracketLevel(sStop, "Stop")
        drawBracketLevel(sLevel, "Entry")
        drawBracketLevel(sTP, "TP")

    if canSend
        int  dayKey     = year * 10000 + month * 100 + dayofmonth
        bool firstOfDay = dayKey != lastFlattenDay
        bool flatten    = firstOfDay or isReversal or exitedOnBreak
        string payload  = "key=" + WEBHOOK_KEY + ";command=place;account=" + Account + ";instrument=" + instrument + ";action=" + (sigDir == -1 ? "SELL" : "BUY") + ";qty=" + str.tostring(Trade_Qty) + ";order_type=market;tif=day;take_profit=" + str.tostring(sTP, format.mintick) + ";stop_loss=" + str.tostring(sStop, format.mintick) + ";" + (flatten ? "flatten_first=true;" : "")
        alert(payload, alert.freq_once_per_bar_close)
        if firstOfDay
            lastFlattenDay := dayKey

    if isReversal
        recordTrade((close - tradeEntry) * tradeDir)   // old trade is flattened at market
    inTrade    := true
    tradeDir   := sigDir
    tradeEntry := fillPrice
    tradeStop  := sStop
    tradeTP    := sTP

// =====================================================================
// EXIT ALERT: opposite-break exit or window-end exit (only when no new trade is entered on the same bar)
//   CLOSEPOSITION flattens the position and cancels the resting stop/TP orders.
// =====================================================================
if (exitedOnBreak or exitedAtWindowEnd) and not takeIt and canSend
    alert("key=" + WEBHOOK_KEY + ";command=closeposition;account=" + Account + ";instrument=" + instrument + ";", alert.freq_once_per_bar_close)

// =====================================================================
// SKIPPED-SIGNAL REASONS (answers "why was this not marked?")
// =====================================================================
if Show_Skips and skipWhy != ""
    if highBroken
        label.new(bar_index, low, skipWhy, color = Lbl_BG, style = label.style_label_up, textcolor = Sig_Text, size = Skip_Size)
    else
        label.new(bar_index, high, skipWhy, color = Lbl_BG, style = label.style_label_down, textcolor = Sig_Text, size = Skip_Size)

// =====================================================================
// STATUS BOX (spot-check against NT8 before turning the webhook on)
// =====================================================================
var table statusTbl = table.new(position.top_right, 1, 3, bgcolor = Tbl_BG, frame_color = Tbl_Frame, frame_width = 1)
if Show_Status and barstate.islast
    table.cell(statusTbl, 0, 0, "Webhook: " + (Send_Webhook ? (canSend ? "ON" : "ON (incomplete setup)") : "OFF"), text_color = Send_Webhook ? (canSend ? Tbl_Profit : color.orange) : color.gray, text_size = Tbl_Size)
    table.cell(statusTbl, 0, 1, "Acct: " + (Account == "" ? "(not set)" : Account), text_color = Tbl_Text, text_size = Tbl_Size)
    table.cell(statusTbl, 0, 2, "Instr: " + (instrument == "" ? "(not resolved)" : instrument), text_color = Tbl_Text, text_size = Tbl_Size)

// =====================================================================
// SETTINGS TABLE (screenshot-friendly summary; never shows the account name or webhook key)
// =====================================================================
Cfg_Pos = switch Cfg_Pos_Input
    "Top Left"     => position.top_left
    "Top Right"    => position.top_right
    "Middle Left"  => position.middle_left
    "Middle Right" => position.middle_right
    "Bottom Left"  => position.bottom_left
    "Bottom Right" => position.bottom_right

var table cfgTbl = table.new(Cfg_Pos, 2, 15, bgcolor = Tbl_BG, frame_color = Tbl_Frame, frame_width = 1, border_color = color.new(Tbl_Frame, 60), border_width = 1)

onOff(bool _b) =>
    _b ? "On" : "Off"

cfgRow(int _r, string _k, string _v) =>
    table.cell(cfgTbl, 0, _r, _k, text_color = Tbl_Text, text_size = Tbl_Size, text_halign = text.align_left)
    table.cell(cfgTbl, 1, _r, _v, text_color = Tbl_Text, text_size = Tbl_Size, text_halign = text.align_left)

if Show_Cfg_Table and barstate.islast
    table.cell(cfgTbl, 0, 0, "BOS MSS Bracket", text_color = Tbl_Head, text_size = Tbl_Size, text_halign = text.align_left)
    table.cell(cfgTbl, 1, 0, SCRIPT_VERSION, text_color = Tbl_Head, text_size = Tbl_Size, text_halign = text.align_left)
    cfgRow(1,  "Symbol / TF",         syminfo.ticker + " / " + timeframe.period)
    cfgRow(2,  "Swing length",        str.tostring(Swing_Length))
    cfgRow(3,  "Touch tolerance",     str.tostring(Touch_Tolerance) + " bars")
    cfgRow(4,  "Exit on opp. break",  onOff(Exit_On_Break))
    cfgRow(5,  "Shorts / Longs",      onOff(Trade_Shorts) + " / " + onOff(Trade_Longs))
    cfgRow(6,  "Quantity",            str.tostring(Trade_Qty))
    cfgRow(7,  "Reverse on opposite", onOff(Reverse_On_Opposite))
    string stopTxt = Stop_Mode == "Fixed points" ? str.tostring(Stop_Points, "0.0#") + " pts" : (Stop_Mode == "% of TP distance" ? str.tostring(Stop_Pct, "0.#") + "% of TP" : str.tostring(Stop_Mult, "0.0#") + "x zone")
    string tpTxt   = TP_Mode == "Fixed points" ? str.tostring(TP_Points, "0.0#") + " pts" : str.tostring(TP_Mult, "0.0#") + "x zone"
    string rrTxt   = Stop_Mode == "% of TP distance" ? " (" + str.tostring(100.0 / Stop_Pct, "0.0#") + "R)" : (Stop_Mode == TP_Mode ? " (" + str.tostring(Stop_Mode == "Fixed points" ? TP_Points / Stop_Points : TP_Mult / Stop_Mult, "0.0#") + "R)" : "")
    cfgRow(8,  "Stop / TP",           stopTxt + " / " + tpTxt + rrTxt)
    cfgRow(9,  "Day ends",            str.tostring(Day_End_Hour, "00") + ":00 " + Day_Timezone)
    cfgRow(10, "Webhook",             onOff(Send_Webhook))
    cfgRow(11, "Background",          BG_Mode == "Off" ? Theme_Input : BG_Mode)
    cfgRow(12, "P&L basis",           "Virtual trades (not fills)")
    string winTxt = not Use_Windows ? "Off (all day)" : ((W1_On or W2_On or W3_On) ? ((W1_On ? W1_Sess + " " : "") + (W2_On ? W2_Sess + " " : "") + (W3_On ? W3_Sess + " " : "") + Window_TZ + (Close_At_End ? ", close at end" : "")) : "none enabled (no trades)")
    cfgRow(13, "Trade windows",       winTxt)
    cfgRow(14, "Structure / Entry",   Use_HTF_Entry ? (HTF_Period + " structure, " + (Entry_Trigger == "Immediate (HTF break close)" ? "immediate entry" : str.tostring(Retrace_Pct, "0") + "% retrace entry")) : "Chart timeframe only")

// =====================================================================
// DAILY P&L TABLE
//   Built from this script's own virtual trades, NOT from broker fills: entry = close of the signal bar,
//   exits at exactly the stop/TP price, no commissions or slippage, stop wins a same-bar tie.
//   A trade counts on the day it CLOSES. $ = points x point value of the symbol x quantity.
// =====================================================================
Table_Pos = switch Table_Pos_Input
    "Top Left"     => position.top_left
    "Top Right"    => position.top_right
    "Middle Left"  => position.middle_left
    "Middle Right" => position.middle_right
    "Bottom Left"  => position.bottom_left
    "Bottom Right" => position.bottom_right

var table pnlTbl = table.new(Table_Pos, 6, Table_Days + 2, bgcolor = Tbl_BG, frame_color = Tbl_Frame, frame_width = 1, border_color = color.new(Tbl_Frame, 60), border_width = 1)

dayLabel(int _key) =>
    int _m = int((_key % 10000) / 100)
    int _d = _key % 100
    str.tostring(_m, "00") + "/" + str.tostring(_d, "00")

fmtPts(float _x) =>
    (_x > 0 ? "+" : "") + str.tostring(_x, "0.00")

fmtUsd(float _x) =>
    (_x > 0 ? "+$" : (_x < 0 ? "-$" : "$")) + str.tostring(math.abs(_x), "0.00")

fmtWinPct(int _w, int _l) =>
    (_w + _l) > 0 ? str.tostring(100.0 * _w / (_w + _l), "0") + "%" : "-"

pnlColor(float _x) =>
    _x > 0 ? Tbl_Profit : (_x < 0 ? Tbl_Loss : Tbl_Text)

if Show_PnL_Table and barstate.islast
    color hdrCol = Tbl_Head
    table.cell(pnlTbl, 0, 0, "Day",     text_color = hdrCol, text_size = Tbl_Size, text_halign = text.align_left)
    table.cell(pnlTbl, 1, 0, "Trades",  text_color = hdrCol, text_size = Tbl_Size)
    table.cell(pnlTbl, 2, 0, "W-L",     text_color = hdrCol, text_size = Tbl_Size)
    table.cell(pnlTbl, 3, 0, "Win %",   text_color = hdrCol, text_size = Tbl_Size)
    table.cell(pnlTbl, 4, 0, "P&L pts", text_color = hdrCol, text_size = Tbl_Size)
    table.cell(pnlTbl, 5, 0, "P&L $",   text_color = hdrCol, text_size = Tbl_Size)

    int   nDays = array.size(dKey)
    int   shown = math.min(nDays, Table_Days)
    int   tTr   = 0
    int   tW    = 0
    int   tL    = 0
    float tP    = 0.0
    if shown > 0
        for k = 0 to shown - 1
            int   idx = nDays - 1 - k
            int   trd = array.get(dTrades, idx)
            int   win = array.get(dWins, idx)
            int   los = array.get(dLosses, idx)
            float pts = array.get(dPts, idx)
            float usd = pts * syminfo.pointvalue * Trade_Qty
            color pc  = pnlColor(pts)
            table.cell(pnlTbl, 0, k + 1, dayLabel(array.get(dKey, idx)), text_color = Tbl_Text, text_size = Tbl_Size, text_halign = text.align_left)
            table.cell(pnlTbl, 1, k + 1, str.tostring(trd), text_color = Tbl_Text, text_size = Tbl_Size)
            table.cell(pnlTbl, 2, k + 1, str.tostring(win) + "-" + str.tostring(los), text_color = Tbl_Text, text_size = Tbl_Size)
            table.cell(pnlTbl, 3, k + 1, fmtWinPct(win, los), text_color = Tbl_Text, text_size = Tbl_Size)
            table.cell(pnlTbl, 4, k + 1, fmtPts(pts), text_color = pc, text_size = Tbl_Size)
            table.cell(pnlTbl, 5, k + 1, fmtUsd(usd), text_color = pc, text_size = Tbl_Size)
            tTr += trd
            tW  += win
            tL  += los
            tP  += pts

    float tUsd = tP * syminfo.pointvalue * Trade_Qty
    color tc   = pnlColor(tP)
    int   tRow = Table_Days + 1
    table.cell(pnlTbl, 0, tRow, "Total", text_color = hdrCol, text_size = Tbl_Size, text_halign = text.align_left)
    table.cell(pnlTbl, 1, tRow, str.tostring(tTr), text_color = hdrCol, text_size = Tbl_Size)
    table.cell(pnlTbl, 2, tRow, str.tostring(tW) + "-" + str.tostring(tL), text_color = hdrCol, text_size = Tbl_Size)
    table.cell(pnlTbl, 3, tRow, fmtWinPct(tW, tL), text_color = hdrCol, text_size = Tbl_Size)
    table.cell(pnlTbl, 4, tRow, fmtPts(tP), text_color = tc, text_size = Tbl_Size)
    table.cell(pnlTbl, 5, tRow, fmtUsd(tUsd), text_color = tc, text_size = Tbl_Size)

Overview

Clusterbreak triggers signals when a reversal breaks a swing point formed inside an opposing zone, projecting stop loss and take profit targets based on zone height, fixed points, or percentages. Users can enable higher-timeframe structure tracking with lower-timeframe retrace entries, filter entries using configurable trading sessions, and output webhook alert payloads. A simulated daily P&L table and settings dashboard display performance metrics and current configurations directly on screen.

Details

Published by

Jferheart on GitHub

Type

Indicator, Pine Script v5

Draws

On the price chart

Category

SMC / ICT / price action, Support & resistance, Sessions & time, Screeners & dashboards

Markets

Any symbol

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
Swing Point Lengthint8Swing_Length
Send Webhook AlertsboolfalseSend_Webhook
AccountstringAccount
Auto-Detect Instrument From ChartbooltrueAuto_Instrument
Manual Instrument OverridestringManual_Instrument
Quantityint3Trade_Qty
Flatten & reverse on opposite signalboolfalseReverse_On_Opposite
Show status boxbooltrueShow_Status
Show daily P&L tablebooltrueShow_PnL_Table
Days to showint5Table_Days
Trading day ends at (hour, 0-23)int16Day_End_Hour
Day timezonestringAmerica/New_YorkDay_Timezone
Show all 40 settings
SettingTypeDefaultVariable
Table positionstringBottom RightTable_Pos_Input
Show settings tablebooltrueShow_Cfg_Table
Settings table positionstringTop LeftCfg_Pos_Input
Only trade inside time windowsbooltrueUse_Windows
Window timezonestringAmerica/New_YorkWindow_TZ
Window 1booltrueW1_On
W1_Sesssession0500-1330W1_Sess
Window 2booltrueW2_On
W2_Sesssession1330-1530W2_Sess
Window 3booltrueW3_On
W3_Sesssession0300-0500W3_Sess
Close open trade when the window endsboolfalseClose_At_End
Shade the windows on the chartbooltrueShade_Windows
Window_Shadecolorcolor.new(color.teal, 92)Window_Shade
Detect structure on a higher timeframeboolfalseUse_HTF_Entry
Structure timeframetimeframe1HTF_Period
Entry triggerstringRetrace into zone (better price)Entry_Trigger
Retrace % into the zonefloat50.0Retrace_Pct
Retrace timeout (LTF bars)int40Retrace_Timeout
Show retrace target while waitingbooltrueShow_Retrace_Line
Trade shorts (bearish MSS after bullish MSS)booltrueTrade_Shorts
Trade longs (bullish MSS after bearish MSS)booltrueTrade_Longs
Draw Stop / Entry / TP levelsbooltrueShow_Bracket
Show price on Stop / Entry / TP labelsboolfalseShow_Bracket_Price
Bracket line length (bars)int15Bracket_Bars
Touch tolerance (bars)int10Touch_Tolerance
Stop loss modestringZone multipleStop_Mode
Stop multiple (x zone height, used in Zone multiple mode)float1.0Stop_Mult
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

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

This indicator detects and draws the first Fair Value Gap (FVG) that forms in each hourly session based on a selected lower timeframe, with specialized handling for Regular Trading Hours (RTH) confluence windows. It draws FVG range boxes, midpoint lines, hourl

Indicator v5 SMC / ICT / price actionSupport & resistance Repaints

TTrades Daily Bias [TFO]

by dbruce1990
12 stars

This indicator determines daily and weekly bias using higher-timeframe structural levels and liquidity targets based on TTrades concepts. It plots period separators, tracks previous highs and lows, and changes level colors when liquidity is raided. An on-scree

Indicator v5 SMC / ICT / price actionSupport & resistance
Updated 1 year ago · None stated GitHub repository of TTrades Daily Bias [TFO]

Apex Protocol Assistant

by alboogycOdR
6 stars

Apex Protocol Assistant is a Smart Money Concepts indicator designed for session-based trading. It determines market direction using a higher timeframe Exponential Moving Average and highlights Asian range boundaries alongside London and New York killzones. Th

Indicator v5 SMC / ICT / price actionSessions & time
Updated 7 months ago · None stated GitHub repository of Apex Protocol Assistant
575 stars

This overlay indicator scans a pasted list of symbols for ICT liquidity sweep patterns using prior swing sweeps. The screening method looks back one, two, or three swings and can list long and short signals in a results table with adjustable rows, position, an

Indicator v5 SMC / ICT / price actionScreeners & dashboards
Updated 1 year ago · MPL-2.0 GitHub repository of ICT Liquidity Sweep Pattern Scanner
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

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