Indicator · Pine Script v6 · On the chart

TheStrat Suite

by 345-strategies

TheStrat Suite is a multi-timeframe price-action indicator that implements Rob Smith's TheStrat on the price chart.

View on GitHub

Repaints

GitHub stars5on the repository
Pine versionv6current
Lines of code3,074
Settings40inputs
LicenceMPL-2.0
Last update2 days ago26 Oct 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

3,074 lines of Pine Script v6 from pine/TheStratSuite_v2.2.4-split.pine, published under MPL-2.0. Keep the author's credit and licence lines when you reuse it.

TheStratSuite_v2.2.4-split.pine
//@version=6
// TheStrat Suite - v2.2.4-split
//
// Multi-timeframe price action indicator implementing TheStrat methodology.
// Detects key patterns including inside bars, hammers, shooters, and Failed 2s
// (Range Reclaims) with intelligent signal consolidation and extensive customization.
//
// Features:
//   - 6 configurable timeframes with independent settings
//   - Pattern detection: Inside bars, Hammers, Shooters, Failed 2s, Outside bars
//   - Magnitude and Exhaustion level targets
//   - Full Timeframe Continuity (FTFC) filtering
//   - Domino detection (consecutive inside bars across timeframes)
//   - Preview mode for planning ahead before market open
//   - Consolidated alerts with emoji indicators
//   - Data table showing current state across all timeframes
//
// For setup guides and documentation, visit: thestratsuite.com

indicator("TheStrat Suite - v2.2.4-split", shorttitle = "TheStrat Suite", overlay = true, max_labels_count = 200, max_lines_count = 200, max_boxes_count = 200, max_bars_back = 200)

// ============================================================================
// SECTION 1: HELPER FUNCTIONS
// ============================================================================

getTablePos(pos) =>
    result = switch pos
        "Bottom Center" => position.bottom_center
        "Bottom Left"   => position.bottom_left
        "Bottom Right"  => position.bottom_right
        "Middle Center" => position.middle_center
        "Middle Left"   => position.middle_left
        "Middle Right"  => position.middle_right
        "Top Center"    => position.top_center
        "Top Left"      => position.top_left
        "Top Right"     => position.top_right
        => position.bottom_right
    result

getSize(sizeStr) =>
    result = switch sizeStr
        "Tiny"   => size.tiny
        "Small"  => size.small
        "Normal" => size.normal
        "Large"  => size.large
        => size.small
    result

getTimeframeLabel(tf) =>
    result = switch tf
        "1" => "1m"
        "3" => "3m"
        "5" => "5m"
        "15" => "15m"
        "30" => "30m"
        "60" => "1H"
        "120" => "2H"
        "240" => "4H"
        "480" => "8H"
        "720" => "12H"
        "D" => "D"
        "1d" => "D"
        "W" => "W"
        "1W" => "W"
        "M" => "M"
        "1M" => "M"
        "3M" => "3M"
        "6M" => "6M"
        "12M" => "12M"
        => tf
    result

getLineStyle(style) =>
    style == "Solid" ? line.style_solid : style == "Dashed" ? line.style_dashed : line.style_dotted


// Update an existing line in-place or create a new one. Returns the line reference.
updateOrCreateLine(line ref, int x1, float y1, int x2, float y2, color c, int w, string style) =>
    if na(ref)
        line.new(x1, y1, x2, y2, color = c, width = w, style = style, xloc = xloc.bar_time)
    else
        line.set_xy1(ref, x1, y1)
        line.set_xy2(ref, x2, y2)
        line.set_color(ref, c)
        line.set_width(ref, w)
        line.set_style(ref, style)
        ref

// Update an existing box in-place or create a new one. Returns the box reference.
updateOrCreateBox(box ref, int x1, float y1, int x2, float y2, color bgc, color borderc) =>
    if na(ref)
        box.new(x1, y1, x2, y2, bgcolor = bgc, border_color = borderc, xloc = xloc.bar_time)
    else
        box.set_lefttop(ref, x1, y1)
        box.set_rightbottom(ref, x2, y2)
        box.set_bgcolor(ref, bgc)
        box.set_border_color(ref, borderc)
        ref

// Delete a line if it exists, return na
deleteLine(line ref) =>
    if not na(ref)
        line.delete(ref)
    line(na)

// Delete a box if it exists, return na
deleteBox(box ref) =>
    if not na(ref)
        box.delete(ref)
    box(na)

var chartTfSeconds = timeframe.in_seconds(timeframe.period)

// ============================================================================
// SECTION 2: INPUTS
// ============================================================================

// -------------------- PRESET --------------------
grpPreset = "Timeframe Preset"
tfPreset = input.string("TheStrat Classic", "Preset", options = ["Custom", "TheStrat Classic", "Scalp", "Day Trade", "Futures/Crypto", "Swing Trade", "Investing"], group = grpPreset, tooltip = "Select a timeframe preset to auto-configure timeframes and line widths. When set to anything other than Custom, individual Timeframe 1-6 settings are ignored. You must select Custom to use your own timeframe configuration.")
labelStyleMode = input.string("TheStrat", "Label Style", options = ["TheStrat", "Universal"], group = grpPreset, tooltip = "TheStrat: standard Strat combo notation (e.g. 2d-1-2u HAM). Universal: plain language labels like REVERSAL, CONTINUATION, INSIDE, and OUTSIDE with diamond for hammer/shooter confidence. Designed for traders who are new to TheStrat or prefer a simplified view.")
simpleLabelMode = labelStyleMode == "Universal"
customTFs = tfPreset == "Custom"  // FIX MOD-3: true only when no preset is active; gates the Timeframe rows' `active`

// -------------------- TIMEFRAMES --------------------
// FIX MOD-3: every Timeframe row is greyed out (active = customTFs) while a preset is selected,
// since presets override these. Direct UX signal that the manual config is inert under a preset.
grpTFs = "Timeframes"
tf1Enabled = input.bool(false, "Timeframe 1", inline = "tf1a", group = grpTFs, active = customTFs)
tf1 = input.timeframe("15", "", inline = "tf1a", group = grpTFs, active = customTFs)
tf1LineWidth = input.int(1, "Width", minval = 1, maxval = 5, inline = "tf1b", group = grpTFs, active = customTFs)
tf1ShowOpen = input.bool(false, "Open", inline = "tf1b", group = grpTFs, active = customTFs)

tf2Enabled = input.bool(false, "Timeframe 2", inline = "tf2a", group = grpTFs, active = customTFs)
tf2 = input.timeframe("30", "", inline = "tf2a", group = grpTFs, active = customTFs)
tf2LineWidth = input.int(1, "Width", minval = 1, maxval = 5, inline = "tf2b", group = grpTFs, active = customTFs)
tf2ShowOpen = input.bool(false, "Open", inline = "tf2b", group = grpTFs, active = customTFs)

tf3Enabled = input.bool(false, "Timeframe 3", inline = "tf3a", group = grpTFs, active = customTFs)
tf3 = input.timeframe("60", "", inline = "tf3a", group = grpTFs, active = customTFs)
tf3LineWidth = input.int(1, "Width", minval = 1, maxval = 5, inline = "tf3b", group = grpTFs, active = customTFs)
tf3ShowOpen = input.bool(false, "Open", inline = "tf3b", group = grpTFs, active = customTFs)

tf4Enabled = input.bool(false, "Timeframe 4", inline = "tf4a", group = grpTFs, active = customTFs)
tf4 = input.timeframe("D", "", inline = "tf4a", group = grpTFs, active = customTFs)
tf4LineWidth = input.int(2, "Width", minval = 1, maxval = 5, inline = "tf4b", group = grpTFs, active = customTFs)
tf4ShowOpen = input.bool(false, "Open", inline = "tf4b", group = grpTFs, active = customTFs)

tf5Enabled = input.bool(false, "Timeframe 5", inline = "tf5a", group = grpTFs, active = customTFs)
tf5 = input.timeframe("W", "", inline = "tf5a", group = grpTFs, active = customTFs)
tf5LineWidth = input.int(3, "Width", minval = 1, maxval = 5, inline = "tf5b", group = grpTFs, active = customTFs)
tf5ShowOpen = input.bool(false, "Open", inline = "tf5b", group = grpTFs, active = customTFs)

tf6Enabled = input.bool(false, "Timeframe 6", inline = "tf6a", group = grpTFs, active = customTFs)
tf6 = input.timeframe("M", "", inline = "tf6a", group = grpTFs, active = customTFs)
tf6LineWidth = input.int(4, "Width", minval = 1, maxval = 5, inline = "tf6b", group = grpTFs, active = customTFs)
tf6ShowOpen = input.bool(false, "Open", inline = "tf6b", group = grpTFs, active = customTFs)

// Resolve preset overrides
// When preset != Custom, these override the individual TF inputs
usePreset = tfPreset != "Custom"

rTF1 = usePreset ? (tfPreset == "TheStrat Classic" ? "30"  : tfPreset == "Scalp" ? "5"   : tfPreset == "Day Trade" ? "15"  : tfPreset == "Futures/Crypto" ? "60"  : tfPreset == "Swing Trade" ? "240" : tfPreset == "Investing" ? "W"   : tf1) : tf1
rTF2 = usePreset ? (tfPreset == "TheStrat Classic" ? "60"  : tfPreset == "Scalp" ? "15"  : tfPreset == "Day Trade" ? "30"  : tfPreset == "Futures/Crypto" ? "240" : tfPreset == "Swing Trade" ? "D"   : tfPreset == "Investing" ? "M"   : tf2) : tf2
rTF3 = usePreset ? (tfPreset == "TheStrat Classic" ? "D"   : tfPreset == "Scalp" ? "30"  : tfPreset == "Day Trade" ? "60"  : tfPreset == "Futures/Crypto" ? "720" : tfPreset == "Swing Trade" ? "W"   : tfPreset == "Investing" ? "3M"  : tf3) : tf3
rTF4 = usePreset ? (tfPreset == "TheStrat Classic" ? "W"   : tfPreset == "Scalp" ? "60"  : tfPreset == "Day Trade" ? "240" : tfPreset == "Futures/Crypto" ? "D"   : tfPreset == "Swing Trade" ? "M"   : tfPreset == "Investing" ? "12M" : tf4) : tf4
rTF5 = usePreset ? (tfPreset == "TheStrat Classic" ? "M"   : tfPreset == "Scalp" ? "60"  : tfPreset == "Day Trade" ? "D"   : tfPreset == "Futures/Crypto" ? "W"   : tfPreset == "Swing Trade" ? "M"   : tfPreset == "Investing" ? "12M" : tf5) : tf5
rTF6 = usePreset ? (tfPreset == "TheStrat Classic" ? "M"   : tfPreset == "Scalp" ? "60"  : tfPreset == "Day Trade" ? "D"   : tfPreset == "Futures/Crypto" ? "W"   : tfPreset == "Swing Trade" ? "M"   : tfPreset == "Investing" ? "12M" : tf6) : tf6

rEN1 = usePreset ? true  : tf1Enabled
rEN2 = usePreset ? true  : tf2Enabled
rEN3 = usePreset ? true  : tf3Enabled
rEN4 = usePreset ? true  : tf4Enabled
rEN5 = usePreset ? (tfPreset == "TheStrat Classic" or tfPreset == "Futures/Crypto" or tfPreset == "Day Trade") : tf5Enabled
rEN6 = usePreset ? false : tf6Enabled

rLW1 = usePreset ? 1 : tf1LineWidth
rLW2 = usePreset ? (tfPreset == "TheStrat Classic" or tfPreset == "Day Trade" ? 1 : 2) : tf2LineWidth
rLW3 = usePreset ? (tfPreset == "TheStrat Classic" ? 2 : tfPreset == "Futures/Crypto" ? 2 : tfPreset == "Day Trade" ? 2 : 3) : tf3LineWidth
rLW4 = usePreset ? (tfPreset == "TheStrat Classic" ? 3 : tfPreset == "Futures/Crypto" ? 3 : tfPreset == "Day Trade" ? 3 : 4) : tf4LineWidth
rLW5 = usePreset ? 4 : tf5LineWidth
rLW6 = usePreset ? 4 : tf6LineWidth

rSO1 = usePreset ? false : tf1ShowOpen
rSO2 = usePreset ? false : tf2ShowOpen
rSO3 = usePreset ? false : tf3ShowOpen
rSO4 = usePreset ? false : tf4ShowOpen
rSO5 = usePreset ? false : tf5ShowOpen
rSO6 = usePreset ? false : tf6ShowOpen

// -------------------- SIGNALS --------------------
grpSigRev = "Signals - Reversals"
showInsideReversals = input.bool(true, "Inside Reversals |", inline = "s_ir", group = grpSigRev, tooltip = "C1 is inside, CC breaks opposite C2's direction (e.g. 2d-1-2u). The bread-and-butter Strat pattern: price consolidated then reversed.")
insideRevOnlyMomo = input.bool(false, "HAM/SHO", inline = "s_ir", group = grpSigRev, tooltip = "HAM/SHO filter: Require C1 to be a hammer or shooter. These candles tested one side and got rejected, adding conviction that the breakout direction has structural backing.")
insideRevRequireFTFC = input.bool(false, "FTFC", inline = "s_ir", group = grpSigRev, tooltip = "FTFC filter: Require all monitored timeframes to agree on direction. Removes setups that fight the broader structural alignment, keeping only those swimming with the current.")

showAllReversals = input.bool(true, "2-2 Reversals |", inline = "s_2r", group = grpSigRev, tooltip = "Direct reversal with no inside bar pause (e.g. 2d-2u). CC immediately breaks opposite C1. Faster signal but less coiled energy than inside reversals.")
reversalsRequireActionable = input.bool(false, "HAM/SHO", inline = "s_2r", group = grpSigRev, tooltip = "HAM/SHO filter: Require C1 to be a hammer or shooter. These candles tested one side and got rejected, adding conviction that the breakout direction has structural backing.")
reversalsRequireC1F2 = input.bool(false, "F2", inline = "s_2r", group = grpSigRev, tooltip = "F2 filter: Require C1 to be a Failed 2 itself (e.g. F2d-2u). Adds structural confluence: the prior bar already reversed a failed breakout, so trapped traders from that failure fuel the current move.")
reversalsRequireFTFC = input.bool(false, "FTFC", inline = "s_2r", group = grpSigRev, tooltip = "FTFC filter: Require all monitored timeframes to agree on direction. Removes setups that fight the broader structural alignment, keeping only those swimming with the current.")

grpSigCont = "Signals - Continuations"
showInsideContinuations = input.bool(true, "Inside Continuations |", inline = "s_ic", group = grpSigCont, tooltip = "C1 is inside, CC breaks same direction as C2 (e.g. 2u-1-2u). Price paused to consolidate then continued the prior move. Trend continuation with a pullback.")
insideContOnlyMomo = input.bool(false, "HAM/SHO", inline = "s_ic", group = grpSigCont, tooltip = "HAM/SHO filter: Require C1 to be a hammer or shooter. These candles tested one side and got rejected, adding conviction that the breakout direction has structural backing.")
insideContRequireFTFC = input.bool(false, "FTFC", inline = "s_ic", group = grpSigCont, tooltip = "FTFC filter: Require all monitored timeframes to agree on direction. Removes setups that fight the broader structural alignment, keeping only those swimming with the current.")

showContinuations = input.bool(false, "2-2 Continuations |", inline = "s_2c", group = grpSigCont, tooltip = "Sustained momentum with no pause (e.g. 2u-2u). CC keeps pushing in C1's direction. Can be noisy without filters since any two trending bars qualify.")
twoTwoContOnlyMomo = input.bool(false, "HAM/SHO", inline = "s_2c", group = grpSigCont, tooltip = "HAM/SHO filter: Require C1 to be a hammer or shooter. These candles tested one side and got rejected, adding conviction that the breakout direction has structural backing.")
continuationsRequireFTFC = input.bool(false, "FTFC", inline = "s_2c", group = grpSigCont, tooltip = "FTFC filter: Require all monitored timeframes to agree on direction. Removes setups that fight the broader structural alignment, keeping only those swimming with the current.")

grpSigF2 = "Signals - Reclaims"
show_p3_toggle = input.bool(false, "Failing 2s (Range Reclaims) |", inline = "s_f2", group = grpSigF2, tooltip = "Show Range Reclaim signals where price reverses after breaking out. Also known as Failing 2s, Potential 3, or 1-bar Rev Strat.")
p3RequireFTFC = input.bool(false, "FTFC", inline = "s_f2", group = grpSigF2, tooltip = "FTFC filter: Require all monitored timeframes to agree on direction. Removes setups that fight the broader structural alignment, keeping only those swimming with the current.")

grpSigExp = "Signals - Expansions"
showRangeExpansions = input.bool(false, "3-2 Expansions |", inline = "s_3e", group = grpSigExp, tooltip = "C1 was a 3-bar (outside bar), CC commits to one direction (e.g. 3u-2u). After C1 took both sides, CC resolves the indecision. Often powerful when the 3-bar shook out weak hands.")
rangeExpOnlyMomo = input.bool(false, "HAM/SHO", inline = "s_3e", group = grpSigExp, tooltip = "HAM/SHO filter: Require C1 to be a hammer or shooter. A 3-bar that also formed a hammer/shooter tested and rejected one side despite the range expansion, adding directional conviction.")
rangeExpansionsRequireFTFC = input.bool(false, "FTFC", inline = "s_3e", group = grpSigExp, tooltip = "FTFC filter: Require all monitored timeframes to agree on direction. Removes setups that fight the broader structural alignment, keeping only those swimming with the current.")

show3Expansions = input.bool(false, "Outside Bars (3 Exp) |", inline = "s_ob", group = grpSigExp, tooltip = "CC itself is a 3-bar, engulfing C1's entire range. Represents range expansion and volatility. Direction determined by close vs open (3u = bullish, 3d = bearish).")
threeExpRequireFTFC = input.bool(false, "FTFC", inline = "s_ob", group = grpSigExp, tooltip = "FTFC filter: Require all monitored timeframes to agree on direction. Removes setups that fight the broader structural alignment, keeping only those swimming with the current.")

// -------------------- FILTERS --------------------
grpHammerShooter = "Filters - Hammer/Shooter Detection"
logicType = input.string("Broad (Loose)", "Definition", options = ["Broad (Loose)", "Classic", "Pin Bar (Strict)"], group = grpHammerShooter, tooltip = "Broad: relaxed detection based on body position. Classic: small body, long wick (3x body), body in upper/lower third. Pin Bar: stricter wick requirements.")
onlyColor = input.bool(false, "Match Candle Color", group = grpHammerShooter, tooltip = "When enabled, hammers must be green (bullish) and shooters must be red (bearish) to qualify")

grpDomino = "Filters - Domino Setups"
minDominoTFs = input.int(2, "Minimum Timeframes", minval = 2, maxval = 6, group = grpDomino, tooltip = "Minimum number of consecutive inside bar timeframes (with visible signals) to trigger a Domino. Respects your signal filter settings.")
showDominoInTable = input.bool(true, "Show in Data Table", group = grpDomino, tooltip = "Show a Domino row in the Data Table when consecutive inside bars are detected")

grpDirFilter = "Filters - Lead Signal"
enableDirectionalFilter = input.bool(false, "Only Show Signals Following the Lead", group = grpDirFilter, tooltip = "When enabled, only shows signals on lower timeframes that align with the direction of the highest timeframe signal in-force (the Lead). If no higher TF has a signal in-force, all signals are shown.")
// -------------------- MAGNITUDE & EXHAUSTION --------------------
grpMagExh = "Targets - Magnitude & Exhaustion"
showMagnitude = input.bool(true, "Show Magnitude Levels", group = grpMagExh, tooltip = "Show magnitude (C2 high/low) as take-profit targets for actionable signals")
magOnlyWhenInForce = input.bool(false, "Only When In-Force", group = grpMagExh, tooltip = "Only show magnitude levels when the signal has triggered (breakout occurred). Hides targets on potential setups so you only see them when the trade is live.")
showExhaustion = input.bool(true, "Show Exhaustion Levels", group = grpMagExh, tooltip = "Show exhaustion levels (prior swing high/low beyond C2) as extended targets")
exhOnlyWhenInForce = input.bool(false, "Only When In-Force", group = grpMagExh, tooltip = "Only show exhaustion levels when the signal has triggered (breakout occurred). Hides extended targets on potential setups so you only see them when the trade is live.")
exhRequiresMagHit = input.bool(false, "Only After Magnitude Hit", group = grpMagExh, tooltip = "When enabled, exhaustion levels only appear after price has reached the magnitude level")

// Gate for exhaustion search: only scan for pivot-based exhaustion when targets are enabled
showAnyTargets = showMagnitude or showExhaustion

// -------------------- TAKE ACTION WINDOWS --------------------
grpTAW = "Targets - Take Action Windows"
showTakeActionWindows = input.bool(true, "Show Take Action Windows", group = grpTAW, tooltip = "Highlight the zone between trigger and target levels for actionable signals")
tawOnlyWhenInForce = input.bool(true, "Only When In-Force", group = grpTAW, tooltip = "Only show take action windows when the signal has triggered (breakout occurred)")
tawExtendToExhaustion = input.bool(true, "Extend to Exhaustion", group = grpTAW, tooltip = "Extend the take action window to the exhaustion level instead of magnitude")
tawExhOnlyWhenInForce = input.bool(true, "Only When In-Force", group = grpTAW, tooltip = "Only extend to exhaustion when the signal has triggered")
tawFillOpacity = input.int(8, "Fill Opacity", minval = 0, maxval = 100, group = grpTAW, tooltip = "Opacity of the take action window fill (0 = transparent, 100 = solid)")
tawBorderOpacity = input.int(30, "Border Opacity", minval = 0, maxval = 100, group = grpTAW, tooltip = "Opacity of the take action window border")
// -------------------- STOP LEVELS --------------------
grpStops = "Stops - Stop Levels"
showStopLevels = input.bool(false, "Enable", group = grpStops, tooltip = "Show stop loss levels on the opposite side of the trigger. Once a signal goes in force, the stop is locked and persists for the remainder of that period even if the signal is no longer in force.")
stopReference = input.string("CC", "Reference", options = ["CC", "C1"], group = grpStops, tooltip = "CC: stop tracks the breakout bar's opposite extreme in real time (updates as the candle forms, locks when it closes). Tighter risk, invalidated when the breakout candle itself is reversed. C1: stop at the prior bar's opposite side, locked when signal goes in force. Wider risk, invalidated when the full setup range is taken out. Failing 2 signals always use CC regardless of this setting.")
stopBEatMag = input.bool(false, "Break Even at Magnitude", group = grpStops, tooltip = "Move stop to the entry trigger (break even) when price hits the magnitude level. If no magnitude level exists, uses exhaustion as the trigger instead.")
stopBEatExh = input.bool(false, "Break Even at Exhaustion", group = grpStops, tooltip = "Move stop to the entry trigger (break even) when price hits the exhaustion level. If no exhaustion level exists, uses magnitude as the trigger instead.")
stopColorMode = input.string("Opposite Signal", "Color Mode", options = ["Opposite Signal", "Match Signal", "Custom"], group = grpStops, tooltip = "Opposite Signal: bullish stops use the bearish signal color, bearish stops use the bullish signal color. Match Signal: stops match their signal's color. Custom: use the single color below for all stops.")
colorStop = input.color(#FFFFFF, "Custom Color", group = grpStops, tooltip = "Color for stop level lines and labels when Color Mode is set to Custom", active = showStopLevels and stopColorMode == "Custom")  // FIX MOD-3: only relevant when stops on AND Custom color mode
// -------------------- DISPLAY - PREVIEW MODE --------------------
grpPreview = "Display - Preview Mode"
previewMode = input.string("Auto", "Preview Mode", options = ["Off", "On", "Auto"], group = grpPreview, tooltip = "Off: never preview. Must be set to Off for Bar Replay mode. On: always preview next period levels. Auto: activates whenever the market is not actively trading, automatically disabled during bar replay. Stocks/ETFs/Options: before 9:30 AM, after 4 PM ET, and weekends. Futures/Forex/Commodities: weekends and the daily 5-6 PM ET maintenance window. Crypto: only activates during extended outages. Also activates during holidays for all asset types.")

// -------------------- DISPLAY - DATA TABLE --------------------
grpTable = "Display - Data Table"
showBarTypeTable = input.bool(true, "Show Data Table", group = grpTable, tooltip = "Show the multi-timeframe data table")
tableMode = input.string("Full (TheStrat)", "Mode", options = ["Full (TheStrat)", "Compact (Universal)"], group = grpTable, tooltip = "Full (TheStrat): Complete table with C2, C1, AS, CC columns using Strat notation. Compact (Universal): Horizontal row of TF labels only. Yellow = inside & above open, Orange = inside & below open, Green/Red = signal in force.")
tablePosition = input.string("Top Right", "Position", options = ["Bottom Center", "Bottom Left", "Bottom Right", "Middle Center", "Middle Left", "Middle Right", "Top Center", "Top Left", "Top Right"], group = grpTable, tooltip = "Position of the data table on the chart")
tableTextSize = input.string("Small", "Text Size", options = ["Tiny", "Small", "Normal", "Large"], group = grpTable, tooltip = "Size of the table text")
useDashSeparator = input.bool(true, "Use Dash Separator", group = grpTable, tooltip = "Separate candle values with a dash (e.g., 2u-2d instead of 2u2d)")

// -------------------- DISPLAY - LABELS --------------------
grpLabels = "Display - Labels"
showTimelineLabels = input.bool(true, "Show Timeline Labels", group = grpLabels, tooltip = "Show labels at the end of each level line, anchored to the line's end time")
labelOffset = input.int(0, "Offset (bars)", minval = 0, maxval = 20, group = grpLabels, tooltip = "Number of bars to offset timeline labels from the line end", active = showTimelineLabels)  // FIX MOD-3
showFloatingLabels = input.bool(false, "Show Floating Labels", group = grpLabels, tooltip = "Show labels at a fixed offset from the current bar, keeping them visible on screen as price moves")
floatingLabelOffset = input.int(1, "Offset (bars)", minval = -20, maxval = 50, group = grpLabels, tooltip = "Number of bars to offset floating labels from the current bar (negative = left, positive = right)", active = showFloatingLabels)  // FIX MOD-3
showPriceInLabel = input.bool(false, "Show Price in Label", group = grpLabels, tooltip = "Include the price level in each label")
showHighLowInLabel = input.bool(false, "Show H/L in Labels", group = grpLabels, tooltip = "Show 'H' for high levels and 'L' for low levels in labels and alerts")
labelTextSize = input.string("Small", "Text Size", options = ["Tiny", "Small", "Normal", "Large"], group = grpLabels, tooltip = "Size of the label text")
labelBackgroundColor = input.color(#2e2e2e, "Background Color", group = grpLabels, tooltip = "Background color for labels")
labelBackgroundTransparency = input.int(20, "Transparency", minval = 0, maxval = 100, group = grpLabels, tooltip = "Transparency of the label background (0 = solid, 100 = invisible)")

// Auto-preview detection: activate whenever the market is not actively trading
// Equity-like (stocks, ETFs, options, indices, ADRs): outside 9:30 AM - 4:00 PM ET and weekends
// Futures-like (futures, forex, commodities): weekend closure (Fri 5PM - Sun 6PM ET) and daily maintenance (5-6PM ET)
// Crypto: 24/7, only triggers via holiday fallback
// All others: holiday fallback catches unusual closures
chartPeriodMs = timeframe.in_seconds(timeframe.period) * 1000
etHour = hour(timenow, "America/New_York")
etMinute = minute(timenow, "America/New_York")
etDOW = dayofweek(timenow, "America/New_York")
isWeekendDay = etDOW == dayofweek.saturday or etDOW == dayofweek.sunday
// Stocks/ETFs/Options: closed before 9:30 AM, after 4:00 PM, and weekends
isEquityLike = syminfo.type == "stock" or syminfo.type == "fund" or syminfo.type == "dr" or syminfo.type == "index" or syminfo.type == "option"
isStockClosed = isEquityLike and (isWeekendDay or etHour >= 16 or etHour < 9 or (etHour == 9 and etMinute < 30))
// Futures/Forex/Commodities: weekend closure (Fri 5PM through Sun 6PM) and daily maintenance window (5-6PM ET Mon-Thu)
isFuturesLike = syminfo.type == "futures" or syminfo.type == "forex" or syminfo.type == "commodity"
isFuturesWeekendClosed = etDOW == dayofweek.saturday or (etDOW == dayofweek.friday and etHour >= 17) or (etDOW == dayofweek.sunday and etHour < 18)
isFuturesMaintenance = not isWeekendDay and etDOW != dayofweek.friday and etHour >= 17 and etHour < 18
isFuturesClosed = isFuturesLike and (isFuturesWeekendClosed or isFuturesMaintenance)
// Holiday fallback: now is 3x chart period or 4 hours (whichever is larger) past the bar's
// SCHEDULED CLOSE.
// FIX GLUE-1 (2026-07-06): measure from time_close, not time (the open). CME glues holiday
// half-sessions into the next trade date (Fri Jul 3 2026 lives inside Mon Jul 6's bar), so
// the LIVE Monday bar opened with a Thursday-evening stamp and (timenow - time) read ~87h:
// Auto preview engaged during a live session and demoted the forming week/day into C1
// ("weekly levels on the current day", D/W = "?", phantom PREVIEW MODE banner). A live bar's
// scheduled close is always in the future, so this can never fire while a session trades;
// genuinely halted markets still trip it once now exceeds the last scheduled close.
isHolidayClosed = not na(time_close) and (timenow - time_close) > math.max(chartPeriodMs * 3, 4 * 60 * 60 * 1000)
isBarReplay = bar_index < last_bar_index
marketLikelyClosed = barstate.islast and not isBarReplay and (isStockClosed or isFuturesClosed or isHolidayClosed)
previewNextPeriod = previewMode == "On" or (previewMode == "Auto" and marketLikelyClosed)
// FIX HTF-STRADDLE-1 (T1): a chart bar can straddle a higher-TF boundary (e.g. a weekly bar spanning
// a mid-week month/quarter roll). request.security maps each chart bar to the HTF bar in effect at the
// chart bar's OPEN, so the served M/3M/6M/12M data stays one period stale for the rest of that chart
// bar even while the market trades. The preview shift already fixes exactly this problem in its
// market-closed form; this flag arms the same shift for calendar TFs on the live last bar too.
// previewMode = Off still disables everything (preserves the bar-replay workaround).
// FIX HTF-STRADDLE-1: live-straddle window (market open, last bar, not replay; previewMode Off disables).
// RE-ENABLED 2026-07-02 with the corrected session-offset-aware detection (see shouldStraddle above).
bool straddleWindowOK = previewMode != "Off" and barstate.islast and not isBarReplay

// -------------------- STYLE --------------------
grpStyleBullish = "Style - Bullish Levels"
colorBullishActive = input.color(color.green, "Signal", group = grpStyleBullish, inline = "bull1")
colorBullishHit = input.color(#9c9c9c, "Crossed", group = grpStyleBullish, inline = "bull1")
colorBullishPotential = input.color(#ffeb3b, "1 (Inside Bar)", group = grpStyleBullish, inline = "bull2")
colorBullish3Bar = input.color(#089981, "3 (Outside Bar)", group = grpStyleBullish, inline = "bull2")
colorBullishP3 = input.color(#81c784, "Range Reclaim (F2d)", group = grpStyleBullish, tooltip = "Color for bullish Range Reclaim signals (failed bearish breakout)")

grpStyleBearish = "Style - Bearish Levels"
colorBearishActive = input.color(color.red, "Signal", group = grpStyleBearish, inline = "bear1")
colorBearishHit = input.color(#4a4a4a, "Crossed", group = grpStyleBearish, inline = "bear1")
colorBearishPotential = input.color(#ff9800, "1 (Inside Bar)", group = grpStyleBearish, inline = "bear2")
colorBearish3Bar = input.color(#e91e63, "3 (Outside Bar)", group = grpStyleBearish, inline = "bear2")
colorBearishP3 = input.color(#f77c80, "Range Reclaim (F2u)", group = grpStyleBearish, tooltip = "Color for bearish Range Reclaim signals (failed bullish breakout)")

var color COLOR_FAILED_2U = colorBearishP3
var color COLOR_FAILED_2D = colorBullishP3
var color COLOR_3_HIGH = colorBullish3Bar
var color COLOR_3_LOW = colorBearish3Bar
var color COLOR_INSIDE_HIGH = colorBullishPotential
var color COLOR_INSIDE_LOW = colorBearishPotential
var color COLOR_HIT_HIGH = colorBullishHit
var color COLOR_HIT_LOW = colorBearishHit
var color COLOR_BG_BLACK = color.new(color.black, 0)
var color COLOR_BG_GRAY50 = color.new(color.gray, 50)

colorStopHigh = stopColorMode == "Opposite Signal" ? colorBearishActive : stopColorMode == "Match Signal" ? colorBullishActive : colorStop
colorStopLow = stopColorMode == "Opposite Signal" ? colorBullishActive : stopColorMode == "Match Signal" ? colorBearishActive : colorStop

tawFillTransp = 100 - tawFillOpacity
tawBorderTransp = 100 - tawBorderOpacity
tawBullishFill = color.new(colorBullishActive, tawFillTransp)
tawBullishBorder = color.new(colorBullishActive, tawBorderTransp)
tawBearishFill = color.new(colorBearishActive, tawFillTransp)
tawBearishBorder = color.new(colorBearishActive, tawBorderTransp)


// -------------------- ALERTS --------------------
grpAlerts = "Alerts - Detailed"
alertTF1 = input.bool(true, "TF1", inline = "alert_tfs", group = grpAlerts, tooltip = "Select which timeframes to include in the consolidated 'Signal In-Force (Any)' alert. Uncheck a TF to exclude it from alert messages. Per-TF alertconditions (e.g. 'Signal In-Force TF1') are not affected by these checkboxes.")
alertTF2 = input.bool(true, "TF2", inline = "alert_tfs", group = grpAlerts)
alertTF3 = input.bool(true, "TF3", inline = "alert_tfs", group = grpAlerts)
alertTF4 = input.bool(true, "TF4", inline = "alert_tfs", group = grpAlerts)
alertTF5 = input.bool(true, "TF5", inline = "alert_tfs", group = grpAlerts)
alertTF6 = input.bool(true, "TF6", inline = "alert_tfs", group = grpAlerts)
alertShowFTFC = input.bool(false, "Include FTFC", inline = "alert_content", group = grpAlerts, tooltip = "Enrich the consolidated alert message with additional context. Each toggle appends data to the alert when available. Example output: 1H 2d-1-2u HAM @ 542.10 MAG 545.30 EXH 548.20 STOP 538.10 | FTFC Up. Fields that don't exist for a signal (e.g. no magnitude) are omitted automatically.")
alertShowTrigger = input.bool(false, "Trigger", inline = "alert_content", group = grpAlerts)
alertShowMagnitude = input.bool(false, "MAG", inline = "alert_content", group = grpAlerts)
alertShowExhaustion = input.bool(false, "EXH", inline = "alert_content", group = grpAlerts)
alertShowStop = input.bool(false, "STOP", inline = "alert_content", group = grpAlerts)

// -------------------- ADVANCED --------------------
grpAdvExh = "Advanced - Exhaustion Behavior"
exhDisablesInForce = input.bool(false, "Exhaustion Disables 'In Force'", group = grpAdvExh, tooltip = "OFF (default): A signal stays 'in force' even after its targets are hit. The data table continues showing the signal as active, and the Lead Signal filter continues using it for directional bias. ON: Once a signal hits exhaustion (or magnitude if no exhaustion exists), it is no longer considered 'in force.' This frees up the Lead Signal filter to allow reversal setups on lower timeframes. Example: if the Daily has a bullish signal that already hit exhaustion, turning this on lets bearish setups appear on the 15m/1H instead of staying suppressed by a move that may be done.")
exhDisablesFTFC = input.bool(false, "Exhaustion Excludes from FTFC", group = grpAdvExh, tooltip = "OFF (default): All timeframes count toward FTFC regardless of whether their targets have been hit. ON: A timeframe that has hit exhaustion (or magnitude if no exhaustion exists) is removed from the FTFC calculation. This means a timeframe whose move is structurally complete no longer blocks FTFC from flipping. Example: if 5 of 6 timeframes are bearish but the 6th is bullish and already hit its exhaustion, turning this on excludes that exhausted bullish timeframe so FTFC reads as bearish instead of Conflict.")

grpAdvOB = "Advanced - Outside Bars"
show3ExpMagnitude = input.bool(true, "Show Magnitude for Outside Bars", group = grpAdvOB, tooltip = "Show the C2 high/low as a magnitude target for outside bar (3-bar) expansions")
show3ExpExhaustion = input.bool(true, "Show Exhaustion for Outside Bars", group = grpAdvOB, tooltip = "Show exhaustion levels for outside bar (3-bar) expansions")

grpAdvF2 = "Advanced - Failing 2 Detection"
enableFailed2Detection = input.bool(true, "Enable Failing 2 Detection", group = grpAdvF2, tooltip = "When enabled, detects Failing 2s (f2u/f2d) where price reclaims the prior range or crosses back against the breakout direction. When disabled, only shows standard 2u/2d signals.")
failed_2_method = input.string("Reclaim", "Detection Method", options = ["Reclaim", "Open", "Reclaim + Open", "Reclaim OR Open"], group = grpAdvF2, tooltip = "Reclaim: closes back inside C1 range. Open: closes against breakout direction. Reclaim + Open: requires both conditions. Reclaim OR Open: requires one or the other.")
showF2OpenLine = input.bool(false, "Show Failing 2 Open Level", group = grpAdvF2, tooltip = "Show the CC open price as a dotted reference line when a Failing 2 is detected. The open acts as the trigger level for the reclaim.")

grpAdvTable = "Advanced - Data Table"
colorTFWhenInForce = input.bool(true, "Color TF When In-Force", group = grpAdvTable, tooltip = "Highlight the TF column with the CC color when a signal you have enabled is in-force (Full mode only)")

grpAdvTAW = "Advanced - Take Action Windows"
showP3TakeActionWindows = input.bool(true, "Include Failing 2s", group = grpAdvTAW, tooltip = "Highlight the C1 range as a take action zone for Failing 2 (Range Reclaim) setups")
show3ExpTakeActionWindows = input.bool(true, "Include Outside Bars", group = grpAdvTAW, tooltip = "Highlight the zone between trigger and magnitude for outside bar (3-bar) expansions")

grpAdvDomino = "Advanced - Domino Setups"
enableDominoAlerts = input.bool(false, "Include in Consolidated Alerts", group = grpAdvDomino, tooltip = "Include Domino alerts in the main alert() function. You can also set up a dedicated Domino alert using the 'Domino Setup' alertcondition.")

// -------------------- DEBUG --------------------
grpDebug = "Debug"
showDebugPanel = input.bool(false, "Show Debug Panel", group = grpDebug, tooltip = "Show detailed debug information for a selected timeframe to troubleshoot signal logic")
debugTimeframe = input.string("TF1", "Timeframe to Debug", options = ["TF1", "TF2", "TF3", "TF4", "TF5", "TF6"], group = grpDebug, tooltip = "Select which timeframe slot to show debug info for (TF1 = first timeframe in settings, etc.)", active = showDebugPanel)  // FIX MOD-3

// ============================================================================
// SECTION 2A: HELPER FUNCTIONS (POST-INPUTS)
// ============================================================================

// Format candle combo string with optional dash separator
// e.g., "2u2d" becomes "2u-2d" when useDashSeparator is true
formatCombo(string combo) =>
    if not useDashSeparator or str.length(combo) < 2
        combo
    else
        string result = ""
        int i = 0
        int len = str.length(combo)
        while i < len
            char = str.substring(combo, i, i + 1)
            // Check if current char is start of a bar type (digit, F, or *)
            isBarStart = char == "1" or char == "2" or char == "3" or char == "F" or char == "f" or char == "*"
            // Add dash before bar types (except first)
            if isBarStart and i > 0
                prevChar = str.substring(combo, i - 1, i)
                // Dash after completed bar types: "u"/"d" suffixed bars, or standalone "1"/"3"
                if prevChar == "u" or prevChar == "d" or prevChar == "1" or prevChar == "2" or prevChar == "3"
                    result := result + "-"
            result := result + char
            i := i + 1
        result

// ============================================================================
// SECTION 3: USER-DEFINED TYPES
// ============================================================================

type CandleMetrics
    float body
    float bodyPct
    float upperWick
    float lowerWick
    float upperWickPct
    float lowerWickPct
    float closeFromHigh
    float closeFromLow
    float openFromHigh
    float openFromLow
    float bodyCenterFromHigh
    float bodyCenterFromLow
    bool isBullish
    bool isFlat

type TimeframeData
    line highLine
    line lowLine
    line openLine
    line magHighLine
    line magLowLine
    line exhHighLine
    line exhLowLine
    box actionWindowBullish
    box actionWindowBearish
    float prevHigh
    float prevLow
    float currOpen
    float prevMagHigh
    float prevMagLow
    float exhHighPrice
    float exhLowPrice
    int prevTime
    int prevMagTime
    int exhHighTime
    int exhLowTime
    bool magHighCrossed
    bool magLowCrossed
    bool exhHighCrossed
    bool exhLowCrossed
    bool c1IsInside
    bool c1IsHammer
    bool c1IsShooter
    bool c1IsHighContinuation
    bool c1IsLowContinuation
    line stopHighLine       // stop for bullish signal (drawn at C1 low)
    line stopLowLine        // stop for bearish signal (drawn at C1 high)
    bool stopHighTriggered  // sticky: bullish signal was in force this period
    bool stopLowTriggered   // sticky: bearish signal was in force this period
    float stopHighPrice     // locked stop price for bullish signal
    float stopLowPrice      // locked stop price for bearish signal
    line f2OpenLine          // pre-F2: CC open shown as potential trigger when reclaim met but open not crossed
    int lastPeriodStart      // FIX P0-1: per-slot HTF period-open time (replaces the shared ta.change(time(tf)) buffer)
    bool wasPreview = false  // FIX P1-d: prior-bar preview state, used to re-latch when preview exits mid-period (default false avoids na() on series bool)

type SimpleLabelData
    array<float> prices
    array<string> texts
    array<color> colors
    array<int> times

type LabelPoolT          // FIX P1-i: reusable label pool - update-in-place instead of delete+recreate every tick
    array<label> items
    int used

type LineInfo
    float price
    int tfSeconds
    line lineRef
    TimeframeData owner   // FIX P1-h: owning slot, so a suppressed line's field can be nulled after delete
    int kind              // FIX P1-h: which TimeframeData line field this ref came from (see nullSuppressedLine)

type TFRawData
    int c1Time
    float c1H
    float c1L
    float c1O
    float c1C
    int ccTime
    float ccO
    float ccH
    float ccL
    float ccC
    int c2Time
    float c2H
    float c2L
    float c2O
    float c2C
    float c3H
    float c3L
    float c4H
    float c4L
    float exhHP
    int exhHT
    float exhLP
    int exhLT
    bool isPreview
    int realPeriodTime       // FIX P0-1: unshifted current HTF period-open time, for per-slot new-period detection

type ProcessingResult
    float high
    float low
    float open
    float magHigh
    float magLow
    float exhHigh
    float exhLow
    color highColor
    color lowColor
    color openColor
    color magHighColor
    color magLowColor
    color exhHighColor
    color exhLowColor
    bool drawHigh
    bool drawLow
    bool drawOpen
    bool drawMagHigh
    bool drawMagLow
    bool drawExhHigh
    bool drawExhLow
    int time
    string tfLabel
    bool isF2u
    bool isF2d
    bool c1Is3
    string c3Num
    string c2Num
    string c1Num
    string c1Type
    string ccType
    bool signalInForceHigh
    bool signalInForceLow
    float stopHigh          // bullish stop price (C1 low)
    float stopLow           // bearish stop price (C1 high)
    bool drawStopHigh
    bool drawStopLow
    bool preF2u             // pre-F2u: CC broke high, reclaimed, close still above open
    bool preF2d             // pre-F2d: CC broke low, reclaimed, close still below open
    float f2OpenPrice       // CC open price for pre-F2 line
    int ccStartTime         // CC candle start time for Domino dedup
    bool c1WasF2            // C1 was itself a Failed 2
    bool c2WasF2            // C2 was itself a Failed 2
    string highStyle        // computed style for high trigger line
    string lowStyle         // computed style for low trigger line

type DebugInfo
    string tfLabel
    bool c1_was_2u
    bool c1_was_2d
    bool c1_is_3
    bool isHammer
    bool isShooter
    bool isInside
    bool cc_is_2u
    bool cc_is_2d
    bool cc_is_3
    bool cc_broke_high
    bool cc_broke_low
    bool is_f2u
    bool is_f2d
    bool pre_f2u
    bool pre_f2d
    bool ftfc_up
    bool ftfc_down
    bool is_hammer_reversal
    bool is_hammer_momo
    bool hammer_shows
    bool is_shooter_reversal
    bool is_shooter_momo
    bool shooter_shows
    bool drawHigh
    bool drawLow
    bool drawMagHigh
    bool drawMagLow
    bool drawExhHigh
    bool drawExhLow
    bool force_mag_high
    bool force_mag_low
    bool mag_high_passes_ftfc
    bool mag_low_passes_ftfc

type TFConfig
    bool enabled
    string tf
    int lineWidth
    bool showOpen
    string debugLabel

var DebugInfo debugData = DebugInfo.new("", false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false)

// ============================================================================
// SECTION 3B: CLEANUP HELPER METHODS
// ============================================================================

// FIX P2-1: removed five unused cleanup methods (clearHighExtras / clearLowExtras /
// clearHighAll / clearLowAll / clearCommon). They had zero reachable call sites - the
// only callers were each other (clearHighAll -> clearHighExtras, etc.), so the whole
// cluster was transitively dead. The live suppress* methods below are unaffected.

// ProcessingResult flag suppression methods
method suppressHighFlags(ProcessingResult this) =>
    this.drawHigh := false
    this.drawMagHigh := false
    this.drawExhHigh := false
    this.drawStopHigh := false

method suppressLowFlags(ProcessingResult this) =>
    this.drawLow := false
    this.drawMagLow := false
    this.drawExhLow := false
    this.drawStopLow := false

method suppressAllFlags(ProcessingResult this) =>
    this.suppressHighFlags()
    this.suppressLowFlags()

// ============================================================================
// SECTION 4: PATTERN DETECTION FUNCTIONS
// ============================================================================

calcMetrics(o, h, l, c) =>
    float r = h - l
    if r == 0
        CandleMetrics.new(0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, true, true)
    else
        float bodySize = math.abs(c - o)
        float bodyCenter = (o + c) / 2
        float upperWick = h - math.max(o, c)
        float lowerWick = math.min(o, c) - l
        CandleMetrics.new(bodySize, bodySize / r, upperWick, lowerWick, upperWick / r, lowerWick / r, (h - c) / r, (c - l) / r, (h - o) / r, (o - l) / r, (h - bodyCenter) / r, (bodyCenter - l) / r, c > o, false)

isClassicHammer(CandleMetrics m) =>
    if m.isFlat
        false
    else
        float wickRatio = m.bodyPct > 0.001 ? (m.lowerWick / m.body) : 0
        m.bodyPct <= 0.30 and wickRatio >= 3.0 and m.upperWickPct <= 0.35 and m.bodyCenterFromHigh <= 0.33 and m.closeFromHigh <= 0.25

isClassicShooter(CandleMetrics m) =>
    if m.isFlat
        false
    else
        float wickRatio = m.bodyPct > 0.001 ? (m.upperWick / m.body) : 0
        m.bodyPct <= 0.30 and wickRatio >= 3.0 and m.lowerWickPct <= 0.35 and m.bodyCenterFromLow <= 0.33 and m.closeFromLow <= 0.25

isPinBarHammer(CandleMetrics m) =>
    m.isFlat ? false : m.closeFromHigh <= 0.25 and m.openFromHigh <= 0.25

isPinBarShooter(CandleMetrics m) =>
    m.isFlat ? false : m.closeFromLow <= 0.25 and m.openFromLow <= 0.25

isBroadHammer(CandleMetrics m) =>
    // FIX BTC-2: strict > 0.50 so a body centered exactly at the range midpoint is NOT a hammer.
    // Paired with the strict < in isBroadShooter, a centered doji classifies as neither (correct -
    // no directional rejection), instead of satisfying BOTH and drawing a spurious wrong-side line.
    m.isFlat ? false : m.openFromLow > 0.50 and m.closeFromLow > 0.50

isBroadShooter(CandleMetrics m) =>
    m.isFlat ? false : m.openFromLow < 0.50 and m.closeFromLow < 0.50  // FIX BTC-2: strict < (see isBroadHammer)

isInsideBar(currH, currL, prevH, prevL) =>
    currH <= prevH and currL >= prevL

detectPatterns(c1_open, c1_high, c1_low, c1_close) =>
    m = calcMetrics(c1_open, c1_high, c1_low, c1_close)
    bool rawHammer = false
    bool rawShooter = false
    if logicType == "Classic"
        rawHammer := isClassicHammer(m)
        rawShooter := isClassicShooter(m)
    else if logicType == "Pin Bar (Strict)"
        rawHammer := isPinBarHammer(m)
        rawShooter := isPinBarShooter(m)
    else
        rawHammer := isBroadHammer(m)
        rawShooter := isBroadShooter(m)
    isHammer = rawHammer and (not onlyColor or m.isBullish)
    isShooter = rawShooter and (not onlyColor or not m.isBullish)
    [isHammer, isShooter]

findExhaustionLevels() =>
    float exhHighPrice = na
    int exhHighTime = na
    float exhLowPrice = na
    int exhLowTime = na
    // Only calculate on recent bars - historical exhaustion is never displayed
    // Loop goes to 48, but accesses i+1, so max offset is 49
    // Require bar_index > 50 to ensure enough historical data
    if showAnyTargets and bar_index > last_bar_index - 100 and bar_index > 50
        // FIX P0-2: seed running max/min from the completed bar [1], not the forming bar [0],
        // and start the scan at i=2 so the i-1 neighbor test never reads the forming bar.
        // Under lookahead_on, high[0] in the HTF context returns the developing value live
        // but the final period value on reload, so the [0] seed made the latched exhaustion
        // level repaint. Max offset is now i+1 = 49 (loop still ends at 48).
        // DELIBERATELY NOT math.max(high[1], high[0]) for the seed: pulling the forming bar
        // back into the intact-pivot gate would re-introduce the live-vs-reload repaint this
        // fix removes. A period-open exhaustion snapshot that sits slightly behind live price
        // on a rare mid-period preview re-latch is acceptable and, crucially, reload-stable.
        float maxHighSoFar = high[1]
        float minLowSoFar = low[1]
        for i = 2 to 48
            if na(exhHighPrice)
                isPivotHigh = high[i] > high[i-1] and high[i] > high[i+1]
                if isPivotHigh and maxHighSoFar < high[i]
                    exhHighPrice := high[i]
                    exhHighTime := time[i]
            if na(exhLowPrice)
                isPivotLow = low[i] < low[i-1] and low[i] < low[i+1]
                if isPivotLow and minLowSoFar > low[i]
                    exhLowPrice := low[i]
                    exhLowTime := time[i]
            maxHighSoFar := math.max(maxHighSoFar, high[i])
            minLowSoFar := math.min(minLowSoFar, low[i])
            if not na(exhHighPrice) and not na(exhLowPrice)
                break
    [exhHighPrice, exhHighTime, exhLowPrice, exhLowTime]

calcBarType(prevH, prevL, currH, currL, currO, currC) =>
    if na(prevH) or na(prevL) or na(currH) or na(currL) or na(currO) or na(currC)
        ""
    else
        aboveOpen = currC > currO
        highCrossed = currH > prevH
        lowCrossed = currL < prevL
        if highCrossed and lowCrossed
            aboveOpen ? "3u" : "3d"
        else if highCrossed and not lowCrossed
            "2u"
        else if lowCrossed and not highCrossed
            "2d"
        else
            "1"

calcBarNum(prevH, prevL, currH, currL) =>
    if na(prevH) or na(prevL) or na(currH) or na(currL)
        ""
    else
        highCrossed = currH > prevH
        lowCrossed = currL < prevL
        if highCrossed and lowCrossed
            "3"
        else if highCrossed or lowCrossed
            "2"
        else
            "1"

getDataWithExhaustion() =>
    [exhHP, exhHT, exhLP, exhLT] = findExhaustionLevels()
    [time, open, high, low, close, time[1], high[1], low[1], open[1], close[1], time[2], high[2], low[2], open[2], close[2], high[3], low[3], high[4], low[4], exhHP, exhHT, exhLP, exhLT]

calculateFTFC(tf1_en, tf1_o, tf1_c, tf2_en, tf2_o, tf2_c, tf3_en, tf3_o, tf3_c, tf4_en, tf4_o, tf4_c, tf5_en, tf5_o, tf5_c, tf6_en, tf6_o, tf6_c) =>
    ftfc_up = true
    ftfc_down = true
    if tf1_en and not na(tf1_c) and not na(tf1_o)
        if tf1_c > tf1_o
            ftfc_down := false
        else
            ftfc_up := false
    if tf2_en and not na(tf2_c) and not na(tf2_o)
        if tf2_c > tf2_o
            ftfc_down := false
        else
            ftfc_up := false
    if tf3_en and not na(tf3_c) and not na(tf3_o)
        if tf3_c > tf3_o
            ftfc_down := false
        else
            ftfc_up := false
    if tf4_en and not na(tf4_c) and not na(tf4_o)
        if tf4_c > tf4_o
            ftfc_down := false
        else
            ftfc_up := false
    if tf5_en and not na(tf5_c) and not na(tf5_o)
        if tf5_c > tf5_o
            ftfc_down := false
        else
            ftfc_up := false
    if tf6_en and not na(tf6_c) and not na(tf6_o)
        if tf6_c > tf6_o
            ftfc_down := false
        else
            ftfc_up := false
    [ftfc_up, ftfc_down]

// ============================================================================
// SECTION 5: DECISION LOGIC FUNCTIONS
// ============================================================================

detectFailed2(c1_high, c1_low, cc_open, cc_close, cc_high, cc_low, method) =>
    cc_is_3 = cc_high > c1_high and cc_low < c1_low
    cc_is_2u = cc_high > c1_high and cc_low >= c1_low and not cc_is_3
    cc_is_2d = cc_low < c1_low and cc_high <= c1_high and not cc_is_3
    is_f2u = false
    is_f2d = false
    pre_f2u = false
    pre_f2d = false
    if enableFailed2Detection and (cc_is_2u or cc_is_2d) and not cc_is_3
        cc_inside_c1 = cc_close <= c1_high and cc_close >= c1_low
        cc_above_open = cc_close > cc_open
        if method == "Open"
            is_f2u := cc_is_2u and not cc_above_open
            is_f2d := cc_is_2d and cc_above_open
        else if method == "Reclaim"
            is_f2u := cc_is_2u and cc_inside_c1
            is_f2d := cc_is_2d and cc_inside_c1
        else if method == "Reclaim + Open"
            is_f2u := cc_is_2u and not cc_above_open and cc_inside_c1
            is_f2d := cc_is_2d and cc_above_open and cc_inside_c1
        else
            is_f2u := cc_is_2u and (not cc_above_open or cc_inside_c1)
            is_f2d := cc_is_2d and (cc_above_open or cc_inside_c1)
        // Pre-F2: CC broke range, reclaimed back inside, but close hasn't crossed open yet
        // Only relevant when method uses open condition (Open or Reclaim + Open)
        if (method == "Open" or method == "Reclaim + Open") and not is_f2u and not is_f2d
            if cc_is_2u and cc_inside_c1 and cc_above_open
                pre_f2u := true
            if cc_is_2d and cc_inside_c1 and not cc_above_open
                pre_f2d := true
    [cc_is_2u, cc_is_2d, cc_is_3, is_f2u, is_f2d, pre_f2u, pre_f2d]

shouldDrawC1Level(bool isBullish, c1_is_inside, c1_is_hammer, c1_is_shooter, cc_is_2u, cc_is_2d, cc_is_3, is_f2u, is_f2d, ftfc_up, ftfc_down, c1_was_2u, c1_was_2d, c1_is_3, c2_closed_up, c2_closed_down, c2_was_3) =>
    result = false
    is_continuation = false
    cc_is_inside = not cc_is_2u and not cc_is_2d and not cc_is_3
    ftfc_blocks = isBullish ? ftfc_down : ftfc_up
    c2_same_dir = isBullish ? c2_closed_up : c2_closed_down
    c2_opp_dir = isBullish ? c2_closed_down : c2_closed_up
    momo_pattern = isBullish ? c1_is_hammer : c1_is_shooter
    cc_in_force = isBullish ? cc_is_2u : cc_is_2d
    cc_opposite = isBullish ? cc_is_2d : cc_is_2u
    c1_was_same = isBullish ? c1_was_2u : c1_was_2d
    c1_was_opp = isBullish ? c1_was_2d : c1_was_2u
    failed_same = isBullish ? is_f2u : is_f2d
    failed_opp = isBullish ? is_f2d : is_f2u
    inside_rev_ftfc_blocks = insideRevRequireFTFC and ftfc_blocks
    reversals_ftfc_blocks = reversalsRequireFTFC and ftfc_blocks
    continuations_ftfc_blocks = continuationsRequireFTFC and ftfc_blocks
    inside_cont_ftfc_blocks = insideContRequireFTFC and ftfc_blocks
    expansions_ftfc_blocks = rangeExpansionsRequireFTFC and ftfc_blocks
    threeExp_ftfc_blocks = threeExpRequireFTFC and ftfc_blocks
    p3_ftfc_blocks = p3RequireFTFC and ftfc_blocks
    inside_is_continuation = c1_is_inside and c2_same_dir
    inside_is_reversal = c1_is_inside and (c2_opp_dir or (not c2_closed_up and not c2_closed_down))
    if cc_is_3
        if show3Expansions and not threeExp_ftfc_blocks
            result := true
    else if show_p3_toggle and failed_same
        result := not p3_ftfc_blocks
    else if show_p3_toggle and failed_opp
        result := not p3_ftfc_blocks
    else if c1_is_inside
        if inside_is_reversal and showInsideReversals
            if insideRevOnlyMomo and not momo_pattern
                result := false
            else
                result := not inside_rev_ftfc_blocks
        else if inside_is_reversal and not showInsideReversals
            result := false
        else if inside_is_continuation and showInsideContinuations
            if insideContOnlyMomo and not momo_pattern
                result := false
            else
                result := not inside_cont_ftfc_blocks
                is_continuation := result
        else if inside_is_continuation and not showInsideContinuations
            result := false
        else
            if showInsideReversals
                if insideRevOnlyMomo and not momo_pattern
                    result := false
                else
                    result := not inside_rev_ftfc_blocks
    else if c1_is_3 and cc_in_force and showRangeExpansions
        if rangeExpOnlyMomo and not momo_pattern
            result := false
        else
            result := not expansions_ftfc_blocks
            is_continuation := result
    else if c1_is_3 and cc_is_inside and showRangeExpansions
        if rangeExpOnlyMomo and not momo_pattern
            result := false
        else
            result := not expansions_ftfc_blocks
            is_continuation := result
    else if momo_pattern and not (c1_is_3 and not showRangeExpansions)
        is_momo_reversal = c1_was_opp or (c2_opp_dir and not c1_was_same) or (not c1_was_2u and not c1_was_2d and not c2_same_dir)
        is_momo_cont = c1_was_same or (c2_same_dir and not c1_was_opp)
        cc_is_opp_continuation = cc_opposite and c1_was_opp
        if cc_is_opp_continuation
            result := false
        else if is_momo_reversal and not cc_opposite
            result := not reversals_ftfc_blocks       // FIX BTC-1: momo (hammer/shooter) reversal now obeys the Reversals-FTFC toggle (was wrongly using inside-reversal's)
        else if is_momo_cont and showContinuations
            result := not continuations_ftfc_blocks   // FIX BTC-1: momo continuation now obeys the Continuations-FTFC toggle (was wrongly using inside-reversal's)
            is_continuation := result
    else if c1_was_opp and cc_in_force and showAllReversals
        if momo_pattern
            result := not reversals_ftfc_blocks
        else if reversalsRequireActionable
            result := false
        else
            result := not reversals_ftfc_blocks
    else if c1_was_opp and showAllReversals and not cc_opposite
        if momo_pattern
            result := not reversals_ftfc_blocks
        else if reversalsRequireActionable
            result := false
        else
            result := not reversals_ftfc_blocks
    else if c1_was_same and cc_in_force and showContinuations and not c2_was_3
        if twoTwoContOnlyMomo and not momo_pattern
            result := false
        else
            result := not continuations_ftfc_blocks
            is_continuation := result
    else if c1_was_same and cc_is_inside and showContinuations and not c2_was_3
        if twoTwoContOnlyMomo and not momo_pattern
            result := false
        else
            result := not continuations_ftfc_blocks
            is_continuation := result
    [result, is_continuation]

shouldDrawC1High(c1_is_inside, c1_is_hammer, c1_is_shooter, cc_is_2u, cc_is_2d, cc_is_3, is_f2u, is_f2d, ftfc_up, ftfc_down, c1_was_2u, c1_was_2d, c1_is_3, c2_closed_up, c2_closed_down, c2_was_3) =>
    shouldDrawC1Level(true, c1_is_inside, c1_is_hammer, c1_is_shooter, cc_is_2u, cc_is_2d, cc_is_3, is_f2u, is_f2d, ftfc_up, ftfc_down, c1_was_2u, c1_was_2d, c1_is_3, c2_closed_up, c2_closed_down, c2_was_3)

shouldDrawC1Low(c1_is_inside, c1_is_shooter, c1_is_hammer, cc_is_2u, cc_is_2d, cc_is_3, is_f2u, is_f2d, ftfc_up, ftfc_down, c1_was_2u, c1_was_2d, c1_is_3, c2_closed_up, c2_closed_down, c2_was_3) =>
    shouldDrawC1Level(false, c1_is_inside, c1_is_hammer, c1_is_shooter, cc_is_2u, cc_is_2d, cc_is_3, is_f2u, is_f2d, ftfc_up, ftfc_down, c1_was_2u, c1_was_2d, c1_is_3, c2_closed_up, c2_closed_down, c2_was_3)
shouldDrawMagnitude(c1_broke_c2, is_signal_in_force) =>
    not c1_broke_c2 and is_signal_in_force

// ============================================================================
// SECTION 6: DATA STRUCTURES (Array-based for loop processing)
// ============================================================================

var array<TimeframeData> tfDataArr = array.new<TimeframeData>(6)
if barstate.isfirst
    for i = 0 to 5
        array.set(tfDataArr, i, TimeframeData.new(na, na, na, na, na, na, na, na, na, na, na, na, na, na, na, na, na, na, na, na, false, false, false, false, false, false, false, false, false, na, na, false, false, na, na))

var array<TFConfig> tfConfigs = array.new<TFConfig>(6)
if barstate.isfirst
    array.set(tfConfigs, 0, TFConfig.new(rEN1, rTF1, rLW1, rSO1, "TF1"))
    array.set(tfConfigs, 1, TFConfig.new(rEN2, rTF2, rLW2, rSO2, "TF2"))
    array.set(tfConfigs, 2, TFConfig.new(rEN3, rTF3, rLW3, rSO3, "TF3"))
    array.set(tfConfigs, 3, TFConfig.new(rEN4, rTF4, rLW4, rSO4, "TF4"))
    array.set(tfConfigs, 4, TFConfig.new(rEN5, rTF5, rLW5, rSO5, "TF5"))
    array.set(tfConfigs, 5, TFConfig.new(rEN6, rTF6, rLW6, rSO6, "TF6"))

// Tiered calc_bars_count: Monthly+=100, Weekly=100, Daily=150, 4H+=200, intraday=250
// Minimum 100 required for exhaustion scan (bar_index > 50 gate inside security context)
// Inlined as simple int: Pine functions return series, which request.security rejects.
simple int cb1 = rTF1 == "M" or rTF1 == "1M" or rTF1 == "3M" or rTF1 == "6M" or rTF1 == "12M" ? 100 : rTF1 == "W" or rTF1 == "1W" ? 100 : rTF1 == "D" or rTF1 == "1D" ? 150 : rTF1 == "240" or rTF1 == "480" or rTF1 == "720" ? 200 : 250
simple int cb2 = rTF2 == "M" or rTF2 == "1M" or rTF2 == "3M" or rTF2 == "6M" or rTF2 == "12M" ? 100 : rTF2 == "W" or rTF2 == "1W" ? 100 : rTF2 == "D" or rTF2 == "1D" ? 150 : rTF2 == "240" or rTF2 == "480" or rTF2 == "720" ? 200 : 250
simple int cb3 = rTF3 == "M" or rTF3 == "1M" or rTF3 == "3M" or rTF3 == "6M" or rTF3 == "12M" ? 100 : rTF3 == "W" or rTF3 == "1W" ? 100 : rTF3 == "D" or rTF3 == "1D" ? 150 : rTF3 == "240" or rTF3 == "480" or rTF3 == "720" ? 200 : 250
simple int cb4 = rTF4 == "M" or rTF4 == "1M" or rTF4 == "3M" or rTF4 == "6M" or rTF4 == "12M" ? 100 : rTF4 == "W" or rTF4 == "1W" ? 100 : rTF4 == "D" or rTF4 == "1D" ? 150 : rTF4 == "240" or rTF4 == "480" or rTF4 == "720" ? 200 : 250
simple int cb5 = rTF5 == "M" or rTF5 == "1M" or rTF5 == "3M" or rTF5 == "6M" or rTF5 == "12M" ? 100 : rTF5 == "W" or rTF5 == "1W" ? 100 : rTF5 == "D" or rTF5 == "1D" ? 150 : rTF5 == "240" or rTF5 == "480" or rTF5 == "720" ? 200 : 250
simple int cb6 = rTF6 == "M" or rTF6 == "1M" or rTF6 == "3M" or rTF6 == "6M" or rTF6 == "12M" ? 100 : rTF6 == "W" or rTF6 == "1W" ? 100 : rTF6 == "D" or rTF6 == "1D" ? 150 : rTF6 == "240" or rTF6 == "480" or rTF6 == "720" ? 200 : 250

[tf1CurrTime, tf1O, tf1H_curr, tf1L_curr, tf1C, tf1Time, tf1H, tf1L, tf1O_C1, tf1C_C1, tf1MagTime, tf1MagH, tf1MagL, tf1O_C2, tf1C_C2, tf1H_C3, tf1L_C3, tf1H_C4, tf1L_C4, tf1ExhHP, tf1ExhHT, tf1ExhLP, tf1ExhLT] = request.security(syminfo.tickerid, rTF1, getDataWithExhaustion(), lookahead = barmerge.lookahead_on, calc_bars_count = cb1)
[tf2CurrTime, tf2O, tf2H_curr, tf2L_curr, tf2C, tf2Time, tf2H, tf2L, tf2O_C1, tf2C_C1, tf2MagTime, tf2MagH, tf2MagL, tf2O_C2, tf2C_C2, tf2H_C3, tf2L_C3, tf2H_C4, tf2L_C4, tf2ExhHP, tf2ExhHT, tf2ExhLP, tf2ExhLT] = request.security(syminfo.tickerid, rTF2, getDataWithExhaustion(), lookahead = barmerge.lookahead_on, calc_bars_count = cb2)
[tf3CurrTime, tf3O, tf3H_curr, tf3L_curr, tf3C, tf3Time, tf3H, tf3L, tf3O_C1, tf3C_C1, tf3MagTime, tf3MagH, tf3MagL, tf3O_C2, tf3C_C2, tf3H_C3, tf3L_C3, tf3H_C4, tf3L_C4, tf3ExhHP, tf3ExhHT, tf3ExhLP, tf3ExhLT] = request.security(syminfo.tickerid, rTF3, getDataWithExhaustion(), lookahead = barmerge.lookahead_on, calc_bars_count = cb3)
[tf4CurrTime, tf4O, tf4H_curr, tf4L_curr, tf4C, tf4Time, tf4H, tf4L, tf4O_C1, tf4C_C1, tf4MagTime, tf4MagH, tf4MagL, tf4O_C2, tf4C_C2, tf4H_C3, tf4L_C3, tf4H_C4, tf4L_C4, tf4ExhHP, tf4ExhHT, tf4ExhLP, tf4ExhLT] = request.security(syminfo.tickerid, rTF4, getDataWithExhaustion(), lookahead = barmerge.lookahead_on, calc_bars_count = cb4)
[tf5CurrTime, tf5O, tf5H_curr, tf5L_curr, tf5C, tf5Time, tf5H, tf5L, tf5O_C1, tf5C_C1, tf5MagTime, tf5MagH, tf5MagL, tf5O_C2, tf5C_C2, tf5H_C3, tf5L_C3, tf5H_C4, tf5L_C4, tf5ExhHP, tf5ExhHT, tf5ExhLP, tf5ExhLT] = request.security(syminfo.tickerid, rTF5, getDataWithExhaustion(), lookahead = barmerge.lookahead_on, calc_bars_count = cb5)
[tf6CurrTime, tf6O, tf6H_curr, tf6L_curr, tf6C, tf6Time, tf6H, tf6L, tf6O_C1, tf6C_C1, tf6MagTime, tf6MagH, tf6MagL, tf6O_C2, tf6C_C2, tf6H_C3, tf6L_C3, tf6H_C4, tf6L_C4, tf6ExhHP, tf6ExhHT, tf6ExhLP, tf6ExhLT] = request.security(syminfo.tickerid, rTF6, getDataWithExhaustion(), lookahead = barmerge.lookahead_on, calc_bars_count = cb6)

// FIX HTF-STRADDLE-1 (T3, probe-validated): completed DAILY sub-bars of the current chart bar, used
// to rebuild the true forming HTF candle for straddled slots. Constant gate: the request either always
// executes (chart above D) or never does - both legal per v6 dynamic requests; charts at/below D never
// need it (probe confirmed clean load on a 1D chart with the gate off).
bool canRecon = timeframe.in_seconds() > timeframe.in_seconds("D")
array<int> reconDT = na
array<float> reconDO = na
array<float> reconDH = na
array<float> reconDL = na
if canRecon
    [rdT, rdO, rdH, rdL] = request.security_lower_tf(syminfo.tickerid, "D", [time, open, high, low])
    reconDT := rdT
    reconDO := rdO
    reconDH := rdH
    reconDL := rdL

[ftfc_up, ftfc_down] = calculateFTFC(rEN1, tf1O, tf1C, rEN2, tf2O, tf2C, rEN3, tf3O, tf3C, rEN4, tf4O, tf4C, rEN5, tf5O, tf5C, rEN6, tf6O, tf6C)

// FIX HTF-STRADDLE-1: calendar timeframes are the only ones that can straddle a chart bar - their
// period boundaries do not have to align with chart bar opens (months/quarters roll mid-week on a
// weekly chart). Intraday/daily/weekly requests from lower charts always align on session opens.
isCalendarTF(string tf) =>
    tf == "W" or tf == "1W" or tf == "M" or tf == "1M" or tf == "3M" or tf == "6M" or tf == "12M"

// FIX HTF-STRADDLE-1 (corrected 2026-07-02, probe-validated): live straddle detection, session aware.
// A futures HTF bar's open is stamped at the prior-evening session (~18:00 ET), so naive month()/
// quarter() on it reads ONE calendar period early (the probe confirmed: the current Q3 quarterly bar
// is stamped 2026-06-30 18:00 -> parses as Q2). Adding +12h before parsing normalizes any period-open
// into its true calendar period, robust across futures (18:00 prior day -> next morning), equities
// (9:30 -> same day) and crypto (00:00 -> same day) without ever overshooting the day.
// Straddle fires ONLY for a calendar slot STRICTLY coarser than the chart (so the chart's own TF can
// never straddle itself) whose served bar's scheduled close has already passed (see shouldStraddle).
OFF_MS = 43200000  // 12h in ms (used by pkeyCal's session-stamp normalization for daily recon)
isStraddleTF(string tf) =>
    isCalendarTF(tf) and timeframe.in_seconds(tf) > chartTfSeconds
// FIX GLUE-2b (2026-07-06): staleness test is now purely time-based: the served HTF bar is
// stale exactly when "now" is past its SCHEDULED CLOSE. The previous calendar-key approach
// (open stamp + 12h) broke on holiday-glued bars: Monday Jul 6's WEEKLY bar opens Thursday
// Jul 2 18:00 ET (Friday's half-session belongs to Monday's trade date), ~84h before its
// true period, so it still parsed as last week and kept a phantom W straddle firing through
// the live Monday session, pinning weekly C1 to the forming week. time_close(tf) is computed
// from the same trade-date calendar that stamps the bars, so it is immune to early opens,
// needs no week-numbering, and is direction-aware (a trade-date bar running AHEAD of the
// calendar, like the glued Friday session inside next week's bar, can never fire it). The
// mid-bar calendar-roll case this feature exists for still works: once the served Q2 bar's
// Jun 30 17:00 close passes, the shift arms for the rest of the straddling chart bar. This
// also obsoletes the 07-02 double-reading hardening: a period-end-afternoon phantom cannot
// occur, because the bar's close has not passed while its session still trades.
shouldStraddle(string tf) =>
    int tc = time_close(tf)
    not na(tc) and timenow > tc

// FIX HTF-STRADDLE-1 (T3): comparable period key for a calendar TF from a timestamp. Bar-side
// timestamps must be passed +12h-normalized (session-stamped opens land in their true calendar day);
// "now" is passed raw, which is valid whenever the hardened straddle test fired, since that test
// requires the raw and +12h readings of now to agree.
pkeyCal(int t, string tf) =>
    m = month(t, "America/New_York")
    y = year(t, "America/New_York")
    w = weekofyear(t, "America/New_York")
    q = m <= 3 ? 1 : m <= 6 ? 2 : m <= 9 ? 3 : 4
    tf == "12M" ? y : tf == "6M" ? y * 10 + (m <= 6 ? 1 : 2) : tf == "3M" ? y * 10 + q : tf == "W" or tf == "1W" ? y * 1000 + w : y * 100 + m

// Classic preview - UNCHANGED from the pre-straddle version (market-closed / previewMode=On projection).
shouldApplyPreview(string tf, bool previewEnabled) =>
    if not previewEnabled
        false
    else
        // Compare last bar's period vs current calendar period
        // This handles: Friday EOD -> Weekend (same period), but also Jan 31 -> Feb 1 weekend (new period)
        currentMonth = month(timenow, "America/New_York")
        currentYear = year(timenow, "America/New_York")
        currentWeek = weekofyear(timenow, "America/New_York")
        currentQuarter = math.ceil(currentMonth / 3)
        currentSemiAnnual = currentMonth <= 6 ? 1 : 2

        barMonth = month(time, "America/New_York")
        barYear = year(time, "America/New_York")
        barWeek = weekofyear(time, "America/New_York")
        barQuarter = math.ceil(barMonth / 3)
        barSemiAnnual = barMonth <= 6 ? 1 : 2

        isWeekly = tf == "W" or tf == "1W"
        isMonthly = tf == "M" or tf == "1M"
        isQuarterly = tf == "3M"
        isSemiAnnual = tf == "6M"
        isYearly = tf == "12M"

        if isWeekly
            differentWeek = currentWeek != barWeek or currentYear != barYear
            dow = dayofweek(timenow, "America/New_York")
            isWeekend = dow == dayofweek.friday or dow == dayofweek.saturday or dow == dayofweek.sunday
            differentWeek or isWeekend
        else if isYearly
            currentYear != barYear
        else if isSemiAnnual
            currentSemiAnnual != barSemiAnnual or currentYear != barYear
        else if isQuarterly
            currentQuarter != barQuarter or currentYear != barYear
        else if isMonthly
            currentMonth != barMonth or currentYear != barYear
        else
            true

applyPreviewShift(int c1Time, float c1H, float c1L, float c1O, float c1C, int ccTime, float ccO, float ccH, float ccL, float ccC, int c2Time, float c2H, float c2L, float c2O, float c2C, float c3H, float c3L, float c4H, float c4L, float exhHP, int exhHT, float exhLP, int exhLT, string tf, bool previewEnabled, bool straddleEnabled) =>
    // FIX HTF-STRADDLE-1: classic preview OR a session-aware live straddle (calendar slot coarser than chart).
    // FIX GLUE-2b (2026-07-06): straddle = now is past the served HTF bar's scheduled close.
    doPreview = shouldApplyPreview(tf, previewEnabled) or (straddleEnabled and isStraddleTF(tf) and shouldStraddle(tf))
    if doPreview
        newC1Time = ccTime
        newC1H = ccH
        newC1L = ccL
        newC1O = ccO
        newC1C = ccC
        newCCTime = time
        newCCO = ccC
        newCCH = ccH
        newCCL = ccL
        newCCC = (ccH + ccL) / 2
        newC2Time = c1Time
        newC2H = c1H
        newC2L = c1L
        newC2O = c1O
        newC2C = c1C
        newC3H = c2H
        newC3L = c2L
        newC4H = c3H
        newC4L = c3L
        // FIX P0-1: realPeriodTime = the unshifted real current period-open (the `ccTime` param),
        // even in preview where ccTime is reused as the synthetic C1. New-period detection uses this.
        TFRawData.new(newC1Time, newC1H, newC1L, newC1O, newC1C, newCCTime, newCCO, newCCH, newCCL, newCCC, newC2Time, newC2H, newC2L, newC2O, newC2C, newC3H, newC3L, newC4H, newC4L, exhHP, exhHT, exhLP, exhLT, true, ccTime)
    else
        TFRawData.new(c1Time, c1H, c1L, c1O, c1C, ccTime, ccO, ccH, ccL, ccC, c2Time, c2H, c2L, c2O, c2C, c3H, c3L, c4H, c4L, exhHP, exhHT, exhLP, exhLT, false, ccTime)

var array<TFRawData> tfRawArr = array.new<TFRawData>(6)
array.set(tfRawArr, 0, applyPreviewShift(tf1Time, tf1H, tf1L, tf1O_C1, tf1C_C1, tf1CurrTime, tf1O, tf1H_curr, tf1L_curr, tf1C, tf1MagTime, tf1MagH, tf1MagL, tf1O_C2, tf1C_C2, tf1H_C3, tf1L_C3, tf1H_C4, tf1L_C4, tf1ExhHP, tf1ExhHT, tf1ExhLP, tf1ExhLT, rTF1, previewNextPeriod, straddleWindowOK))
array.set(tfRawArr, 1, applyPreviewShift(tf2Time, tf2H, tf2L, tf2O_C1, tf2C_C1, tf2CurrTime, tf2O, tf2H_curr, tf2L_curr, tf2C, tf2MagTime, tf2MagH, tf2MagL, tf2O_C2, tf2C_C2, tf2H_C3, tf2L_C3, tf2H_C4, tf2L_C4, tf2ExhHP, tf2ExhHT, tf2ExhLP, tf2ExhLT, rTF2, previewNextPeriod, straddleWindowOK))
array.set(tfRawArr, 2, applyPreviewShift(tf3Time, tf3H, tf3L, tf3O_C1, tf3C_C1, tf3CurrTime, tf3O, tf3H_curr, tf3L_curr, tf3C, tf3MagTime, tf3MagH, tf3MagL, tf3O_C2, tf3C_C2, tf3H_C3, tf3L_C3, tf3H_C4, tf3L_C4, tf3ExhHP, tf3ExhHT, tf3ExhLP, tf3ExhLT, rTF3, previewNextPeriod, straddleWindowOK))
array.set(tfRawArr, 3, applyPreviewShift(tf4Time, tf4H, tf4L, tf4O_C1, tf4C_C1, tf4CurrTime, tf4O, tf4H_curr, tf4L_curr, tf4C, tf4MagTime, tf4MagH, tf4MagL, tf4O_C2, tf4C_C2, tf4H_C3, tf4L_C3, tf4H_C4, tf4L_C4, tf4ExhHP, tf4ExhHT, tf4ExhLP, tf4ExhLT, rTF4, previewNextPeriod, straddleWindowOK))
array.set(tfRawArr, 4, applyPreviewShift(tf5Time, tf5H, tf5L, tf5O_C1, tf5C_C1, tf5CurrTime, tf5O, tf5H_curr, tf5L_curr, tf5C, tf5MagTime, tf5MagH, tf5MagL, tf5O_C2, tf5C_C2, tf5H_C3, tf5L_C3, tf5H_C4, tf5L_C4, tf5ExhHP, tf5ExhHT, tf5ExhLP, tf5ExhLT, rTF5, previewNextPeriod, straddleWindowOK))
array.set(tfRawArr, 5, applyPreviewShift(tf6Time, tf6H, tf6L, tf6O_C1, tf6C_C1, tf6CurrTime, tf6O, tf6H_curr, tf6L_curr, tf6C, tf6MagTime, tf6MagH, tf6MagL, tf6O_C2, tf6C_C2, tf6H_C3, tf6L_C3, tf6H_C4, tf6L_C4, tf6ExhHP, tf6ExhHT, tf6ExhLP, tf6ExhLT, rTF6, previewNextPeriod, straddleWindowOK))

// FIX HTF-STRADDLE-1 (T3, probe-validated 2026-07-02): for a straddle-shifted slot, rebuild the TRUE
// forming HTF candle from the chart bar's daily sub-bars plus the live close, replacing the synthetic
// preview candle. The probe reproduced the finer charts' CC exactly (BTC 1W: f2d including the
// sub-C1-low wick the earlier estimate missed). Gated on shouldStraddle, so a classic next-period
// preview (weekend W/M projection, whose period has no dailies yet) is never overridden; n == 0
// leaves the "?" candle. isPreview stays TRUE: the every-tick preview re-latch is what keeps the
// shifted C1/C2 alive across realtime rollback, and alerts stay conservatively suppressed.
// FIX GLUE-2b (2026-07-06): staleness re-check is time-based (see shouldStraddle).
var array<bool> reconOKArr = array.new<bool>(6, false)
if barstate.islast
    for iS = 0 to 5
        array.set(reconOKArr, iS, false)
        cfgS = array.get(tfConfigs, iS)
        rawS = array.get(tfRawArr, iS)
        if cfgS.enabled and not na(rawS) and rawS.isPreview and canRecon and not na(reconDT) and array.size(reconDT) > 0 and isStraddleTF(cfgS.tf) and shouldStraddle(cfgS.tf)
            int pNow = pkeyCal(timenow, cfgS.tf)
            float rO = na
            float rH = na
            float rL = na
            int rT = na
            int n = 0
            for jS = 0 to array.size(reconDT) - 1
                if pkeyCal(array.get(reconDT, jS) + OFF_MS, cfgS.tf) == pNow
                    if n == 0
                        rO := array.get(reconDO, jS)
                        rT := array.get(reconDT, jS)
                    rH := na(rH) ? array.get(reconDH, jS) : math.max(rH, array.get(reconDH, jS))
                    rL := na(rL) ? array.get(reconDL, jS) : math.min(rL, array.get(reconDL, jS))
                    n += 1
            if n > 0
                rawS.ccTime := rT
                rawS.ccO := rO
                rawS.ccC := close
                rawS.ccH := math.max(rH, close)
                rawS.ccL := math.min(rL, close)
                array.set(reconOKArr, iS, true)

var array<ProcessingResult> tfResults = array.new<ProcessingResult>(6)

// ============================================================================
// SECTION 7: MAIN PROCESSING FUNCTION
// ============================================================================

// Detect bar type and Failed 2 status (used by processTimeframe and table rendering)
detectBarTypeAndFailed(prevH, prevL, currH, currL, currO, currC) =>
    if na(prevH) or na(prevL) or na(currH) or na(currL) or na(currO) or na(currC) or currH < currL or prevH < prevL
        ["", false]
    else
        aboveOpen = currC > currO
        gapUp = currO > prevH
        gapDown = currO < prevL
        is2up = false
        is2down = false
        isInside = false
        isOutside = false
        if gapUp
            if currL < prevL
                isOutside := true
            else
                is2up := true
        else if gapDown
            if currH > prevH
                isOutside := true
            else
                is2down := true
        else
            highCrossed = currH > prevH
            lowCrossed = currL < prevL
            if highCrossed and lowCrossed
                isOutside := true
            else if highCrossed and not lowCrossed
                is2up := true
            else if lowCrossed and not highCrossed
                is2down := true
            else
                isInside := true
        isFailed = false
        insidePrevRange = currC <= prevH and currC >= prevL
        // FIX P1-f: honor enableFailed2Detection here too. The main engine (detectFailed2)
        // is gated on this input, but the table/label path (detectBarTypeAndFailed) was not,
        // so with detection OFF the table showed f2u/f2d while the engine emitted plain 2u/2d.
        if enableFailed2Detection and (is2up or is2down)
            if failed_2_method == "Open"
                if is2up
                    isFailed := not aboveOpen
                else if is2down
                    isFailed := aboveOpen
            else if failed_2_method == "Reclaim"
                isFailed := insidePrevRange
            else if failed_2_method == "Reclaim + Open"
                if is2up
                    isFailed := not aboveOpen and insidePrevRange
                else if is2down
                    isFailed := aboveOpen and insidePrevRange
            else
                if is2up
                    isFailed := not aboveOpen or insidePrevRange
                else if is2down
                    isFailed := aboveOpen or insidePrevRange
        barType = ""
        if isInside
            barType := aboveOpen ? "1u" : "1d"
        else if is2up
            barType := isFailed ? "F2u" : "2u"
        else if is2down
            barType := isFailed ? "F2d" : "2d"
        else if isOutside
            barType := aboveOpen ? "3u" : "3d"
        [barType, isFailed]

computeSignalState(data, tf, raw, showOpen, populateDebug, debugTfStr) =>
    tfTime = raw.c1Time
    tfH = raw.c1H
    tfL = raw.c1L
    tfO_C1 = raw.c1O
    tfC_C1 = raw.c1C
    tfCurrTime = raw.ccTime
    tfO = raw.ccO
    tfH_curr = raw.ccH
    tfL_curr = raw.ccL
    tfC = raw.ccC
    tfMagTime = raw.c2Time
    tfMagH = raw.c2H
    tfMagL = raw.c2L
    tfO_C2 = raw.c2O
    tfC_C2 = raw.c2C
    tfH_C3 = raw.c3H
    tfL_C3 = raw.c3L
    tfH_C4 = raw.c4H
    tfL_C4 = raw.c4L
    tfExhHP = raw.exhHP
    tfExhHT = raw.exhHT
    tfExhLP = raw.exhLP
    tfExhLT = raw.exhLT
    isInPreview = raw.isPreview
    // FIX P0-1: detect a new HTF period PER SLOT by comparing the security-returned real
    // period-open time against a per-slot stored value. The old `ta.change(time(tf)) != 0`
    // shared ONE history buffer across all 6 loop iterations (computeSignalState has a single
    // written call site inside `for i = 5 to 0`), so its [1] compared this bar's time(tfA)
    // against the previous bar's last-iteration time(tfB) and fired on nearly every bar -
    // constantly wiping the crossed-flag "came back" memory and the sticky stops. No ta.* state.
    // FIX P1-d: also re-latch when preview has just EXITED mid-period (no real rollover);
    // otherwise the preview-shifted prevHigh/prevMag persist until the next genuine period.
    bool prevWasPreview = data.wasPreview
    newPeriod = na(data.lastPeriodStart) or raw.realPeriodTime != data.lastPeriodStart
    if newPeriod or (isInPreview and barstate.islast) or (prevWasPreview and not isInPreview)
        data.magHighCrossed := false
        data.magLowCrossed := false
        data.exhHighCrossed := false
        data.exhLowCrossed := false
        data.stopHighTriggered := false
        data.stopLowTriggered := false
        data.stopHighPrice := na
        data.stopLowPrice := na
        data.prevHigh := tfH
        data.prevLow := tfL
        data.prevTime := tfTime
        data.prevMagHigh := tfMagH
        data.prevMagLow := tfMagL
        data.prevMagTime := tfMagTime
        data.exhHighPrice := tfExhHP
        data.exhHighTime := tfExhHT
        data.exhLowPrice := tfExhLP
        data.exhLowTime := tfExhLT
        data.lastPeriodStart := raw.realPeriodTime
    data.wasPreview := isInPreview
    data.currOpen := tfO
    validTimeframe = timeframe.in_seconds() <= timeframe.in_seconds(tf)
    float retHigh = na, float retLow = na, float retOpen = na, float retMagHigh = na, float retMagLow = na, float retExhHigh = na, float retExhLow = na
    color retHighColor = na, color retLowColor = na, color retOpenColor = na, color retMagHighColor = na, color retMagLowColor = na, color retExhHighColor = na, color retExhLowColor = na
    bool retDrawHigh = false, bool retDrawLow = false, bool retDrawOpen = false, bool retDrawMagHigh = false, bool retDrawMagLow = false, bool retDrawExhHigh = false, bool retDrawExhLow = false
    int retTime = na, bool retIsF2u = false, bool retIsF2d = false, bool retC1Is3 = false
    string retC3Num = "", string retC2Num = "", string retC1Num = "", string retC1Type = "", string retCCType = ""
    bool retC1WasF2 = false, bool retC2WasF2 = false
    bool retSignalInForceHigh = false, bool retSignalInForceLow = false
    float retStopHigh = na, float retStopLow = na, bool retDrawStopHigh = false, bool retDrawStopLow = false
    bool retPreF2u = false, bool retPreF2d = false, float retF2OpenPrice = na
    string retHighStyle = "Dashed", string retLowStyle = "Dashed"
    if validTimeframe and not na(data.prevHigh)
        if not na(data.prevMagHigh) and not data.magHighCrossed and tfH_curr >= data.prevMagHigh
            data.magHighCrossed := true
        if not na(data.prevMagLow) and not data.magLowCrossed and tfL_curr <= data.prevMagLow
            data.magLowCrossed := true
        if not na(data.exhHighPrice) and not data.exhHighCrossed and tfH_curr >= data.exhHighPrice
            data.exhHighCrossed := true
        if not na(data.exhLowPrice) and not data.exhLowCrossed and tfL_curr <= data.exhLowPrice
            data.exhLowCrossed := true
        // FIX P1-a: run the detection + sticky-stop latch on every bar when stops are enabled,
        // not only on barstate.islast. The stop "locks for the period" semantic requires the
        // latch to have fired on the bars where the signal went in force; with the old islast
        // gate, a chart reload (those bars now historical) never re-armed the latch and the
        // locked stop vanished. Gated on showStopLevels so the common case (stops off, default)
        // keeps the original islast-only cost - no performance change unless stops are on.
        if barstate.islast or showStopLevels
            [isHammer, isShooter] = detectPatterns(tfO_C1, tfH, tfL, tfC_C1)
            isInside = isInsideBar(tfH, tfL, tfMagH, tfMagL)
            data.c1IsInside := isInside
            data.c1IsHammer := isHammer
            data.c1IsShooter := isShooter
            [cc_is_2u, cc_is_2d, cc_is_3, is_f2u, is_f2d, pre_f2u, pre_f2d] = detectFailed2(data.prevHigh, data.prevLow, tfO, tfC, tfH_curr, tfL_curr, failed_2_method)
            retIsF2u := is_f2u
            retIsF2d := is_f2d
            retC3Num := calcBarNum(tfH_C4, tfL_C4, tfH_C3, tfL_C3)
            retC2Num := calcBarNum(tfH_C3, tfL_C3, tfMagH, tfMagL)
            retC1Num := calcBarNum(tfMagH, tfMagL, tfH, tfL)
            string c1Type = calcBarType(tfMagH, tfMagL, tfH, tfL, tfO_C1, tfC_C1)
            if is_f2u
                retCCType := "F2u"
            else if is_f2d
                retCCType := "F2d"
            else if cc_is_3
                retCCType := tfC > tfO ? "3u" : "3d"
            else if cc_is_2u
                retCCType := "2u"
            else if cc_is_2d
                retCCType := "2d"
            else
                retCCType := "1"
            retC1Type := c1Type
            // Detect if C1/C2 were themselves Failed 2s (for label accuracy)
            [_c1BarType, c1f2flag] = detectBarTypeAndFailed(tfMagH, tfMagL, tfH, tfL, tfO_C1, tfC_C1)
            [_c2BarType, c2f2flag] = detectBarTypeAndFailed(tfH_C3, tfL_C3, tfMagH, tfMagL, tfO_C2, tfC_C2)
            retC1WasF2 := c1f2flag
            retC2WasF2 := c2f2flag
            cc_broke_high = tfH_curr > data.prevHigh
            cc_broke_low = tfL_curr < data.prevLow
            c1_broke_mag_high = tfH > tfMagH
            c1_broke_mag_low = tfL < tfMagL
            c1_was_2u = c1_broke_mag_high and not c1_broke_mag_low
            c1_was_2d = c1_broke_mag_low and not c1_broke_mag_high
            c2_broke_c3_high = tfMagH > tfH_C3
            c2_broke_c3_low = tfMagL < tfL_C3
            c2_was_2u = c2_broke_c3_high and not c2_broke_c3_low
            c2_was_2d = c2_broke_c3_low and not c2_broke_c3_high
            c2_was_3 = c2_broke_c3_high and c2_broke_c3_low
            c2_closed_up = tfC_C2 > tfO_C2
            c2_closed_down = tfC_C2 < tfO_C2

            // C1 F2d (bullish): broke C2 low but closed back inside range
            // C1 F2u (bearish): broke C2 high but closed back inside range
            c1_was_f2d = c1_was_2d and tfC_C1 > tfMagL  // broke low, closed above it
            c1_was_f2u = c1_was_2u and tfC_C1 < tfMagH  // broke high, closed below it

            c1_was_neutral = not c1_was_2u and not c1_was_2d
            inside_bullish = isInside and c2_closed_up
            inside_bearish = isInside and c2_closed_down
            is_hammer_reversal = c1_was_2d or inside_bearish or (c1_was_neutral and not isInside)
            is_shooter_reversal = c1_was_2u or inside_bullish or (c1_was_neutral and not isInside)
            is_hammer_momo = c1_was_2u or inside_bullish
            is_shooter_momo = c1_was_2d or inside_bearish
            hammer_shows = isHammer and (is_hammer_reversal or (is_hammer_momo and showContinuations))
            shooter_shows = isShooter and (is_shooter_reversal or (is_shooter_momo and showContinuations))
            inside_cont_high_passes_momo = not insideContOnlyMomo or isHammer
            inside_cont_low_passes_momo = not insideContOnlyMomo or isShooter
            twotwo_cont_high_passes_momo = (not twoTwoContOnlyMomo or isHammer) and not c2_was_3
            twotwo_cont_low_passes_momo = (not twoTwoContOnlyMomo or isShooter) and not c2_was_3
            c1_is_3 = tfH > tfMagH and tfL < tfMagL
            retC1Is3 := c1_is_3
            is_22_cont_high = c1_was_2u and c2_was_2u and showContinuations and twotwo_cont_high_passes_momo
            is_22_cont_low = c1_was_2d and c2_was_2d and showContinuations and twotwo_cont_low_passes_momo
            is_32_cont_high = c1_is_3 and cc_is_2u and showRangeExpansions
            is_32_cont_low = c1_is_3 and cc_is_2d and showRangeExpansions
            twotwo_rev_high_passes = (not reversalsRequireActionable or isHammer) and (not reversalsRequireC1F2 or c1_was_f2d)
            twotwo_rev_low_passes = (not reversalsRequireActionable or isShooter) and (not reversalsRequireC1F2 or c1_was_f2u)

            p3_high_passes_ftfc = not p3RequireFTFC or ftfc_up
            p3_low_passes_ftfc = not p3RequireFTFC or ftfc_down
            // FIX CSS-1: a flat (doji) C2 counts as an inside reversal in-force, matching the draw path
            // (shouldDrawC1Level treats flat C2 as a reversal). The old strict c2_closed_down/c2_closed_up
            // left flat-C2 inside reversals drawn + alerting but never marked in-force, so the table, the
            // sticky stop latch, and the "only when in force" target/TAW gates all disagreed with the line.
            signal_in_force_high = (isInside and not c2_closed_up and cc_is_2u) or (inside_bullish and showInsideContinuations and inside_cont_high_passes_momo and cc_is_2u) or (hammer_shows and cc_broke_high) or (showAllReversals and c1_was_2d and cc_is_2u and twotwo_rev_high_passes) or (showContinuations and c1_was_2u and cc_is_2u and twotwo_cont_high_passes_momo) or (showRangeExpansions and c1_is_3 and cc_is_2u) or (show_p3_toggle and is_f2d and p3_high_passes_ftfc)
            signal_in_force_low = (isInside and not c2_closed_down and cc_is_2d) or (inside_bearish and showInsideContinuations and inside_cont_low_passes_momo and cc_is_2d) or (shooter_shows and cc_broke_low) or (showAllReversals and c1_was_2u and cc_is_2d and twotwo_rev_low_passes) or (showContinuations and c1_was_2d and cc_is_2d and twotwo_cont_low_passes_momo) or (showRangeExpansions and c1_is_3 and cc_is_2d) or (show_p3_toggle and is_f2u and p3_low_passes_ftfc)
            // For directional filter, signal is only "currently in force" if price is still beyond the trigger
            currently_above_trigger = tfC > data.prevHigh
            currently_below_trigger = tfC < data.prevLow
            three_2u_suppress_low = c1_is_3 and cc_is_2u and not is_f2u
            three_2d_suppress_high = c1_is_3 and cc_is_2d and not is_f2d
            cc_3u_suppress_low = cc_is_3 and tfC > tfO
            cc_3d_suppress_high = cc_is_3 and tfC <= tfO
            inside_breakout_suppress_low = isInside and cc_broke_high and not is_f2u and not cc_is_3
            inside_breakout_suppress_high = isInside and cc_broke_low and not is_f2d and not cc_is_3
            // F2 filter for 2-2 reversals: suppress draw when C1 wasn't itself a Failed 2
            // Skip suppression when CC has an active F2 (range reclaim signal draws both sides)
            twotwo_rev_f2_suppress_high = reversalsRequireC1F2 and c1_was_2d and not c1_was_f2d and not c1_is_3 and not isInside and not is_f2u and not is_f2d
            twotwo_rev_f2_suppress_low = reversalsRequireC1F2 and c1_was_2u and not c1_was_f2u and not c1_is_3 and not isInside and not is_f2u and not is_f2d
            [drawHighResult, isHighContinuation] = shouldDrawC1High(isInside, isHammer, isShooter, cc_is_2u, cc_is_2d, cc_is_3, is_f2u, is_f2d, ftfc_up, ftfc_down, c1_was_2u, c1_was_2d, c1_is_3, c2_closed_up, c2_closed_down, c2_was_3)
            [drawLowResult, isLowContinuation] = shouldDrawC1Low(isInside, isShooter, isHammer, cc_is_2u, cc_is_2d, cc_is_3, is_f2u, is_f2d, ftfc_up, ftfc_down, c1_was_2u, c1_was_2d, c1_is_3, c2_closed_up, c2_closed_down, c2_was_3)
            drawHigh = drawHighResult and not three_2d_suppress_high and not inside_breakout_suppress_high and not twotwo_rev_f2_suppress_high
            drawLow = drawLowResult and not three_2u_suppress_low and not inside_breakout_suppress_low and not twotwo_rev_f2_suppress_low
            // signalInForce must also require drawHigh/drawLow to align with user settings
            // If exhDisablesInForce is on, exhausted signals are not considered in force
            // If no exhaustion level exists, magnitude serves the same function
            // F2 signals are "in force" when the F2 detection conditions are met (per user's method),
            //         without requiring price beyond the opposite trigger. For non-F2 signals, still require
            //         price beyond trigger (currently_above/below_trigger).
            bool magFallbackHigh = na(data.exhHighPrice) and not na(data.prevMagHigh) and data.magHighCrossed
            bool magFallbackLow = na(data.exhLowPrice) and not na(data.prevMagLow) and data.magLowCrossed
            exhDisablesHigh = exhDisablesInForce and (data.exhHighCrossed or magFallbackHigh)
            exhDisablesLow = exhDisablesInForce and (data.exhLowCrossed or magFallbackLow)
            // Non-F2 in-force requires price beyond trigger; F2 in-force is confirmed by detection itself
            non_f2_in_force_high = signal_in_force_high and not is_f2d and currently_above_trigger
            f2_in_force_high = show_p3_toggle and is_f2d and drawHigh and p3_high_passes_ftfc
            non_f2_in_force_low = signal_in_force_low and not is_f2u and currently_below_trigger
            f2_in_force_low = show_p3_toggle and is_f2u and drawLow and p3_low_passes_ftfc
            // Raw in-force (before exhaustion gate) - used for stop triggers
            // Exhaustion disabling affects signal display/filtering, not protective stops
            bool rawInForceHigh = (non_f2_in_force_high or f2_in_force_high) and drawHigh
            bool rawInForceLow = (non_f2_in_force_low or f2_in_force_low) and drawLow
            retSignalInForceHigh := rawInForceHigh and not exhDisablesHigh
            retSignalInForceLow := rawInForceLow and not exhDisablesLow
            // Sticky stop triggers - once in force this period, stop persists
            // F2 signals always use CC (the failed candle defines the trade)
            // Non-F2: CC mode = breakout bar's opposite side, C1 mode = prior bar's opposite side
            // CC mode: update each bar to track running CC extreme (consistent across chart TFs)
            // C1 mode: lock on first trigger (stable, uses completed prior candle)
            useCC = stopReference == "CC"
            if rawInForceHigh and not data.stopHighTriggered
                data.stopHighTriggered := true
                data.stopHighPrice := (is_f2d or useCC) ? tfL_curr : data.prevLow
            else if data.stopHighTriggered and (is_f2d or useCC)
                data.stopHighPrice := tfL_curr
            if rawInForceLow and not data.stopLowTriggered
                data.stopLowTriggered := true
                data.stopLowPrice := (is_f2u or useCC) ? tfH_curr : data.prevHigh
            else if data.stopLowTriggered and (is_f2u or useCC)
                data.stopLowPrice := tfH_curr
            // Break even: move stop to entry trigger when magnitude/exhaustion is hit
            // Bullish stop (stopHigh) protects long → BE = prevHigh (entry level, moves stop up)
            // Bearish stop (stopLow) protects short → BE = prevLow (entry level, moves stop down)
            if data.stopHighTriggered
                bool magHitHigh = data.magHighCrossed and not na(data.prevMagHigh)
                bool exhHitHigh = data.exhHighCrossed and not na(data.exhHighPrice)
                bool noMagHigh = na(data.prevMagHigh)
                bool noExhHigh = na(data.exhHighPrice)
                bool beMagTriggerHigh = stopBEatMag and (magHitHigh or (noMagHigh and exhHitHigh))
                bool beExhTriggerHigh = stopBEatExh and (exhHitHigh or (noExhHigh and magHitHigh))
                if beMagTriggerHigh or beExhTriggerHigh
                    data.stopHighPrice := data.prevHigh
            if data.stopLowTriggered
                bool magHitLow = data.magLowCrossed and not na(data.prevMagLow)
                bool exhHitLow = data.exhLowCrossed and not na(data.exhLowPrice)
                bool noMagLow = na(data.prevMagLow)
                bool noExhLow = na(data.exhLowPrice)
                bool beMagTriggerLow = stopBEatMag and (magHitLow or (noMagLow and exhHitLow))
                bool beExhTriggerLow = stopBEatExh and (exhHitLow or (noExhLow and magHitLow))
                if beMagTriggerLow or beExhTriggerLow
                    data.stopLowPrice := data.prevLow
            data.c1IsHighContinuation := isHighContinuation
            data.c1IsLowContinuation := isLowContinuation
            cc_above_open_for_mag = tfC > tfO
            threeExp_show_mag_high = cc_is_3 and show3Expansions and show3ExpMagnitude and (not threeExpRequireFTFC or ftfc_up) and cc_above_open_for_mag
            threeExp_show_mag_low = cc_is_3 and show3Expansions and show3ExpMagnitude and (not threeExpRequireFTFC or ftfc_down) and not cc_above_open_for_mag
            // FIX CSS-2: require global showExhaustion, mirroring how outside-bar magnitude requires
            // global showMagnitude (via force_mag). Was asymmetric: OB exhaustion drew with Show Exhaustion off.
            threeExp_show_exh_high = cc_is_3 and show3Expansions and showExhaustion and show3ExpExhaustion and (not threeExpRequireFTFC or ftfc_up) and cc_above_open_for_mag
            threeExp_show_exh_low = cc_is_3 and show3Expansions and showExhaustion and show3ExpExhaustion and (not threeExpRequireFTFC or ftfc_down) and not cc_above_open_for_mag
            // Gate inside→3 mag behind show3Expansions to prevent orphan magnitude when 3-exp display is off
            inside_3u_setup = isInside and cc_is_3 and cc_above_open_for_mag and show3Expansions and (not threeExpRequireFTFC or ftfc_up)
            inside_3d_setup = isInside and cc_is_3 and not cc_above_open_for_mag and show3Expansions and (not threeExpRequireFTFC or ftfc_down)
            // Directional F2 mag/exh suppression
            // F2u (failed bullish breakout → bearish): suppress bullish mag/exh (upside failed)
            // F2d (failed bearish breakout → bullish): suppress bearish mag/exh (downside failed)
            p3_suppress_mag_high = is_f2u
            p3_suppress_mag_low = is_f2d
            cont_suppress_mag_high = is_22_cont_high or is_32_cont_high
            cont_suppress_mag_low = is_22_cont_low or is_32_cont_low

            high_suppress_base = three_2d_suppress_high or inside_breakout_suppress_high or cc_3d_suppress_high
            low_suppress_base = three_2u_suppress_low or inside_breakout_suppress_low or cc_3u_suppress_low

            exh_high_valid = not na(data.exhHighPrice) and data.exhHighPrice != data.prevHigh
            exh_low_valid = not na(data.exhLowPrice) and data.exhLowPrice != data.prevLow

            cont_show_exh_high = showExhaustion and drawHigh and cont_suppress_mag_high
            cont_show_exh_low = showExhaustion and drawLow and cont_suppress_mag_low

            force_mag_high = showMagnitude and (drawHigh or inside_3u_setup or threeExp_show_mag_high) and not p3_suppress_mag_high and not cont_suppress_mag_high
            force_mag_low = showMagnitude and (drawLow or inside_3d_setup or threeExp_show_mag_low) and not p3_suppress_mag_low and not cont_suppress_mag_low
            mag_high_passes_ftfc = true
            mag_low_passes_ftfc = true
            price_hit_mag_high = tfH_curr >= data.prevMagHigh
            price_hit_mag_low = tfL_curr <= data.prevMagLow
            exh_high_mag_filter = not exhRequiresMagHit or price_hit_mag_high
            exh_low_mag_filter = not exhRequiresMagHit or price_hit_mag_low

            exh_high_base_condition = showExhaustion and mag_high_passes_ftfc and exh_high_mag_filter and exh_high_valid and data.exhHighPrice != data.prevLow and data.exhHighPrice != data.prevMagLow and not high_suppress_base and not p3_suppress_mag_high
            exh_low_base_condition = showExhaustion and mag_low_passes_ftfc and exh_low_mag_filter and exh_low_valid and data.exhLowPrice != data.prevHigh and data.exhLowPrice != data.prevMagHigh and not low_suppress_base and not p3_suppress_mag_low
            drawExhHigh = ((exh_high_base_condition and drawHigh) or (exh_high_base_condition and data.exhHighCrossed) or (cont_show_exh_high and exh_high_valid) or (threeExp_show_exh_high and exh_high_valid)) and (not exhOnlyWhenInForce or rawInForceHigh)
            drawExhLow = ((exh_low_base_condition and drawLow) or (exh_low_base_condition and data.exhLowCrossed) or (cont_show_exh_low and exh_low_valid) or (threeExp_show_exh_low and exh_low_valid)) and (not exhOnlyWhenInForce or rawInForceLow)
            exh_high_same_as_mag = drawExhHigh and data.exhHighPrice == data.prevMagHigh
            exh_low_same_as_mag = drawExhLow and data.exhLowPrice == data.prevMagLow
            mag_high_same_as_trigger = data.prevMagHigh == data.prevHigh
            mag_low_same_as_trigger = data.prevMagLow == data.prevLow
            // shouldDrawMagnitude fallback requires drawHigh/drawLow to prevent orphan mag lines
            //         when signal toggles are off but signal_in_force fires
            drawMagHigh = ((force_mag_high and mag_high_passes_ftfc and not c1_broke_mag_high) or (drawHigh and shouldDrawMagnitude(c1_broke_mag_high, signal_in_force_high))) and not high_suppress_base and not exh_high_same_as_mag and not p3_suppress_mag_high and not cont_suppress_mag_high and not mag_high_same_as_trigger and (not magOnlyWhenInForce or rawInForceHigh)
            drawMagLow = ((force_mag_low and mag_low_passes_ftfc and not c1_broke_mag_low) or (drawLow and shouldDrawMagnitude(c1_broke_mag_low, signal_in_force_low))) and not low_suppress_base and not exh_low_same_as_mag and not p3_suppress_mag_low and not cont_suppress_mag_low and not mag_low_same_as_trigger and (not magOnlyWhenInForce or rawInForceLow)
            if populateDebug
                debugData.tfLabel := getTimeframeLabel(debugTfStr)
                debugData.c1_was_2u := c1_was_2u
                debugData.c1_was_2d := c1_was_2d
                debugData.c1_is_3 := c1_is_3
                debugData.isHammer := isHammer
                debugData.isShooter := isShooter
                debugData.isInside := isInside
                debugData.cc_is_2u := cc_is_2u
                debugData.cc_is_2d := cc_is_2d
                debugData.cc_is_3 := cc_is_3
                debugData.cc_broke_high := cc_broke_high
                debugData.cc_broke_low := cc_broke_low
                debugData.is_f2u := is_f2u
                debugData.is_f2d := is_f2d
                debugData.pre_f2u := pre_f2u
                debugData.pre_f2d := pre_f2d
                debugData.ftfc_up := ftfc_up
                debugData.ftfc_down := ftfc_down
                debugData.is_hammer_reversal := is_hammer_reversal
                debugData.is_hammer_momo := is_hammer_momo
                debugData.hammer_shows := hammer_shows
                debugData.is_shooter_reversal := is_shooter_reversal
                debugData.is_shooter_momo := is_shooter_momo
                debugData.shooter_shows := shooter_shows
                debugData.drawHigh := drawHigh
                debugData.drawLow := drawLow
                debugData.drawMagHigh := drawMagHigh
                debugData.drawMagLow := drawMagLow
                debugData.drawExhHigh := drawExhHigh
                debugData.drawExhLow := drawExhLow
                debugData.force_mag_high := force_mag_high
                debugData.force_mag_low := force_mag_low
                debugData.mag_high_passes_ftfc := mag_high_passes_ftfc
                debugData.mag_low_passes_ftfc := mag_low_passes_ftfc
            highColor = color.gray
            lowColor = color.gray
            highStyle = "Dashed"
            lowStyle = "Dashed"
            cc_is_inside = not cc_broke_high and not cc_broke_low and not cc_is_3
            if drawHigh
                if cc_is_3
                    cc_close_below = tfC < data.prevLow
                    cc_closing_up = tfC > tfO
                    highColor := COLOR_3_HIGH
                    if show3Expansions and cc_closing_up and not cc_close_below
                        highStyle := "Dashed"
                    else
                        highStyle := "Solid"
                else if is_f2d
                    highColor := COLOR_FAILED_2D
                    highStyle := "Solid"
                else if c1_is_3 and cc_is_2u
                    highColor := colorBullishActive
                    highStyle := "Dashed"
                else if c1_is_3 and cc_is_inside
                    highColor := COLOR_INSIDE_HIGH
                    highStyle := "Dashed"
                else if c1_is_3
                    highColor := COLOR_3_HIGH
                    highStyle := "Solid"
                else if cc_broke_high
                    highColor := colorBullishActive
                    highStyle := "Dashed"
                else if cc_is_inside
                    highColor := COLOR_INSIDE_HIGH
                    highStyle := "Dashed"
                else
                    highColor := colorBullishActive
                    highStyle := "Dashed"
            if drawLow
                if cc_is_3
                    cc_close_above = tfC > data.prevHigh
                    cc_closing_down = tfC < tfO
                    lowColor := COLOR_3_LOW
                    if show3Expansions and cc_closing_down and not cc_close_above
                        lowStyle := "Dashed"
                    else
                        lowStyle := "Solid"
                else if is_f2u
                    lowColor := COLOR_FAILED_2U
                    lowStyle := "Solid"
                else if c1_is_3 and cc_is_2d
                    lowColor := colorBearishActive
                    lowStyle := "Dashed"
                else if c1_is_3 and cc_is_inside
                    lowColor := COLOR_INSIDE_LOW
                    lowStyle := "Dashed"
                else if c1_is_3
                    lowColor := COLOR_3_LOW
                    lowStyle := "Solid"
                else if cc_broke_low
                    lowColor := colorBearishActive
                    lowStyle := "Dashed"
                else if cc_is_inside
                    lowColor := COLOR_INSIDE_LOW
                    lowStyle := "Dashed"
                else
                    lowColor := colorBearishActive
                    lowStyle := "Dashed"
            // F2 override: side being failed goes dashed (color preserved from pattern logic)
            if is_f2u or pre_f2u
                highStyle := "Dashed"
            if is_f2d or pre_f2d
                lowStyle := "Dashed"
            retHighStyle := highStyle
            retLowStyle := lowStyle
            // Compute phase: populate return values from draw conditions
            periodDuration = timeframe.in_seconds(tf) * 1000
            lineEndTime = tfCurrTime + periodDuration
            if drawHigh
                retHigh := data.prevHigh
                retHighColor := highColor
                retDrawHigh := true
                retTime := lineEndTime
            if drawLow
                retLow := data.prevLow
                retLowColor := lowColor
                retDrawLow := true
                retTime := lineEndTime
            bool f2SuppressesOpen = (is_f2u or is_f2d or pre_f2u or pre_f2d) and showF2OpenLine
            if showOpen and (drawHigh or drawLow) and not f2SuppressesOpen
                retOpen := data.currOpen
                retOpenColor := color.gray
                retDrawOpen := true
                retTime := lineEndTime
            if drawMagHigh
                retMagHigh := data.prevMagHigh
                retMagHighColor := data.magHighCrossed ? COLOR_HIT_HIGH : colorBullishActive
                retDrawMagHigh := true
                retTime := lineEndTime
            if drawMagLow
                retMagLow := data.prevMagLow
                retMagLowColor := data.magLowCrossed ? COLOR_HIT_LOW : colorBearishActive
                retDrawMagLow := true
                retTime := lineEndTime
            if drawExhHigh
                retExhHigh := data.exhHighPrice
                retExhHighColor := data.exhHighCrossed ? COLOR_HIT_HIGH : colorBullishActive
                retDrawExhHigh := true
                retTime := lineEndTime
            if drawExhLow
                retExhLow := data.exhLowPrice
                retExhLowColor := data.exhLowCrossed ? COLOR_HIT_LOW : colorBearishActive
                retDrawExhLow := true
                retTime := lineEndTime
            drawStopHigh = showStopLevels and data.stopHighTriggered
            drawStopLow = showStopLevels and data.stopLowTriggered
            if drawStopHigh and not na(data.stopHighPrice)
                retStopHigh := data.stopHighPrice
                retDrawStopHigh := true
            if drawStopLow and not na(data.stopLowPrice)
                retStopLow := data.stopLowPrice
                retDrawStopLow := true
            // F2 open price for pre-F2 and confirmed F2
            if pre_f2d and not na(tfO)
                retPreF2d := true
                retF2OpenPrice := tfO
            else if is_f2d and not na(tfO)
                retF2OpenPrice := tfO
            if pre_f2u and not na(tfO)
                retPreF2u := true
                retF2OpenPrice := tfO
            else if is_f2u and not na(tfO)
                retF2OpenPrice := tfO
    tfLabelStr = getTimeframeLabel(tf)
    ProcessingResult.new(retHigh, retLow, retOpen, retMagHigh, retMagLow, retExhHigh, retExhLow, retHighColor, retLowColor, retOpenColor, retMagHighColor, retMagLowColor, retExhHighColor, retExhLowColor, retDrawHigh, retDrawLow, retDrawOpen, retDrawMagHigh, retDrawMagLow, retDrawExhHigh, retDrawExhLow, retTime, tfLabelStr, retIsF2u, retIsF2d, retC1Is3, retC3Num, retC2Num, retC1Num, retC1Type, retCCType, retSignalInForceHigh, retSignalInForceLow, retStopHigh, retStopLow, retDrawStopHigh, retDrawStopLow, retPreF2u, retPreF2d, retF2OpenPrice, tfCurrTime, retC1WasF2, retC2WasF2, retHighStyle, retLowStyle)


// Render signal levels based on fully-resolved ProcessingResult flags
// Called AFTER directional filter has modified flags, so no draw-then-delete needed
renderSignalLevels(TimeframeData data, ProcessingResult r, int lineWidth, int lineEndTime) =>
    if barstate.islast and not na(r)
        // High trigger line
        if r.drawHigh
            data.highLine := updateOrCreateLine(data.highLine, data.prevTime, r.high, lineEndTime, r.high, r.highColor, lineWidth, getLineStyle(r.highStyle))
        else
            data.highLine := deleteLine(data.highLine)
        // Low trigger line
        if r.drawLow
            data.lowLine := updateOrCreateLine(data.lowLine, data.prevTime, r.low, lineEndTime, r.low, r.lowColor, lineWidth, getLineStyle(r.lowStyle))
        else
            data.lowLine := deleteLine(data.lowLine)
        // Open reference line
        if r.drawOpen
            data.openLine := updateOrCreateLine(data.openLine, r.ccStartTime, r.open, lineEndTime, r.open, color.gray, lineWidth, line.style_dotted)
        else
            data.openLine := deleteLine(data.openLine)
        // Magnitude high
        if r.drawMagHigh
            data.magHighLine := updateOrCreateLine(data.magHighLine, data.prevMagTime, r.magHigh, lineEndTime, r.magHigh, r.magHighColor, lineWidth, line.style_solid)
        else
            data.magHighLine := deleteLine(data.magHighLine)
        // Magnitude low
        if r.drawMagLow
            data.magLowLine := updateOrCreateLine(data.magLowLine, data.prevMagTime, r.magLow, lineEndTime, r.magLow, r.magLowColor, lineWidth, line.style_solid)
        else
            data.magLowLine := deleteLine(data.magLowLine)
        // Exhaustion high
        if r.drawExhHigh
            data.exhHighLine := updateOrCreateLine(data.exhHighLine, data.exhHighTime, r.exhHigh, lineEndTime, r.exhHigh, r.exhHighColor, lineWidth, line.style_solid)
        else
            data.exhHighLine := deleteLine(data.exhHighLine)
        // Exhaustion low
        if r.drawExhLow
            data.exhLowLine := updateOrCreateLine(data.exhLowLine, data.exhLowTime, r.exhLow, lineEndTime, r.exhLow, r.exhLowColor, lineWidth, line.style_solid)
        else
            data.exhLowLine := deleteLine(data.exhLowLine)
        // Stop high (bullish signal stop at C1 low)
        bool stopHighOverlaps = r.drawStopHigh and not na(r.stopHigh) and not na(r.high) and math.round(r.stopHigh / syminfo.mintick) == math.round(r.high / syminfo.mintick)
        if r.drawStopHigh and not na(r.stopHigh) and not stopHighOverlaps
            data.stopHighLine := updateOrCreateLine(data.stopHighLine, r.ccStartTime, r.stopHigh, lineEndTime, r.stopHigh, colorStopHigh, lineWidth, line.style_solid)
        else
            data.stopHighLine := deleteLine(data.stopHighLine)
        // Stop low (bearish signal stop at C1 high)
        bool stopLowOverlaps = r.drawStopLow and not na(r.stopLow) and not na(r.low) and math.round(r.stopLow / syminfo.mintick) == math.round(r.low / syminfo.mintick)
        if r.drawStopLow and not na(r.stopLow) and not stopLowOverlaps
            data.stopLowLine := updateOrCreateLine(data.stopLowLine, r.ccStartTime, r.stopLow, lineEndTime, r.stopLow, colorStopLow, lineWidth, line.style_solid)
        else
            data.stopLowLine := deleteLine(data.stopLowLine)
        // F2 open reference line
        bool f2OpenOverlapsLow = not na(r.f2OpenPrice) and not na(r.low) and math.round(r.f2OpenPrice / syminfo.mintick) == math.round(r.low / syminfo.mintick)
        bool f2OpenOverlapsHigh = not na(r.f2OpenPrice) and not na(r.high) and math.round(r.f2OpenPrice / syminfo.mintick) == math.round(r.high / syminfo.mintick)
        bool drawF2Open = showF2OpenLine and not na(r.f2OpenPrice) and (r.preF2d or r.preF2u or r.isF2d or r.isF2u)
        if drawF2Open
            color f2Color = r.preF2d ? colorBullishActive : r.isF2d ? COLOR_FAILED_2D : r.preF2u ? colorBearishActive : COLOR_FAILED_2U
            bool f2Overlaps = (r.preF2d or r.isF2d) ? f2OpenOverlapsLow : f2OpenOverlapsHigh
            if not f2Overlaps
                data.f2OpenLine := updateOrCreateLine(data.f2OpenLine, r.ccStartTime, r.f2OpenPrice, lineEndTime, r.f2OpenPrice, f2Color, lineWidth, line.style_dotted)
            else
                data.f2OpenLine := deleteLine(data.f2OpenLine)
        else
            data.f2OpenLine := deleteLine(data.f2OpenLine)
        // Action window boxes
        data.actionWindowBullish := deleteBox(data.actionWindowBullish)
        data.actionWindowBearish := deleteBox(data.actionWindowBearish)
        // FIX TAW-1: each direction must check only its OWN trigger. The old guards required
        // BOTH r.high and r.low to be non-na, but the opposite trigger is suppressed exactly when
        // a signal goes in force (inside_breakout_suppress_low/high nulls the other side, and
        // one-sided directional signals never draw it). That made the main magnitude/exhaustion
        // Take Action Window unable to render for in-force signals - the default config. Each box's
        // geometry only uses its own side (bullish: prevMagHigh..r.high; bearish: r.low..prevMagLow),
        // so requiring the opposite trigger was both unnecessary and the cause of the window vanishing.
        bool currentlyAboveTrigger = not na(r.high) and close > r.high
        bool currentlyBelowTrigger = not na(r.low) and close < r.low
        bool isF2uSignal = r.isF2u
        bool isF2dSignal = r.isF2d
        bool ccIs3 = str.startswith(r.ccType, "3")
        if showTakeActionWindows or tawExtendToExhaustion
            bool p3SuppressHigh = isF2uSignal
            bool p3SuppressLow = isF2dSignal
            mag_bullish_qualifies = showTakeActionWindows and r.drawHigh and not p3SuppressHigh and not ccIs3 and (not tawOnlyWhenInForce or r.signalInForceHigh) and currentlyAboveTrigger
            mag_bearish_qualifies = showTakeActionWindows and r.drawLow and not p3SuppressLow and not ccIs3 and (not tawOnlyWhenInForce or r.signalInForceLow) and currentlyBelowTrigger
            exh_bullish_qualifies = tawExtendToExhaustion and r.drawExhHigh and not p3SuppressHigh and not ccIs3 and (not tawExhOnlyWhenInForce or r.signalInForceHigh) and currentlyAboveTrigger
            exh_bearish_qualifies = tawExtendToExhaustion and r.drawExhLow and not p3SuppressLow and not ccIs3 and (not tawExhOnlyWhenInForce or r.signalInForceLow) and currentlyBelowTrigger
            if exh_bullish_qualifies and not na(data.exhHighPrice) and not data.exhHighCrossed
                data.actionWindowBullish := updateOrCreateBox(data.actionWindowBullish, data.prevTime, data.exhHighPrice, lineEndTime, r.high, tawBullishFill, tawBullishBorder)
            else if mag_bullish_qualifies and not na(data.prevMagHigh) and not data.magHighCrossed
                data.actionWindowBullish := updateOrCreateBox(data.actionWindowBullish, data.prevTime, data.prevMagHigh, lineEndTime, r.high, tawBullishFill, tawBullishBorder)
            if exh_bearish_qualifies and not na(data.exhLowPrice) and not data.exhLowCrossed
                data.actionWindowBearish := updateOrCreateBox(data.actionWindowBearish, data.prevTime, r.low, lineEndTime, data.exhLowPrice, tawBearishFill, tawBearishBorder)
            else if mag_bearish_qualifies and not na(data.prevMagLow) and not data.magLowCrossed
                data.actionWindowBearish := updateOrCreateBox(data.actionWindowBearish, data.prevTime, r.low, lineEndTime, data.prevMagLow, tawBearishFill, tawBearishBorder)
        if showP3TakeActionWindows and show_p3_toggle
            if isF2dSignal and r.drawHigh and not currentlyAboveTrigger and (not tawOnlyWhenInForce or r.signalInForceHigh)
                data.actionWindowBullish := updateOrCreateBox(data.actionWindowBullish, data.prevTime, r.high, lineEndTime, r.low, tawBullishFill, tawBullishBorder)
            if isF2uSignal and r.drawLow and not currentlyBelowTrigger and (not tawOnlyWhenInForce or r.signalInForceLow)
                data.actionWindowBearish := updateOrCreateBox(data.actionWindowBearish, data.prevTime, r.high, lineEndTime, r.low, tawBearishFill, tawBearishBorder)
        if show3ExpTakeActionWindows and show3Expansions and ccIs3
            bool ccExpandingUp = str.endswith(r.ccType, "u")
            if ccExpandingUp and r.drawHigh and not na(data.prevMagHigh) and not data.magHighCrossed and (not tawOnlyWhenInForce or r.signalInForceHigh)
                data.actionWindowBullish := updateOrCreateBox(data.actionWindowBullish, data.prevTime, data.prevMagHigh, lineEndTime, r.high, tawBullishFill, tawBullishBorder)
            if not ccExpandingUp and r.drawLow and not na(data.prevMagLow) and not data.magLowCrossed and (not tawOnlyWhenInForce or r.signalInForceLow)
                data.actionWindowBearish := updateOrCreateBox(data.actionWindowBearish, data.prevTime, r.low, lineEndTime, data.prevMagLow, tawBearishFill, tawBearishBorder)
    true


// ============================================================================
// SECTION 8: LABEL COLLECTION & CONSOLIDATION
// ============================================================================

var SimpleLabelData highLabels = SimpleLabelData.new(array.new<float>(), array.new<string>(), array.new<color>(), array.new<int>())
var SimpleLabelData lowLabels = SimpleLabelData.new(array.new<float>(), array.new<string>(), array.new<color>(), array.new<int>())
var SimpleLabelData openLabels = SimpleLabelData.new(array.new<float>(), array.new<string>(), array.new<color>(), array.new<int>())
// FIX P1-i: label pool replaces the delete-all-and-recreate-every-tick pattern. Labels are
// updated in place via setters; surplus pooled labels are trimmed after each render. acquireLabel
// takes the pool object by reference so it can advance the cursor (functions cannot mutate globals).
var LabelPoolT lblPool = LabelPoolT.new(array.new<label>(), 0)

acquireLabel(pool, int x, float y, string txt, bool useBarIndex, color bg, color txtCol, sz) =>
    xl = useBarIndex ? xloc.bar_index : xloc.bar_time
    label lbl = na
    if pool.used < array.size(pool.items)
        lbl := array.get(pool.items, pool.used)
        label.set_xloc(lbl, x, xl)
        label.set_y(lbl, y)
        label.set_text(lbl, txt)
        label.set_color(lbl, bg)
        label.set_textcolor(lbl, txtCol)
        label.set_size(lbl, sz)
        label.set_style(lbl, label.style_label_left)
    else
        lbl := label.new(x, y, txt, xloc = xl, style = label.style_label_left, color = bg, textcolor = txtCol, size = sz)
        array.push(pool.items, lbl)
    pool.used += 1
    lbl

collectTimeframeLabels(TimeframeData data, ProcessingResult r) =>
    ccIsInside = r.ccType == "1"
    ccIs3 = str.startswith(r.ccType, "3")
    // Detect if stop is at break even (same price as trigger level)
    bool stopHighAtBE = r.drawStopHigh and not na(r.stopHigh) and not na(r.high) and math.round(r.stopHigh / syminfo.mintick) == math.round(r.high / syminfo.mintick)
    bool stopLowAtBE = r.drawStopLow and not na(r.stopLow) and not na(r.low) and math.round(r.stopLow / syminfo.mintick) == math.round(r.low / syminfo.mintick)
    if r.drawHigh or r.drawLow
        patternSuffixHigh = data.c1IsHammer ? " HAM" : ""
        patternSuffixLow = data.c1IsShooter ? " SHO" : ""
        string comboStrHigh = ""
        string comboStrLow = ""
        bool c1IsInside = r.c1Num == "1"
        bool is22Pattern = r.c1Num == "2"
        bool is32Pattern = r.c1Num == "3"
        bool isDoubleInside = r.c1Num == "1" and r.c2Num == "1"
        // F2-aware display strings for label accuracy
        string c1TypeF2 = (r.c1WasF2 ? "F" : "") + r.c1Type
        string c1NumF2 = (r.c1WasF2 ? "F" : "") + r.c1Num
        string c2NumF2 = (r.c2WasF2 ? "F" : "") + r.c2Num
        if simpleLabelMode
            // Simple mode labels: ↑/↓ direction + CONT, REV, INSIDE, EXP, FAILING, OUTSIDE
            string simpleHigh = ""
            string simpleLow = ""
            bool ccIs2u = str.startswith(r.ccType, "2u")
            bool ccIs2d = str.startswith(r.ccType, "2d")
            // Step 1: Base labels from pattern type
            if ccIs3
                simpleHigh := "↑EXP"
                simpleLow := "↓EXP"
            else if is32Pattern and ccIs2u
                simpleHigh := "↑EXP"
                simpleLow := "*OUTSIDE"
            else if is32Pattern and ccIs2d
                simpleHigh := "*OUTSIDE"
                simpleLow := "↓EXP"
            else if is32Pattern
                simpleHigh := "↑EXP"
                simpleLow := "↓EXP"
            else if c1IsInside
                if ccIsInside
                    simpleHigh := "*↑INSIDE"
                    simpleLow := "*↓INSIDE"
                else
                    simpleHigh := "↑INSIDE"
                    simpleLow := "↓INSIDE"
            else if ccIsInside
                simpleHigh := data.c1IsHighContinuation ? "*↑CONTINUATION" : "*↑REVERSAL"
                simpleLow := data.c1IsLowContinuation ? "*↓CONTINUATION" : "*↓REVERSAL"
            else
                simpleHigh := data.c1IsHighContinuation ? "↑CONTINUATION" : "↑REVERSAL"
                simpleLow := data.c1IsLowContinuation ? "↓CONTINUATION" : "↓REVERSAL"
            // Step 2: Apply ◆ confidence indicator to base label (before FAILING overlay)
            if data.c1IsHammer
                simpleHigh := simpleHigh + "◆"
            if data.c1IsShooter
                simpleLow := simpleLow + "◆"
            // Step 3: F2 overlay - failing side keeps base label + FAILING, opposite side is potential outside
            if r.isF2u or r.preF2u
                // Bullish breakout failing: high label appends FAILING, low is potential outside
                simpleHigh := simpleHigh + " FAILING"
                simpleLow := "*OUTSIDE"
            else if r.isF2d or r.preF2d
                // Bearish breakout failing: low label appends FAILING, high is potential outside
                simpleLow := simpleLow + " FAILING"
                simpleHigh := "*OUTSIDE"
            comboStrHigh := simpleHigh
            comboStrLow := simpleLow
            // Diamond already applied above, clear suffix to avoid doubling
            patternSuffixHigh := ""
            patternSuffixLow := ""
        else
            // Full combo mode
            if ccIsInside
                if isDoubleInside
                    comboStrHigh := "*" + r.c3Num + c2NumF2 + c1NumF2 + "2u"
                    comboStrLow := "*" + r.c3Num + c2NumF2 + c1NumF2 + "2d"
                else if c1IsInside
                    comboStrHigh := "*" + c2NumF2 + c1NumF2 + "2u"
                    comboStrLow := "*" + c2NumF2 + c1NumF2 + "2d"
                else if is32Pattern
                    comboStrHigh := "*32u"
                    comboStrLow := "*32d"
                else
                    if r.c2Num == "3"
                        comboStrHigh := "*3" + c1TypeF2 + "2u"
                        comboStrLow := "*3" + c1TypeF2 + "2d"
                    else
                        comboStrHigh := "*" + c1TypeF2 + "2u"
                        comboStrLow := "*" + c1TypeF2 + "2d"
            else
                // Strip F2 prefix from ccType for base combo (F2 overlay handles it separately)
                string baseCCType = r.ccType
                if r.isF2u
                    baseCCType := "2u"
                else if r.isF2d
                    baseCCType := "2d"
                string defaultCombo = ""
                string c2Prefix = (r.c2Num == "3" and is22Pattern) ? "3" : ""
                if is22Pattern
                    defaultCombo := c2Prefix + c1TypeF2 + baseCCType
                else if is32Pattern
                    defaultCombo := "3" + baseCCType
                else if isDoubleInside
                    defaultCombo := r.c3Num + c2NumF2 + c1NumF2 + baseCCType
                else
                    defaultCombo := c2NumF2 + c1NumF2 + baseCCType
                // F2 labels: failing side = c1Type + f2 tag, opposite side = potential outside
                if r.isF2u
                    comboStrHigh := c1TypeF2 + "f2u"
                    comboStrLow := "*" + c1TypeF2 + "3d"
                else if r.isF2d
                    comboStrLow := c1TypeF2 + "f2d"
                    comboStrHigh := "*" + c1TypeF2 + "3u"
                else
                    comboStrHigh := defaultCombo
                    comboStrLow := defaultCombo
        if r.drawHigh
            string highText = r.tfLabel + " " + formatCombo(comboStrHigh) + patternSuffixHigh + (showHighLowInLabel ? " H" : "") + (stopHighAtBE ? " + STOP" : "")
            array.push(highLabels.prices, r.high)
            array.push(highLabels.texts, highText)
            array.push(highLabels.colors, r.highColor)
            array.push(highLabels.times, r.time)
        if r.drawLow
            string lowText = r.tfLabel + " " + formatCombo(comboStrLow) + patternSuffixLow + (showHighLowInLabel ? " L" : "") + (stopLowAtBE ? " + STOP" : "")
            array.push(lowLabels.prices, r.low)
            array.push(lowLabels.texts, lowText)
            array.push(lowLabels.colors, r.lowColor)
            array.push(lowLabels.times, r.time)
    if r.drawOpen
        array.push(openLabels.prices, r.open)
        array.push(openLabels.texts, r.tfLabel + " OPEN")
        array.push(openLabels.colors, r.openColor)
        array.push(openLabels.times, r.time)
    if r.drawMagHigh
        array.push(highLabels.prices, r.magHigh)
        array.push(highLabels.texts, r.tfLabel + (simpleLabelMode ? " TARGET" : " MAG") + (showHighLowInLabel ? " H" : ""))
        array.push(highLabels.colors, r.magHighColor)
        array.push(highLabels.times, r.time)
    if r.drawMagLow
        array.push(lowLabels.prices, r.magLow)
        array.push(lowLabels.texts, r.tfLabel + (simpleLabelMode ? " TARGET" : " MAG") + (showHighLowInLabel ? " L" : ""))
        array.push(lowLabels.colors, r.magLowColor)
        array.push(lowLabels.times, r.time)
    if r.drawExhHigh
        array.push(highLabels.prices, r.exhHigh)
        array.push(highLabels.texts, r.tfLabel + (simpleLabelMode ? " FINAL" : " EXH") + (showHighLowInLabel ? " H" : ""))
        array.push(highLabels.colors, r.exhHighColor)
        array.push(highLabels.times, r.time)
    if r.drawExhLow
        array.push(lowLabels.prices, r.exhLow)
        array.push(lowLabels.texts, r.tfLabel + (simpleLabelMode ? " FINAL" : " EXH") + (showHighLowInLabel ? " L" : ""))
        array.push(lowLabels.colors, r.exhLowColor)
        array.push(lowLabels.times, r.time)
    // Stop labels - bullish stop at C1 low (lowLabels), bearish stop at C1 high (highLabels)
    // Skip when at break even (consolidated into trigger label above)
    if r.drawStopHigh and not stopHighAtBE
        array.push(lowLabels.prices, r.stopHigh)
        array.push(lowLabels.texts, r.tfLabel + " STOP")
        array.push(lowLabels.colors, colorStopHigh)
        array.push(lowLabels.times, r.time)
    if r.drawStopLow and not stopLowAtBE
        array.push(highLabels.prices, r.stopLow)
        array.push(highLabels.texts, r.tfLabel + " STOP")
        array.push(highLabels.colors, colorStopLow)
        array.push(highLabels.times, r.time)
    // Pre-F2: CC open shown as potential trigger level
    // Skip if open price matches C1 level (FAILING label already marks that price)
    if showF2OpenLine and r.preF2d and not na(r.f2OpenPrice)
        bool preF2dOpenOverlaps = math.round(r.f2OpenPrice / syminfo.mintick) == math.round(r.low / syminfo.mintick)
        if not preF2dOpenOverlaps
            array.push(highLabels.prices, r.f2OpenPrice)
            array.push(highLabels.texts, r.tfLabel + (simpleLabelMode ? " OPEN" : " f2d"))
            array.push(highLabels.colors, colorBullishActive)
            array.push(highLabels.times, r.time)
    if showF2OpenLine and r.preF2u and not na(r.f2OpenPrice)
        bool preF2uOpenOverlaps = math.round(r.f2OpenPrice / syminfo.mintick) == math.round(r.high / syminfo.mintick)
        if not preF2uOpenOverlaps
            array.push(lowLabels.prices, r.f2OpenPrice)
            array.push(lowLabels.texts, r.tfLabel + (simpleLabelMode ? " OPEN" : " f2u"))
            array.push(lowLabels.colors, colorBearishActive)
            array.push(lowLabels.times, r.time)
    // Confirmed F2: CC open as trigger reference (dotted line needs label)
    // Gate: show if at least one side is drawing (trend-aligned F2 keeps one side,
    // counter-trend suppresses both via directional filter)
    if showF2OpenLine and r.isF2d and not r.preF2d and not na(r.f2OpenPrice) and (r.drawHigh or r.drawLow)
        bool f2dOpenOverlaps = math.round(r.f2OpenPrice / syminfo.mintick) == math.round(r.low / syminfo.mintick)
        if not f2dOpenOverlaps
            array.push(highLabels.prices, r.f2OpenPrice)
            array.push(highLabels.texts, r.tfLabel + (simpleLabelMode ? " OPEN" : " f2d"))
            array.push(highLabels.colors, COLOR_FAILED_2D)
            array.push(highLabels.times, r.time)
    if showF2OpenLine and r.isF2u and not r.preF2u and not na(r.f2OpenPrice) and (r.drawHigh or r.drawLow)
        bool f2uOpenOverlaps = math.round(r.f2OpenPrice / syminfo.mintick) == math.round(r.high / syminfo.mintick)
        if not f2uOpenOverlaps
            array.push(lowLabels.prices, r.f2OpenPrice)
            array.push(lowLabels.texts, r.tfLabel + (simpleLabelMode ? " OPEN" : " f2u"))
            array.push(lowLabels.colors, COLOR_FAILED_2U)
            array.push(lowLabels.times, r.time)

consolidateAndCreate(labelData, pool) =>
    numLabels = array.size(labelData.prices)
    if numLabels > 0
        consolidatedPrices = array.new<float>()
        consolidatedTexts = array.new<string>()
        consolidatedColors = array.new<color>()
        consolidatedTimes = array.new<int>()
        // v2.2.0: Map-based dedup replaces O(n^2) inner loop
        var priceToIndex = map.new<float, int>()
        map.clear(priceToIndex)
        for i = 0 to numLabels - 1
            currentPrice = array.get(labelData.prices, i)
            currentText = array.get(labelData.texts, i)
            currentColor = array.get(labelData.colors, i)
            currentTime = array.get(labelData.times, i)
            roundedCurrentPrice = math.round(currentPrice / syminfo.mintick) * syminfo.mintick
            if map.contains(priceToIndex, roundedCurrentPrice)
                idx = map.get(priceToIndex, roundedCurrentPrice)
                existingText = array.get(consolidatedTexts, idx)
                array.set(consolidatedTexts, idx, existingText + " + " + currentText)
            else
                map.put(priceToIndex, roundedCurrentPrice, array.size(consolidatedPrices))
                array.push(consolidatedPrices, currentPrice)
                array.push(consolidatedTexts, currentText)
                array.push(consolidatedColors, currentColor)
                array.push(consolidatedTimes, currentTime)
        labelBgColor = color.new(labelBackgroundColor, labelBackgroundTransparency)
        finalSize = array.size(consolidatedPrices)
        tfSeconds = timeframe.in_seconds() * 1000
        for i = 0 to finalSize - 1
            price = array.get(consolidatedPrices, i)
            labelText = array.get(consolidatedTexts, i)
            labelColor = array.get(consolidatedColors, i)
            labelTime = array.get(consolidatedTimes, i)
            displayText = showPriceInLabel ? labelText + " " + str.tostring(price, format.mintick) : labelText
            lblSize = getSize(labelTextSize)
            if showTimelineLabels
                acquireLabel(pool, labelTime + (labelOffset * tfSeconds), price, displayText, false, labelBgColor, labelColor, lblSize)
            if showFloatingLabels
                acquireLabel(pool, bar_index + floatingLabelOffset, price, displayText, true, labelBgColor, labelColor, lblSize)

if barstate.islast
    array.clear(highLabels.prices)
    array.clear(highLabels.texts)
    array.clear(highLabels.colors)
    array.clear(highLabels.times)
    array.clear(lowLabels.prices)
    array.clear(lowLabels.texts)
    array.clear(lowLabels.colors)
    array.clear(lowLabels.times)
    array.clear(openLabels.prices)
    array.clear(openLabels.texts)
    array.clear(openLabels.colors)
    array.clear(openLabels.times)
    lblPool.used := 0  // FIX P1-i: reset pool cursor; labels are reused in place, surplus trimmed after render

showAnyLabels = showTimelineLabels or showFloatingLabels

// ============================================================================
// SECTION 9: MAIN PROCESSING LOOP (with Lead Signal Filter)
// ============================================================================

getSignalDirection(ProcessingResult r) =>
    int direction = 0
    if not na(r)
        // Use signalInForce which checks both that signal qualifies AND price is currently beyond trigger
        if r.signalInForceHigh and r.signalInForceLow
            // Both directions in force (rare). FIX LEAD-F2-TIEBREAK-2: F2u is bearish and F2d is bullish,
            // so resolve those explicitly first. The generic close-suffix test below maps "F2u" -> +1 (bull),
            // which is inverted and would flip the Lead anchor (and all counter-trend suppression).
            if r.isF2u
                direction := -1
            else if r.isF2d
                direction := 1
            else if str.endswith(r.ccType, "u")
                direction := 1
            else
                direction := -1
        else if r.signalInForceHigh
            direction := 1
        else if r.signalInForceLow
            direction := -1
    direction

for i = 5 to 0
    cfg = array.get(tfConfigs, i)
    if cfg.enabled
        data = array.get(tfDataArr, i)
        raw = array.get(tfRawArr, i)
        isDebugTF = showDebugPanel and debugTimeframe == cfg.debugLabel
        result = computeSignalState(data, cfg.tf, raw, cfg.showOpen and not previewNextPeriod and not raw.isPreview, isDebugTF, cfg.tf)  // FIX HTF-STRADDLE-1: no open line for straddled slots (proxy open)
        array.set(tfResults, i, result)

var int anchorIndex = -1
var int anchorDirection = 0

if barstate.islast and enableDirectionalFilter
    anchorIndex := -1
    anchorDirection := 0
    for i = 5 to 0
        cfg = array.get(tfConfigs, i)
        if cfg.enabled and anchorIndex == -1
            r = array.get(tfResults, i)
            dir = getSignalDirection(r)
            if dir != 0
                anchorIndex := i
                anchorDirection := dir

if barstate.islast and enableDirectionalFilter and anchorIndex > 0 and anchorDirection != 0
    for i = 0 to anchorIndex - 1
        cfg = array.get(tfConfigs, i)
        if cfg.enabled
            data = array.get(tfDataArr, i)
            r = array.get(tfResults, i)
            if not na(r)
                // Unified directional filter: modify flags only (render phase handles drawing)
                bool suppressHigh = anchorDirection == -1
                bool isCounterF2 = (suppressHigh ? r.isF2d : r.isF2u) and not r.c1Is3
                bool isTrendF2 = (suppressHigh ? r.isF2u : r.isF2d) and not r.c1Is3
                if isCounterF2
                    // Counter-trend confirmed F2: suppress ALL flags
                    r.suppressAllFlags()
                    r.drawOpen := false
                    // FIX P1-g: also clear in-force so the data table / Lead row do not keep
                    // highlighting a suppressed counter-trend TF as in-force (lines/labels/alerts
                    // were already suppressed via the draw flags; signalInForce was not).
                    r.signalInForceHigh := false
                    r.signalInForceLow := false
                else if isTrendF2
                    // Trend-aligned confirmed F2: suppress counter-side extras only
                    if suppressHigh
                        r.drawMagHigh := false
                        r.drawExhHigh := false
                        r.drawStopHigh := false
                    else
                        r.drawMagLow := false
                        r.drawExhLow := false
                        r.drawStopLow := false
                else
                    // Regular signal or pre-F2: suppress counter-trend side
                    if suppressHigh
                        r.suppressHighFlags()
                        r.signalInForceHigh := false  // FIX P1-g: keep table/Lead consistent with hidden lines
                    else
                        r.suppressLowFlags()
                        r.signalInForceLow := false   // FIX P1-g
                    // Pre-F2: suppress open line (not a confirmed reversal yet)
                    if r.preF2d or r.preF2u
                        r.preF2u := false
                        r.preF2d := false
                    // If active side not drawing, open is orphaned
                    bool activeSideDraw = suppressHigh ? r.drawLow : r.drawHigh
                    if not activeSideDraw
                        r.drawOpen := false
                // Blanket catch: if both sides suppressed by any path, clean up open
                if not r.drawHigh and not r.drawLow
                    r.drawOpen := false
                    if not na(data.openLine)
                        line.delete(data.openLine)
                        data.openLine := na

// Render phase: draw/update/delete all lines and boxes based on final flag state
if barstate.islast
    for i = 5 to 0
        cfg = array.get(tfConfigs, i)
        if cfg.enabled
            data = array.get(tfDataArr, i)
            r = array.get(tfResults, i)
            if not na(r)
                periodDuration = timeframe.in_seconds(cfg.tf) * 1000
                raw = array.get(tfRawArr, i)
                lineEndTime = raw.ccTime + periodDuration
                renderSignalLevels(data, r, cfg.lineWidth, lineEndTime)

if barstate.islast and showAnyLabels
    for i = 5 to 0
        cfg = array.get(tfConfigs, i)
        if cfg.enabled
            data = array.get(tfDataArr, i)
            r = array.get(tfResults, i)
            if not na(r)
                // Label suppression matching line deletion logic (unified direction)
                bool skipHighLabels = false
                bool skipLowLabels = false
                if enableDirectionalFilter and i < anchorIndex
                    bool suppressHigh = anchorDirection == -1
                    bool isCounterF2 = (suppressHigh ? r.isF2d : r.isF2u) and not r.c1Is3
                    bool isTrendF2 = (suppressHigh ? r.isF2u : r.isF2d) and not r.c1Is3
                    if isCounterF2
                        skipHighLabels := true
                        skipLowLabels := true
                    else if not isTrendF2
                        // Regular: suppress counter-trend side labels
                        if suppressHigh
                            skipHighLabels := true
                        else
                            skipLowLabels := true
                if not skipHighLabels and not skipLowLabels
                    collectTimeframeLabels(data, r)
                else
                    modifiedResult = ProcessingResult.new(skipHighLabels ? na : r.high, skipLowLabels ? na : r.low, r.open, skipHighLabels ? na : r.magHigh, skipLowLabels ? na : r.magLow, skipHighLabels ? na : r.exhHigh, skipLowLabels ? na : r.exhLow, r.highColor, r.lowColor, r.openColor, r.magHighColor, r.magLowColor, r.exhHighColor, r.exhLowColor, skipHighLabels ? false : r.drawHigh, skipLowLabels ? false : r.drawLow, r.drawOpen and ((not skipHighLabels and r.drawHigh) or (not skipLowLabels and r.drawLow)), skipHighLabels ? false : r.drawMagHigh, skipLowLabels ? false : r.drawMagLow, skipHighLabels ? false : r.drawExhHigh, skipLowLabels ? false : r.drawExhLow, r.time, r.tfLabel, r.isF2u, r.isF2d, r.c1Is3, r.c3Num, r.c2Num, r.c1Num, r.c1Type, r.ccType, r.signalInForceHigh, r.signalInForceLow, skipHighLabels ? na : r.stopHigh, skipLowLabels ? na : r.stopLow, skipHighLabels ? false : r.drawStopHigh, skipLowLabels ? false : r.drawStopLow, skipHighLabels ? false : r.preF2u, skipLowLabels ? false : r.preF2d, r.f2OpenPrice, r.ccStartTime, r.c1WasF2, r.c2WasF2)
                    collectTimeframeLabels(data, modifiedResult)

// ============================================================================
// SECTION 10: LINE SUPPRESSION
// ============================================================================

collectLinesFromTF(TimeframeData data, string tf, array<LineInfo> highLines, array<LineInfo> lowLines, array<LineInfo> openLines) =>
    tfSec = timeframe.in_seconds(tf)
    // FIX P1-h: each LineInfo now carries its owner slot + a kind code so suppression can
    // null the originating field after deleting the line. Kinds: 0=high 1=low 2=magHigh
    // 3=magLow 4=exhHigh 5=exhLow 6=open 7=f2Open.
    if not na(data.highLine) and not na(data.prevHigh)
        array.push(highLines, LineInfo.new(data.prevHigh, tfSec, data.highLine, data, 0))
    if not na(data.lowLine) and not na(data.prevLow)
        array.push(lowLines, LineInfo.new(data.prevLow, tfSec, data.lowLine, data, 1))
    if not na(data.magHighLine) and not na(data.prevMagHigh)
        array.push(highLines, LineInfo.new(data.prevMagHigh, tfSec, data.magHighLine, data, 2))
    if not na(data.magLowLine) and not na(data.prevMagLow)
        array.push(lowLines, LineInfo.new(data.prevMagLow, tfSec, data.magLowLine, data, 3))
    if not na(data.exhHighLine) and not na(data.exhHighPrice)
        array.push(highLines, LineInfo.new(data.exhHighPrice, tfSec, data.exhHighLine, data, 4))
    if not na(data.exhLowLine) and not na(data.exhLowPrice)
        array.push(lowLines, LineInfo.new(data.exhLowPrice, tfSec, data.exhLowLine, data, 5))
    if not na(data.openLine) and not na(data.currOpen)
        array.push(openLines, LineInfo.new(data.currOpen, tfSec, data.openLine, data, 6))
    if not na(data.f2OpenLine) and not na(data.currOpen)
        array.push(openLines, LineInfo.new(data.currOpen, tfSec, data.f2OpenLine, data, 7))
    // Stop lines are intentionally excluded from line suppression.
    //         Stops must always remain visible for risk management regardless of
    //         overlapping trigger, magnitude, or exhaustion levels.

// FIX P1-h: null the owning TimeframeData line field after a suppressed line is deleted,
// so the render phase recreates it via line.new() next time (instead of calling line.set_*
// on a deleted id - undefined behavior), and a suppressed line can reappear once the price
// collision clears. Kinds match collectLinesFromTF.
nullSuppressedLine(TimeframeData d, int kind) =>
    if kind == 0
        d.highLine := na
    else if kind == 1
        d.lowLine := na
    else if kind == 2
        d.magHighLine := na
    else if kind == 3
        d.magLowLine := na
    else if kind == 4
        d.exhHighLine := na
    else if kind == 5
        d.exhLowLine := na
    else if kind == 6
        d.openLine := na
    else if kind == 7
        d.f2OpenLine := na

suppressLowerTFLines(array<LineInfo> lines) =>
    numLines = array.size(lines)
    if numLines > 1
        var priceToMaxTF = map.new<float, int>()
        var priceToInfo = map.new<float, LineInfo>()
        map.clear(priceToMaxTF)
        map.clear(priceToInfo)
        for i = 0 to numLines - 1
            lineI = array.get(lines, i)
            roundedPrice = math.round(lineI.price / syminfo.mintick) * syminfo.mintick
            if map.contains(priceToMaxTF, roundedPrice)
                existingTF = map.get(priceToMaxTF, roundedPrice)
                existingInfo = map.get(priceToInfo, roundedPrice)
                if lineI.tfSeconds > existingTF
                    // current is higher-TF: delete the previously-kept lower-TF line and null its field
                    line.delete(existingInfo.lineRef)
                    nullSuppressedLine(existingInfo.owner, existingInfo.kind)
                    map.put(priceToMaxTF, roundedPrice, lineI.tfSeconds)
                    map.put(priceToInfo, roundedPrice, lineI)
                else
                    // current loses: delete it and null its field
                    line.delete(lineI.lineRef)
                    nullSuppressedLine(lineI.owner, lineI.kind)
            else
                map.put(priceToMaxTF, roundedPrice, lineI.tfSeconds)
                map.put(priceToInfo, roundedPrice, lineI)
    true

if barstate.islast
    var highLines = array.new<LineInfo>()
    var lowLines = array.new<LineInfo>()
    var openLines = array.new<LineInfo>()
    array.clear(highLines)
    array.clear(lowLines)
    array.clear(openLines)
    for i = 5 to 0
        cfg = array.get(tfConfigs, i)
        if cfg.enabled
            data = array.get(tfDataArr, i)
            collectLinesFromTF(data, cfg.tf, highLines, lowLines, openLines)
    suppressLowerTFLines(highLines)
    suppressLowerTFLines(lowLines)
    suppressLowerTFLines(openLines)

if barstate.islast and showAnyLabels
    consolidateAndCreate(highLabels, lblPool)
    consolidateAndCreate(lowLabels, lblPool)
    consolidateAndCreate(openLabels, lblPool)
// FIX P1-i: trim the pool to the number of labels actually used this bar. When labels are
// disabled, lblPool.used stays 0 (consolidate did not run), so all pooled labels are deleted.
// Runs every islast regardless of showAnyLabels so toggling labels off clears them.
if barstate.islast
    while array.size(lblPool.items) > lblPool.used
        label.delete(array.pop(lblPool.items))

// ============================================================================
// SECTION 11: DATA TABLE
// ============================================================================

getBarTypeColor(barType) =>
    switch barType
        "1u" => color.new(#ffeb3b, 0)
        "1d" => color.new(#ff9800, 0)
        "2u" => color.new(#4caf50, 0)
        "2d" => color.new(#f23645, 0)
        "F2u" => color.new(COLOR_FAILED_2U, 0)
        "F2d" => color.new(COLOR_FAILED_2D, 0)
        "3u" => color.new(colorBullish3Bar, 0)
        "3d" => color.new(colorBearish3Bar, 0)
        => COLOR_BG_GRAY50

getBarTypeTextColor(barType) =>
    switch barType
        "1u" => #ffeb3b
        "1d" => #ff9800
        "2u" => #4caf50
        "2d" => #f23645
        "F2u" => COLOR_FAILED_2U
        "F2d" => COLOR_FAILED_2D
        "3u" => colorBullish3Bar
        "3d" => colorBearish3Bar
        => color.gray

var table barTypeTable = na
var int enabledTFCount = 0
isCompactMode = tableMode == "Compact (Universal)"

if barstate.isfirst
    enabledTFCount := (rEN1 ? 1 : 0) + (rEN2 ? 1 : 0) + (rEN3 ? 1 : 0) + (rEN4 ? 1 : 0) + (rEN5 ? 1 : 0) + (rEN6 ? 1 : 0)
    if showBarTypeTable and enabledTFCount > 0
        if isCompactMode
            // Compact (Universal) table: horizontal row of TFs + rows for FTFC, Filter, Domino, Preview + padding
            barTypeTable := table.new(getTablePos(tablePosition), enabledTFCount, 5, bgcolor = COLOR_BG_BLACK, frame_color = color.gray, frame_width = 1, border_color = #333333, border_width = 1)
        else
            // Full (TheStrat) table: vertical with columns
            barTypeTable := table.new(getTablePos(tablePosition), 5, enabledTFCount + 5, bgcolor = COLOR_BG_BLACK, frame_color = color.gray, frame_width = 1, border_color = #333333, border_width = 1)

populateBarTypeRow(table tbl, int row, string tf, TimeframeData data, TFRawData raw, ProcessingResult pResult, string textSize, bool reconOK) =>
    tfSeconds = timeframe.in_seconds(tf)
    isLowerTF = tfSeconds < chartTfSeconds
    tfLabelStr = getTimeframeLabel(tf)
    sz = getSize(textSize)
    if isLowerTF
        table.cell(tbl, 0, row, tfLabelStr, text_color = #555555, bgcolor = #111111, text_size = sz)
        table.cell(tbl, 1, row, "", text_color = color.gray, bgcolor = COLOR_BG_BLACK, text_size = sz)
        table.cell(tbl, 2, row, "", text_color = color.gray, bgcolor = COLOR_BG_BLACK, text_size = sz)
        table.cell(tbl, 3, row, "", text_color = color.gray, bgcolor = COLOR_BG_BLACK, text_size = sz)
        table.cell(tbl, 4, row, "", text_color = color.gray, bgcolor = COLOR_BG_BLACK, text_size = sz)
    else
        [barType_CC, _isFailed_CC] = detectBarTypeAndFailed(raw.c1H, raw.c1L, raw.ccH, raw.ccL, raw.ccO, raw.ccC)
        [barType_C1, _isFailed_C1] = detectBarTypeAndFailed(raw.c2H, raw.c2L, raw.c1H, raw.c1L, raw.c1O, raw.c1C)
        [barType_C2, _isFailed_C2] = detectBarTypeAndFailed(raw.c3H, raw.c3L, raw.c2H, raw.c2L, raw.c2O, raw.c2C)
        // Use centralized signalInForce from processing result (matches compact table behavior)
        bool isBullInForce = not na(pResult) and pResult.signalInForceHigh
        bool isBearInForce = not na(pResult) and pResult.signalInForceLow
        isInForce = colorTFWhenInForce and (isBullInForce or isBearInForce)
        bool ccIs3Exp = colorTFWhenInForce and show3Expansions and str.startswith(barType_CC, "3")
        bool ccIsInsideBar = colorTFWhenInForce and (barType_CC == "1u" or barType_CC == "1d")
        bool highlightTF = isInForce or ccIs3Exp or ccIsInsideBar
        tfBgColor = highlightTF ? getBarTypeColor(barType_CC) : #111111
        tfTextColor = highlightTF ? color.black : color.white
        table.cell(tbl, 0, row, tfLabelStr, text_color = tfTextColor, bgcolor = tfBgColor, text_size = sz)
        table.cell(tbl, 1, row, barType_C2, text_color = getBarTypeTextColor(barType_C2), bgcolor = COLOR_BG_BLACK, text_size = sz)
        table.cell(tbl, 2, row, barType_C1, text_color = getBarTypeTextColor(barType_C1), bgcolor = COLOR_BG_BLACK, text_size = sz)
        asText = data.c1IsHammer ? "HAM" : data.c1IsShooter ? "SHO" : data.c1IsInside ? "INS" : ""
        table.cell(tbl, 3, row, asText, text_color = color.white, bgcolor = COLOR_BG_BLACK, text_size = sz)
        ccRawText = barType_CC == "F2u" ? "f2u" : barType_CC == "F2d" ? "f2d" : barType_CC
        // FIX HTF-STRADDLE-1 (T3): a straddled slot whose forming candle was rebuilt from real daily
        // sub-bars (reconOK) renders normally - it is true data matching finer charts. Only a preview
        // slot WITHOUT reconstruction (classic preview, or no new-period dailies yet) shows "?".
        bool ccUnknown = raw.isPreview and not reconOK
        ccDisplayText = ccUnknown ? "?" : ccRawText
        ccBgColor = ccUnknown ? COLOR_BG_BLACK : getBarTypeColor(barType_CC)
        ccTextColor = ccUnknown ? color.yellow : barType_CC != "" ? color.black : color.white
        table.cell(tbl, 4, row, ccDisplayText, text_color = ccTextColor, bgcolor = ccBgColor, text_size = sz)

// Domino calculation - CC is inside across consecutive TFs with same direction
// Runs every bar (not just islast) so alertconditions work. Table reads on islast.
int dominoBestRun = 0
string dominoBestTFs = ""
int currentRunBull = 0
int currentRunBear = 0
int bestRunBull = 0
int bestRunBear = 0
string currentTFsBull = ""
string currentTFsBear = ""
string bestTFsBull = ""
string bestTFsBear = ""
int prevCCTimeBull = na
int prevCCTimeBear = na
for i = 5 to 0
    cfg = array.get(tfConfigs, i)
    if cfg.enabled
        result = array.get(tfResults, i)
        tfLabel = getTimeframeLabel(cfg.tf)
        ccIsInside = not na(result) and str.startswith(result.ccType, "1")
        hasBullSetup = not na(result) and result.drawHigh
        hasBearSetup = not na(result) and result.drawLow
        int thisCCTime = not na(result) ? result.ccStartTime : na
        // Bullish Domino: skip if same candle as previous TF in run
        bool bullSameCandle = not na(thisCCTime) and not na(prevCCTimeBull) and thisCCTime == prevCCTimeBull
        if ccIsInside and hasBullSetup and not bullSameCandle
            currentRunBull += 1
            currentTFsBull := currentTFsBull == "" ? tfLabel : tfLabel + "+" + currentTFsBull
            prevCCTimeBull := thisCCTime
            if currentRunBull > bestRunBull
                bestRunBull := currentRunBull
                bestTFsBull := currentTFsBull
        else if ccIsInside and hasBullSetup and bullSameCandle
            // Same candle: don't increment count, just update prevTime to maintain chain
            prevCCTimeBull := thisCCTime
        else
            currentRunBull := 0
            currentTFsBull := ""
            prevCCTimeBull := na
        // Bearish Domino: skip if same candle as previous TF in run
        bool bearSameCandle = not na(thisCCTime) and not na(prevCCTimeBear) and thisCCTime == prevCCTimeBear
        if ccIsInside and hasBearSetup and not bearSameCandle
            currentRunBear += 1
            currentTFsBear := currentTFsBear == "" ? tfLabel : tfLabel + "+" + currentTFsBear
            prevCCTimeBear := thisCCTime
            if currentRunBear > bestRunBear
                bestRunBear := currentRunBear
                bestTFsBear := currentTFsBear
        else if ccIsInside and hasBearSetup and bearSameCandle
            prevCCTimeBear := thisCCTime
        else
            currentRunBear := 0
            currentTFsBear := ""
            prevCCTimeBear := na
if bestRunBull >= bestRunBear
    dominoBestRun := bestRunBull
    dominoBestTFs := bestTFsBull
else
    dominoBestRun := bestRunBear
    dominoBestTFs := bestTFsBear

// Calculate shared values for table (FTFC)
var string tableFtfcText = ""
var color tableFtfcTextColor = color.gray
var color tableFtfcBgColor = color.gray

if barstate.islast
    // Modified FTFC calculation - exclude exhausted TFs if setting is enabled
    // ftfc_up = true means all TFs closed bullish; ftfc_down = true means all TFs closed bearish
    bool modFtfcUp = ftfc_up
    bool modFtfcDown = ftfc_down
    if exhDisablesFTFC
        modFtfcUp := true
        modFtfcDown := true
        for i = 5 to 0
            cfg = array.get(tfConfigs, i)
            if cfg.enabled
                tfSeconds = timeframe.in_seconds(cfg.tf)
                isLowerTF = tfSeconds < chartTfSeconds
                if not isLowerTF
                    raw = array.get(tfRawArr, i)
                    data = array.get(tfDataArr, i)
                    if not na(raw) and not na(data)
                        ccClosedUp = raw.ccC > raw.ccO
                        // If bullish TF hit high exhaustion, don't let it block ftfc_up (bullish setups)
                        // If bearish TF hit low exhaustion, don't let it block ftfc_down (bearish setups)
                        // If no exhaustion exists, magnitude crossed serves same function
                        bool bullMagFallback = na(data.exhHighPrice) and not na(data.prevMagHigh) and data.magHighCrossed
                        bool bearMagFallback = na(data.exhLowPrice) and not na(data.prevMagLow) and data.magLowCrossed
                        bullExhausted = ccClosedUp and (data.exhHighCrossed or bullMagFallback)
                        bearExhausted = not ccClosedUp and (data.exhLowCrossed or bearMagFallback)
                        if ccClosedUp and not bullExhausted
                            // FIX P1-e (doc): this affects the DATA TABLE FTFC cell ONLY. It does
                            // NOT gate signal drawing or alerts - those use the raw ftfc_up/ftfc_down
                            // from calculateFTFC(). A non-exhausted bullish TF keeps the table cell
                            // from reading "FTFC Down". (Wiring exhDisablesFTFC into signal gating is
                            // a deliberate, separately-validated change - see review notes.)
                            modFtfcDown := false
                        if not ccClosedUp and not bearExhausted
                            // FIX P1-e (doc): table-display only (see above); not signal gating.
                            modFtfcUp := false
    tableFtfcText := modFtfcUp ? (simpleLabelMode ? "Trend Up" : "FTFC Up") : modFtfcDown ? (simpleLabelMode ? "Trend Down" : "FTFC Down") : (simpleLabelMode ? "No Trend" : "Conflict")
    tableFtfcTextColor := modFtfcUp ? color.green : modFtfcDown ? color.red : color.gray
    tableFtfcBgColor := modFtfcUp ? color.new(color.green, 80) : modFtfcDown ? color.new(color.red, 80) : color.new(color.gray, 80)

// Shared Lead row values (used by both compact and standard table modes)
var string leadRowText = "No Lead"
var color leadRowTextColor = color.gray
var color leadRowBgColor = color.new(color.gray, 85)
if barstate.islast and enableDirectionalFilter
    string leadTfLabel = ""
    if anchorIndex >= 0 and anchorIndex <= 5
        leadCfg = array.get(tfConfigs, anchorIndex)
        leadTfLabel := getTimeframeLabel(leadCfg.tf)
    string leadDirText = anchorDirection == 1 ? "BULL" : anchorDirection == -1 ? "BEAR" : ""
    leadRowText := leadTfLabel != "" and leadDirText != "" ? "Lead: " + leadTfLabel + " " + leadDirText : "No Lead"
    leadRowTextColor := anchorDirection == 1 ? color.green : anchorDirection == -1 ? color.red : color.gray
    leadRowBgColor := anchorDirection == 1 ? color.new(color.green, 85) : anchorDirection == -1 ? color.new(color.red, 85) : color.new(color.gray, 85)

if barstate.islast and showBarTypeTable and not na(barTypeTable)
    textSize = getSize(tableTextSize)
    if isCompactMode
        // COMPACT MODE
        table.clear(barTypeTable, 0, 0, enabledTFCount - 1, 4)
        // TF labels row
        col = 0
        for i = 5 to 0
            cfg = array.get(tfConfigs, i)
            if cfg.enabled
                tfSeconds = timeframe.in_seconds(cfg.tf)
                isLowerTF = tfSeconds < chartTfSeconds
                tfLabelStr = getTimeframeLabel(cfg.tf)
                raw = array.get(tfRawArr, i)
                result = array.get(tfResults, i)
                // Determine cell color: signal-aware for quick visual scan
                color cellBg = #111111
                color cellText = #555555
                if not isLowerTF and not na(raw) and not na(result)
                    if raw.isPreview and not array.get(reconOKArr, i)  // FIX HTF-STRADDLE-1 (T3): reconstructed slots color normally
                        cellText := color.yellow
                    else if result.signalInForceHigh
                        // Bullish signal in force - check type for color
                        if result.isF2d or result.isF2u
                            cellBg := COLOR_FAILED_2D
                        else if str.startswith(result.ccType, "3")
                            cellBg := COLOR_3_HIGH
                        else
                            cellBg := #4caf50
                        cellText := color.black
                    else if result.signalInForceLow
                        // Bearish signal in force - check type for color
                        if result.isF2u or result.isF2d
                            cellBg := COLOR_FAILED_2U
                        else if str.startswith(result.ccType, "3")
                            cellBg := COLOR_3_LOW
                        else
                            cellBg := #f23645
                        cellText := color.black
                    else
                        // Not in force - check if 3-bar or inside
                        ccIs3Exp = show3Expansions and str.startswith(result.ccType, "3")
                        ccIsInside = raw.ccH <= raw.c1H and raw.ccL >= raw.c1L
                        ccAboveOpen = raw.ccC > raw.ccO
                        if ccIs3Exp
                            cellBg := ccAboveOpen ? COLOR_3_HIGH : COLOR_3_LOW
                            cellText := color.black
                        else if ccIsInside
                            cellBg := ccAboveOpen ? #ffeb3b : #ff9800
                            cellText := color.black
                        else
                            cellBg := #111111
                            cellText := color.white
                table.cell(barTypeTable, col, 0, tfLabelStr, text_color = cellText, bgcolor = cellBg, text_size = textSize)
                col += 1
        // Subsequent rows: Domino, FTFC, Filter, Preview
        compactRow = 1
        if showDominoInTable and dominoBestRun >= minDominoTFs
            table.merge_cells(barTypeTable, 0, compactRow, enabledTFCount - 1, compactRow)
            table.cell(barTypeTable, 0, compactRow, "Domino: " + dominoBestTFs, text_color = color.black, bgcolor = color.white, text_size = textSize)
            compactRow += 1
        // Row 2: FTFC
        table.merge_cells(barTypeTable, 0, compactRow, enabledTFCount - 1, compactRow)
        table.cell(barTypeTable, 0, compactRow, tableFtfcText, text_color = tableFtfcTextColor, bgcolor = tableFtfcBgColor, text_size = textSize)
        compactRow += 1
        // Row 3: Filter (if enabled)
        if enableDirectionalFilter
            table.merge_cells(barTypeTable, 0, compactRow, enabledTFCount - 1, compactRow)
            table.cell(barTypeTable, 0, compactRow, leadRowText, text_color = leadRowTextColor, bgcolor = leadRowBgColor, text_size = textSize)
            compactRow += 1
        // Row 4: Preview mode (if enabled)
        if previewNextPeriod
            table.merge_cells(barTypeTable, 0, compactRow, enabledTFCount - 1, compactRow)
            table.cell(barTypeTable, 0, compactRow, "PREVIEW MODE", text_color = color.yellow, bgcolor = color.new(color.yellow, 85), text_size = textSize)
            compactRow += 1
    else
        // STANDARD MODE
        table.clear(barTypeTable, 0, 0, 4, enabledTFCount + 4)
        table.cell(barTypeTable, 0, 0, "TF", text_color = #888888, bgcolor = #1a1a1a, text_size = textSize)
        table.cell(barTypeTable, 1, 0, "C2", text_color = #888888, bgcolor = #1a1a1a, text_size = textSize)
        table.cell(barTypeTable, 2, 0, "C1", text_color = #888888, bgcolor = #1a1a1a, text_size = textSize)
        table.cell(barTypeTable, 3, 0, "AS", text_color = #888888, bgcolor = #1a1a1a, text_size = textSize)
        table.cell(barTypeTable, 4, 0, "CC", text_color = #888888, bgcolor = #1a1a1a, text_size = textSize)
        row = 1
        for i = 5 to 0
            cfg = array.get(tfConfigs, i)
            if cfg.enabled
                data = array.get(tfDataArr, i)
                raw = array.get(tfRawArr, i)
                result = array.get(tfResults, i)
                populateBarTypeRow(barTypeTable, row, cfg.tf, data, raw, result, tableTextSize, array.get(reconOKArr, i))
                row += 1
        if showDominoInTable and dominoBestRun >= minDominoTFs
            string dominoText = "Domino: " + dominoBestTFs
            table.merge_cells(barTypeTable, 0, row, 4, row)
            table.cell(barTypeTable, 0, row, dominoText, text_color = color.black, bgcolor = color.white, text_size = textSize)
            row += 1
        table.merge_cells(barTypeTable, 0, row, 4, row)
        table.cell(barTypeTable, 0, row, tableFtfcText, text_color = tableFtfcTextColor, bgcolor = tableFtfcBgColor, text_size = textSize)
        row += 1
        if enableDirectionalFilter
            table.merge_cells(barTypeTable, 0, row, 4, row)
            table.cell(barTypeTable, 0, row, leadRowText, text_color = leadRowTextColor, bgcolor = leadRowBgColor, text_size = textSize)
            row += 1
        if previewNextPeriod
            table.merge_cells(barTypeTable, 0, row, 4, row)
            table.cell(barTypeTable, 0, row, "PREVIEW MODE", text_color = color.yellow, bgcolor = color.new(color.yellow, 85), text_size = textSize)
            row += 1

// ============================================================================
// SECTION 12: DEBUG PANEL
// ============================================================================

debugBoolStr(b) => b ? "YES" : "NO"
debugBoolBg(b) => b ? color.new(#00aa00, 20) : color.new(#aa0000, 20)
debugBoolText(b) => b ? color.white : color.new(color.gray, 35)  // FIX P2-3: false branch was also white (dead ternary); now dimmed for contrast
debugHeaderBg = color.new(#333333, 0)
debugRowBg = color.new(#1a1a1a, 0)
debugLabelColor = #aaaaaa

var table debugTable = na
if barstate.isfirst and showDebugPanel
    debugTable := table.new(position.middle_left, 2, 30, bgcolor = color.new(#0d0d0d, 0), frame_color = color.orange, frame_width = 2, border_color = color.new(#333333, 0), border_width = 1)

if barstate.islast and showDebugPanel and not na(debugTable)
    sz = size.small
    r = 0
    table.cell(debugTable, 0, r, "DEBUG: " + debugData.tfLabel, text_color = color.orange, bgcolor = debugHeaderBg, text_size = sz, text_halign = text.align_left)
    table.cell(debugTable, 1, r, "", bgcolor = debugHeaderBg, text_size = sz)
    r += 1
    table.cell(debugTable, 0, r, "C1 Bar Type", text_color = color.white, bgcolor = debugHeaderBg, text_size = sz, text_halign = text.align_center)
    table.cell(debugTable, 1, r, "", bgcolor = debugHeaderBg, text_size = sz)
    r += 1
    table.cell(debugTable, 0, r, "c1_was_2u", text_color = debugLabelColor, bgcolor = debugRowBg, text_size = sz, text_halign = text.align_left)
    table.cell(debugTable, 1, r, debugBoolStr(debugData.c1_was_2u), text_color = debugBoolText(debugData.c1_was_2u), bgcolor = debugBoolBg(debugData.c1_was_2u), text_size = sz, text_halign = text.align_center)
    r += 1
    table.cell(debugTable, 0, r, "c1_was_2d", text_color = debugLabelColor, bgcolor = debugRowBg, text_size = sz, text_halign = text.align_left)
    table.cell(debugTable, 1, r, debugBoolStr(debugData.c1_was_2d), text_color = debugBoolText(debugData.c1_was_2d), bgcolor = debugBoolBg(debugData.c1_was_2d), text_size = sz, text_halign = text.align_center)
    r += 1
    table.cell(debugTable, 0, r, "c1_is_3", text_color = debugLabelColor, bgcolor = debugRowBg, text_size = sz, text_halign = text.align_left)
    table.cell(debugTable, 1, r, debugBoolStr(debugData.c1_is_3), text_color = debugBoolText(debugData.c1_is_3), bgcolor = debugBoolBg(debugData.c1_is_3), text_size = sz, text_halign = text.align_center)
    r += 1
    table.cell(debugTable, 0, r, "Patterns", text_color = color.white, bgcolor = debugHeaderBg, text_size = sz, text_halign = text.align_center)
    table.cell(debugTable, 1, r, "", bgcolor = debugHeaderBg, text_size = sz)
    r += 1
    table.cell(debugTable, 0, r, "isHammer", text_color = debugLabelColor, bgcolor = debugRowBg, text_size = sz, text_halign = text.align_left)
    table.cell(debugTable, 1, r, debugBoolStr(debugData.isHammer), text_color = debugBoolText(debugData.isHammer), bgcolor = debugBoolBg(debugData.isHammer), text_size = sz, text_halign = text.align_center)
    r += 1
    table.cell(debugTable, 0, r, "isShooter", text_color = debugLabelColor, bgcolor = debugRowBg, text_size = sz, text_halign = text.align_left)
    table.cell(debugTable, 1, r, debugBoolStr(debugData.isShooter), text_color = debugBoolText(debugData.isShooter), bgcolor = debugBoolBg(debugData.isShooter), text_size = sz, text_halign = text.align_center)
    r += 1
    table.cell(debugTable, 0, r, "isInside", text_color = debugLabelColor, bgcolor = debugRowBg, text_size = sz, text_halign = text.align_left)
    table.cell(debugTable, 1, r, debugBoolStr(debugData.isInside), text_color = debugBoolText(debugData.isInside), bgcolor = debugBoolBg(debugData.isInside), text_size = sz, text_halign = text.align_center)
    r += 1
    table.cell(debugTable, 0, r, "CC Failed", text_color = color.white, bgcolor = debugHeaderBg, text_size = sz, text_halign = text.align_center)
    table.cell(debugTable, 1, r, "", bgcolor = debugHeaderBg, text_size = sz)
    r += 1
    table.cell(debugTable, 0, r, "cc_is_2u", text_color = debugLabelColor, bgcolor = debugRowBg, text_size = sz, text_halign = text.align_left)
    table.cell(debugTable, 1, r, debugBoolStr(debugData.cc_is_2u), text_color = debugBoolText(debugData.cc_is_2u), bgcolor = debugBoolBg(debugData.cc_is_2u), text_size = sz, text_halign = text.align_center)
    r += 1
    table.cell(debugTable, 0, r, "cc_is_2d", text_color = debugLabelColor, bgcolor = debugRowBg, text_size = sz, text_halign = text.align_left)
    table.cell(debugTable, 1, r, debugBoolStr(debugData.cc_is_2d), text_color = debugBoolText(debugData.cc_is_2d), bgcolor = debugBoolBg(debugData.cc_is_2d), text_size = sz, text_halign = text.align_center)
    r += 1
    table.cell(debugTable, 0, r, "cc_is_3", text_color = debugLabelColor, bgcolor = debugRowBg, text_size = sz, text_halign = text.align_left)
    table.cell(debugTable, 1, r, debugBoolStr(debugData.cc_is_3), text_color = debugBoolText(debugData.cc_is_3), bgcolor = debugBoolBg(debugData.cc_is_3), text_size = sz, text_halign = text.align_center)
    r += 1
    table.cell(debugTable, 0, r, "is_f2u", text_color = debugLabelColor, bgcolor = debugRowBg, text_size = sz, text_halign = text.align_left)
    table.cell(debugTable, 1, r, debugBoolStr(debugData.is_f2u), text_color = debugBoolText(debugData.is_f2u), bgcolor = debugBoolBg(debugData.is_f2u), text_size = sz, text_halign = text.align_center)
    r += 1
    table.cell(debugTable, 0, r, "is_f2d", text_color = debugLabelColor, bgcolor = debugRowBg, text_size = sz, text_halign = text.align_left)
    table.cell(debugTable, 1, r, debugBoolStr(debugData.is_f2d), text_color = debugBoolText(debugData.is_f2d), bgcolor = debugBoolBg(debugData.is_f2d), text_size = sz, text_halign = text.align_center)
    r += 1
    table.cell(debugTable, 0, r, "pre_f2u", text_color = debugLabelColor, bgcolor = debugRowBg, text_size = sz, text_halign = text.align_left)
    table.cell(debugTable, 1, r, debugBoolStr(debugData.pre_f2u), text_color = debugBoolText(debugData.pre_f2u), bgcolor = debugBoolBg(debugData.pre_f2u), text_size = sz, text_halign = text.align_center)
    r += 1
    table.cell(debugTable, 0, r, "pre_f2d", text_color = debugLabelColor, bgcolor = debugRowBg, text_size = sz, text_halign = text.align_left)
    table.cell(debugTable, 1, r, debugBoolStr(debugData.pre_f2d), text_color = debugBoolText(debugData.pre_f2d), bgcolor = debugBoolBg(debugData.pre_f2d), text_size = sz, text_halign = text.align_center)
    r += 1
    table.cell(debugTable, 0, r, "Draw Decisions", text_color = color.white, bgcolor = debugHeaderBg, text_size = sz, text_halign = text.align_center)
    table.cell(debugTable, 1, r, "", bgcolor = debugHeaderBg, text_size = sz)
    r += 1
    table.cell(debugTable, 0, r, "drawHigh", text_color = debugLabelColor, bgcolor = debugRowBg, text_size = sz, text_halign = text.align_left)
    table.cell(debugTable, 1, r, debugBoolStr(debugData.drawHigh), text_color = debugBoolText(debugData.drawHigh), bgcolor = debugBoolBg(debugData.drawHigh), text_size = sz, text_halign = text.align_center)
    r += 1
    table.cell(debugTable, 0, r, "drawLow", text_color = debugLabelColor, bgcolor = debugRowBg, text_size = sz, text_halign = text.align_left)
    table.cell(debugTable, 1, r, debugBoolStr(debugData.drawLow), text_color = debugBoolText(debugData.drawLow), bgcolor = debugBoolBg(debugData.drawLow), text_size = sz, text_halign = text.align_center)
    r += 1
    table.cell(debugTable, 0, r, "drawMagHigh", text_color = debugLabelColor, bgcolor = debugRowBg, text_size = sz, text_halign = text.align_left)
    table.cell(debugTable, 1, r, debugBoolStr(debugData.drawMagHigh), text_color = debugBoolText(debugData.drawMagHigh), bgcolor = debugBoolBg(debugData.drawMagHigh), text_size = sz, text_halign = text.align_center)
    r += 1
    table.cell(debugTable, 0, r, "drawMagLow", text_color = debugLabelColor, bgcolor = debugRowBg, text_size = sz, text_halign = text.align_left)
    table.cell(debugTable, 1, r, debugBoolStr(debugData.drawMagLow), text_color = debugBoolText(debugData.drawMagLow), bgcolor = debugBoolBg(debugData.drawMagLow), text_size = sz, text_halign = text.align_center)

// ============================================================================
// SECTION 13: ALERTS
// ============================================================================
isSetupBull(ProcessingResult r) =>
    bool result = false
    if not na(r) and r.drawHigh and not r.isF2d
        is3Bar = str.startswith(r.ccType, "3")
        if is3Bar
            result := r.ccType == "3u"
        else
            result := r.ccType == "2u"
    result

isSetupBear(ProcessingResult r) =>
    bool result = false
    if not na(r) and r.drawLow and not r.isF2u
        is3Bar = str.startswith(r.ccType, "3")
        if is3Bar
            result := r.ccType == "3d"
        else
            result := r.ccType == "2d"
    result

isF2Bull(ProcessingResult r) =>
    // FIX P0-3: gate on show_p3_toggle so F2 (Range Reclaim) alerts do not fire when
    // Failing 2s are toggled OFF. The in-force state (f2_in_force_high) already gates on
    // show_p3_toggle, so without this the alert fired while the table said "not in force".
    show_p3_toggle and not na(r) and r.drawHigh and r.isF2d

isF2Bear(ProcessingResult r) =>
    show_p3_toggle and not na(r) and r.drawLow and r.isF2u

buildAlertLabel(ProcessingResult r, bool isBullish, TimeframeData data, ProcessingResult r_full) =>
    string result = ""
    if not na(r)
        string patternSuffix = ""
        if simpleLabelMode
            patternSuffix := (isBullish and data.c1IsHammer) ? "◆" : (not isBullish and data.c1IsShooter) ? "◆" : ""
        else
            patternSuffix := (isBullish and data.c1IsHammer) ? " HAM" : (not isBullish and data.c1IsShooter) ? " SHO" : ""
        string combo = ""
        c1IsInside = r.c1Num == "1"
        is32Pattern = r.c1Is3
        ccIs3 = str.startswith(r.ccType, "3")
        ccIsInsideAlert = r.ccType == "1"
        string c1TypeF2a = (r.c1WasF2 ? "F" : "") + r.c1Type
        string c1NumF2a = (r.c1WasF2 ? "F" : "") + r.c1Num
        string c2NumF2a = (r.c2WasF2 ? "F" : "") + r.c2Num
        string dirArrow = ""
        if simpleLabelMode
            dirArrow := isBullish ? "↑" : "↓"
            // Simple mode for alerts
            if ccIs3 or is32Pattern
                combo := dirArrow + "EXP"
            else if (r.isF2d and isBullish) or (r.isF2u and not isBullish)
                combo := dirArrow + "FAILING"
            else if c1IsInside
                combo := dirArrow + "INSIDE"
            else if ccIsInsideAlert
                // CC inside: potential setup (alerts rarely fire here)
                isCont = (isBullish and data.c1IsHighContinuation) or (not isBullish and data.c1IsLowContinuation)
                combo := isCont ? dirArrow + "CONTINUATION" : dirArrow + "REVERSAL"
            else if isBullish and data.c1IsHighContinuation
                combo := dirArrow + "CONTINUATION"
            else if not isBullish and data.c1IsLowContinuation
                combo := dirArrow + "CONTINUATION"
            else
                combo := dirArrow + "REVERSAL"
        else
            // F2 labels: only show C1 prior (CC is live = lowercase f)
            if r.isF2d and isBullish
                combo := c1TypeF2a + "f2d"
            else if r.isF2u and not isBullish
                combo := c1TypeF2a + "f2u"
            else if is32Pattern
                combo := "3" + r.ccType
            else if c1IsInside
                combo := c2NumF2a + c1NumF2a + r.ccType
            else
                combo := c1TypeF2a + r.ccType
        string side = showHighLowInLabel ? (isBullish ? " H" : " L") : ""
        string emoji = isBullish ? " 🟢" : " 🔴"
        result := r.tfLabel + " " + formatCombo(combo) + patternSuffix + side + emoji
        // FIX P1-j: build detail fields into a separate string and only append the " | "
        // separator when at least one field is actually present. The old code appended the
        // pipe unconditionally whenever any toggle was on, producing a dangling "... | " (and
        // double-pipes after TF consolidation) when every field was na - contradicting the
        // tooltip's "omitted automatically" promise. Single-space joins, no trailing space.
        string details = ""
        if alertShowTrigger
            float trigPrice = isBullish ? r_full.high : r_full.low
            if not na(trigPrice)
                details := details + (details == "" ? "" : " ") + "@ " + str.tostring(trigPrice, format.mintick)
        if alertShowMagnitude
            float magPrice = isBullish ? r_full.magHigh : r_full.magLow
            if not na(magPrice)
                details := details + (details == "" ? "" : " ") + "MAG " + str.tostring(magPrice, format.mintick)
        if alertShowExhaustion
            float exhPrice = isBullish ? r_full.exhHigh : r_full.exhLow
            if not na(exhPrice)
                details := details + (details == "" ? "" : " ") + "EXH " + str.tostring(exhPrice, format.mintick)
        if alertShowStop
            float stopPrice = isBullish ? r_full.stopHigh : r_full.stopLow
            if not na(stopPrice)
                details := details + (details == "" ? "" : " ") + "STOP " + str.tostring(stopPrice, format.mintick)
        if details != ""
            result := result + " | " + details
    result

isPotentialBull(ProcessingResult r) =>
    bool result = false
    if not na(r) and r.drawHigh
        result := r.ccType == "1"  // FIX P2-5: ccType only ever emits 1/2u/2d/3u/3d/F2u/F2d; "1u"/"1d" were dead disjuncts
    result

isPotentialBear(ProcessingResult r) =>
    bool result = false
    if not na(r) and r.drawLow
        result := r.ccType == "1"  // FIX P2-5: removed unreachable "1u"/"1d" comparisons
    result
var array<bool> wasSetupBull = array.from(false, false, false, false, false, false)
var array<bool> wasSetupBear = array.from(false, false, false, false, false, false)
var array<bool> wasF2Bull = array.from(false, false, false, false, false, false)
var array<bool> wasF2Bear = array.from(false, false, false, false, false, false)
var array<bool> wasPotentialBull = array.from(false, false, false, false, false, false)
var array<bool> wasPotentialBear = array.from(false, false, false, false, false, false)
var array<int> alertLastPeriodStart = array.new<int>(6, na)  // FIX P1-c: last-seen HTF period-start per slot, to reset was-state on a new period

var bool any_newly_in_force = false
var bool any_newly_potential = false
var string alertMsg = ""

any_newly_in_force := false
any_newly_potential := false
alertMsg := ""

// FIX P1-c: reset per-slot was-state at the start of a new HTF period, so a brand-new
// signal that opens already in force (e.g. a gap-through new period that ended the prior
// period in force) still produces a false->true edge and alerts, instead of being masked
// by the prior period's true->true (no edge). Runs BEFORE the snapshot below so both the
// consolidated alert and the per-TF alertconditions observe the reset.
// Keyed off raw.realPeriodTime (the unshifted real period-open, same value P0-1 uses), NOT
// ccStartTime: ccStartTime is rewritten to the chart-bar time in preview and would force a
// reset every bar. The store sits OUTSIDE the reset guard so the slot latches on first sight
// (na seed) and the boundary test works thereafter; first-sight reset is a no-op (was-state
// is already false at init).
for pi = 0 to 5
    rawp = array.get(tfRawArr, pi)
    if not na(rawp)
        int sp = rawp.realPeriodTime
        if not na(sp)
            prevSp = array.get(alertLastPeriodStart, pi)
            if na(prevSp) or prevSp != sp
                array.set(wasSetupBull, pi, false)
                array.set(wasSetupBear, pi, false)
                array.set(wasF2Bull, pi, false)
                array.set(wasF2Bear, pi, false)
                array.set(wasPotentialBull, pi, false)
                array.set(wasPotentialBear, pi, false)
            array.set(alertLastPeriodStart, pi, sp)

// Snapshot was-state before loop updates it (alertconditions read after loop)
bool wasSetBull0 = array.get(wasSetupBull, 0), bool wasSetBear0 = array.get(wasSetupBear, 0)
bool wasSetBull1 = array.get(wasSetupBull, 1), bool wasSetBear1 = array.get(wasSetupBear, 1)
bool wasSetBull2 = array.get(wasSetupBull, 2), bool wasSetBear2 = array.get(wasSetupBear, 2)
bool wasSetBull3 = array.get(wasSetupBull, 3), bool wasSetBear3 = array.get(wasSetupBear, 3)
bool wasSetBull4 = array.get(wasSetupBull, 4), bool wasSetBear4 = array.get(wasSetupBear, 4)
bool wasSetBull5 = array.get(wasSetupBull, 5), bool wasSetBear5 = array.get(wasSetupBear, 5)
bool wasF2B0 = array.get(wasF2Bull, 0), bool wasF2Br0 = array.get(wasF2Bear, 0)
bool wasF2B1 = array.get(wasF2Bull, 1), bool wasF2Br1 = array.get(wasF2Bear, 1)
bool wasF2B2 = array.get(wasF2Bull, 2), bool wasF2Br2 = array.get(wasF2Bear, 2)
bool wasF2B3 = array.get(wasF2Bull, 3), bool wasF2Br3 = array.get(wasF2Bear, 3)
bool wasF2B4 = array.get(wasF2Bull, 4), bool wasF2Br4 = array.get(wasF2Bear, 4)
bool wasF2B5 = array.get(wasF2Bull, 5), bool wasF2Br5 = array.get(wasF2Bear, 5)

for i = 0 to 5
    cfg = array.get(tfConfigs, i)
    if cfg.enabled
        bool alertEnabled = switch i
            0 => alertTF1
            1 => alertTF2
            2 => alertTF3
            3 => alertTF4
            4 => alertTF5
            5 => alertTF6
            => true
        r = array.get(tfResults, i)
        d = array.get(tfDataArr, i)
        // FIX HTF-STRADDLE-1: never emit alerts from a preview/straddled slot - its CC is synthetic
        // or estimate-grade. Was-state still updates (U2 pattern), and the P1-c period reset re-arms
        // cleanly when the slot exits preview, so the first real bar of the new period alerts normally.
        rawAl = array.get(tfRawArr, i)
        bool slotPreview = straddleWindowOK and not na(rawAl) and rawAl.isPreview
        bool suppressBull = enableDirectionalFilter and anchorDirection == -1 and i < anchorIndex
        bool suppressBear = enableDirectionalFilter and anchorDirection == 1 and i < anchorIndex

        // Check setups (non-F2)
        isNowSetupBull = suppressBull ? false : isSetupBull(r)
        isNowSetupBear = suppressBear ? false : isSetupBear(r)
        wasSetBull = array.get(wasSetupBull, i)
        wasSetBear = array.get(wasSetupBear, i)

        // Check F2s separately
        isNowF2Bull = suppressBull ? false : isF2Bull(r)
        isNowF2Bear = suppressBear ? false : isF2Bear(r)
        wasF2B = array.get(wasF2Bull, i)
        wasF2Br = array.get(wasF2Bear, i)

        // FIX U2: only the consolidated alert() MESSAGE is gated by this TF's alert checkbox.
        // The was-state arrays below are updated UNCONDITIONALLY (was: an early `continue`
        // skipped them), so the per-TF alertconditions - which read the pre-loop snapshots -
        // detect true edges instead of firing every bar when a TF is enabled but its alert
        // checkbox is off.
        if alertEnabled and not slotPreview
            // Alert on new setup (independent of F2 state)
            if isNowSetupBull and not wasSetBull
                any_newly_in_force := true
                newLabel = buildAlertLabel(r, true, d, r)
                alertMsg := alertMsg == "" ? newLabel : alertMsg + " | " + newLabel
            if isNowSetupBear and not wasSetBear
                any_newly_in_force := true
                newLabel = buildAlertLabel(r, false, d, r)
                alertMsg := alertMsg == "" ? newLabel : alertMsg + " | " + newLabel
            // Alert on new F2 (independent of setup state)
            if isNowF2Bull and not wasF2B
                any_newly_in_force := true
                newLabel = buildAlertLabel(r, true, d, r)
                alertMsg := alertMsg == "" ? newLabel : alertMsg + " | " + newLabel
            if isNowF2Bear and not wasF2Br
                any_newly_in_force := true
                newLabel = buildAlertLabel(r, false, d, r)
                alertMsg := alertMsg == "" ? newLabel : alertMsg + " | " + newLabel

        // Update state (unconditional - keeps per-TF alertcondition snapshots accurate)
        array.set(wasSetupBull, i, isNowSetupBull)
        array.set(wasSetupBear, i, isNowSetupBear)
        array.set(wasF2Bull, i, isNowF2Bull)
        array.set(wasF2Bear, i, isNowF2Bear)

        // Potential signals
        isPotBull = isPotentialBull(r)
        isPotBear = isPotentialBear(r)
        wasPotBull = array.get(wasPotentialBull, i)
        wasPotBear = array.get(wasPotentialBear, i)
        if alertEnabled and not slotPreview and ((isPotBull and not wasPotBull) or (isPotBear and not wasPotBear))
            any_newly_potential := true
        array.set(wasPotentialBull, i, isPotBull)
        array.set(wasPotentialBear, i, isPotBear)

if any_newly_in_force and str.length(alertMsg) > 0
    if alertShowFTFC
        string ftfcState = ftfc_up ? "FTFC Up" : ftfc_down ? "FTFC Down" : "Conflict"
        alertMsg := alertMsg + " | " + ftfcState
    alert(alertMsg, alert.freq_once_per_bar)

var string lastDominoCombo = ""
var bool dominoAlertTriggered = false
dominoAlertTriggered := false

// Domino alert (uses shared dominoBestRun/dominoBestTFs computed before table section)
if dominoBestRun >= minDominoTFs and dominoBestTFs != lastDominoCombo
    dominoAlertTriggered := true
    if enableDominoAlerts
        alert("Domino: " + dominoBestTFs, alert.freq_once_per_bar)
    lastDominoCombo := dominoBestTFs
else if dominoBestRun < minDominoTFs
    lastDominoCombo := ""

r1 = array.get(tfResults, 0)
r2 = array.get(tfResults, 1)
r3 = array.get(tfResults, 2)
r4 = array.get(tfResults, 3)
r5 = array.get(tfResults, 4)
r6 = array.get(tfResults, 5)

// FIX HTF-STRADDLE-1: per-TF alertconditions also skip preview/straddled slots
bool okA1 = not (straddleWindowOK and array.get(tfRawArr, 0).isPreview)
bool okA2 = not (straddleWindowOK and array.get(tfRawArr, 1).isPreview)
bool okA3 = not (straddleWindowOK and array.get(tfRawArr, 2).isPreview)
bool okA4 = not (straddleWindowOK and array.get(tfRawArr, 3).isPreview)
bool okA5 = not (straddleWindowOK and array.get(tfRawArr, 4).isPreview)
bool okA6 = not (straddleWindowOK and array.get(tfRawArr, 5).isPreview)

// Per-TF alertconditions use pre-loop snapshots to detect transitions correctly
tf1_newly_bull = rEN1 and okA1 and ((isSetupBull(r1) and not wasSetBull0) or (isF2Bull(r1) and not wasF2B0))
tf1_newly_bear = rEN1 and okA1 and ((isSetupBear(r1) and not wasSetBear0) or (isF2Bear(r1) and not wasF2Br0))
tf2_newly_bull = rEN2 and okA2 and ((isSetupBull(r2) and not wasSetBull1) or (isF2Bull(r2) and not wasF2B1))
tf2_newly_bear = rEN2 and okA2 and ((isSetupBear(r2) and not wasSetBear1) or (isF2Bear(r2) and not wasF2Br1))
tf3_newly_bull = rEN3 and okA3 and ((isSetupBull(r3) and not wasSetBull2) or (isF2Bull(r3) and not wasF2B2))
tf3_newly_bear = rEN3 and okA3 and ((isSetupBear(r3) and not wasSetBear2) or (isF2Bear(r3) and not wasF2Br2))
tf4_newly_bull = rEN4 and okA4 and ((isSetupBull(r4) and not wasSetBull3) or (isF2Bull(r4) and not wasF2B3))
tf4_newly_bear = rEN4 and okA4 and ((isSetupBear(r4) and not wasSetBear3) or (isF2Bear(r4) and not wasF2Br3))
tf5_newly_bull = rEN5 and okA5 and ((isSetupBull(r5) and not wasSetBull4) or (isF2Bull(r5) and not wasF2B4))
tf5_newly_bear = rEN5 and okA5 and ((isSetupBear(r5) and not wasSetBear4) or (isF2Bear(r5) and not wasF2Br4))
tf6_newly_bull = rEN6 and okA6 and ((isSetupBull(r6) and not wasSetBull5) or (isF2Bull(r6) and not wasF2B5))
tf6_newly_bear = rEN6 and okA6 and ((isSetupBear(r6) and not wasSetBear5) or (isF2Bear(r6) and not wasF2Br5))

var bool was_ftfc_up = false
var bool was_ftfc_down = false
ftfc_shifted_up = ftfc_up and not was_ftfc_up
ftfc_shifted_down = ftfc_down and not was_ftfc_down
ftfc_shifted_conflict = not ftfc_up and not ftfc_down and (was_ftfc_up or was_ftfc_down)
ftfc_shifted = ftfc_shifted_up or ftfc_shifted_down or ftfc_shifted_conflict
was_ftfc_up := ftfc_up
was_ftfc_down := ftfc_down

alertcondition(any_newly_in_force, "Signal In-Force (Any)", "Signal In-Force")
alertcondition(tf1_newly_bull or tf1_newly_bear, "Signal In-Force (TF1)", "TF1 In-Force")
alertcondition(tf2_newly_bull or tf2_newly_bear, "Signal In-Force (TF2)", "TF2 In-Force")
alertcondition(tf3_newly_bull or tf3_newly_bear, "Signal In-Force (TF3)", "TF3 In-Force")
alertcondition(tf4_newly_bull or tf4_newly_bear, "Signal In-Force (TF4)", "TF4 In-Force")
alertcondition(tf5_newly_bull or tf5_newly_bear, "Signal In-Force (TF5)", "TF5 In-Force")
alertcondition(tf6_newly_bull or tf6_newly_bear, "Signal In-Force (TF6)", "TF6 In-Force")
alertcondition(tf1_newly_bull or tf2_newly_bull or tf3_newly_bull or tf4_newly_bull or tf5_newly_bull or tf6_newly_bull, "Signal In-Force (Bullish)", "Bullish In-Force")
alertcondition(tf1_newly_bear or tf2_newly_bear or tf3_newly_bear or tf4_newly_bear or tf5_newly_bear or tf6_newly_bear, "Signal In-Force (Bearish)", "Bearish In-Force")
alertcondition(any_newly_potential, "New Potential (Any)", "New Potential Signal")
alertcondition(dominoAlertTriggered, "Domino Setup", "Domino Setup")
alertcondition(ftfc_shifted, "FTFC Shifted", "FTFC Shifted")
alertcondition(ftfc_shifted_up, "FTFC Up", "FTFC Up")
alertcondition(ftfc_shifted_down, "FTFC Down", "FTFC Down")
alertcondition(ftfc_shifted_conflict, "FTFC Conflict", "FTFC Conflict")

Overview

TheStrat Suite classifies bars, marks inside bars, hammers, shooters, Failed 2s and outside bars, and plots magnitude and exhaustion targets plus stop levels. Six timeframe slots, presets, Full Timeframe Continuity filters, Domino detection, a live data table and consolidated alerts control what appears. It warns against Heiken-Ashi charts and reads higher-timeframe data with lookahead enabled.

Details

Published by

345-strategies on GitHub

Type

Indicator, Pine Script v6

Draws

On the price chart

Category

Candlestick patterns, Support & resistance, Screeners & dashboards

Markets

Any symbol

Timeframes

Any timeframe

Risk Notes

  • Reads higher-timeframe data with lookahead on, so the history can show signals that were not there in real time

Topics

indicatorpine-scriptthestrattradingtradingviewprice-actionstratpine-script-librarythestrat-suite

Settings

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

SettingTypeDefaultVariable
PresetstringTheStrat ClassictfPreset
Label StylestringTheStratlabelStyleMode
Timeframe 1boolfalsetf1Enabled
tf1timeframe15tf1
Widthint1tf1LineWidth
Openboolfalsetf1ShowOpen
Timeframe 2boolfalsetf2Enabled
tf2timeframe30tf2
Widthint1tf2LineWidth
Openboolfalsetf2ShowOpen
Timeframe 3boolfalsetf3Enabled
tf3timeframe60tf3
Show all 40 settings
SettingTypeDefaultVariable
Widthint1tf3LineWidth
Openboolfalsetf3ShowOpen
Timeframe 4boolfalsetf4Enabled
tf4timeframeDtf4
Widthint2tf4LineWidth
Openboolfalsetf4ShowOpen
Timeframe 5boolfalsetf5Enabled
tf5timeframeWtf5
Widthint3tf5LineWidth
Openboolfalsetf5ShowOpen
Timeframe 6boolfalsetf6Enabled
tf6timeframeMtf6
Widthint4tf6LineWidth
Openboolfalsetf6ShowOpen
Inside Reversals |booltrueshowInsideReversals
HAM/SHOboolfalseinsideRevOnlyMomo
FTFCboolfalseinsideRevRequireFTFC
2-2 Reversals |booltrueshowAllReversals
HAM/SHOboolfalsereversalsRequireActionable
F2boolfalsereversalsRequireC1F2
FTFCboolfalsereversalsRequireFTFC
Inside Continuations |booltrueshowInsideContinuations
HAM/SHOboolfalseinsideContOnlyMomo
FTFCboolfalseinsideContRequireFTFC
2-2 Continuations |boolfalseshowContinuations
HAM/SHOboolfalsetwoTwoContOnlyMomo
FTFCboolfalsecontinuationsRequireFTFC
Failing 2s (Range Reclaims) |boolfalseshow_p3_toggle
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

46 stars

This overlay indicator draws a set of higher-timeframe candles on the price chart. The user sets the timeframe or lets it select automatically, plus candle count, size, offset, margin, and whether the candles are standard or Heikin Ashi. A table can show the a

Indicator v6 Support & resistanceCandlestick patterns Repaints
Updated 7 weeks ago · None stated GitHub repository of HTF Sweep Model with CISD Table

Fractal Reclaim Strength

by selimgrsoy0-commits
12 stars

The indicator tracks four-bar fractal sweep and reclaim setups on chart timeframes of 15 minutes and above. It calculates Reclaim Strength based on where C2 closes relative to C1's range and plots level lines for the swept C1 extreme and C2 extreme. An on-scre

Indicator v6 SMC / ICT / price actionSupport & resistance
Updated 7 weeks ago · None stated GitHub repository of Fractal Reclaim Strength

Pro Pattern Al-Sat

by MeridianAlgo
9 stars

This overlay indicator marks bullish and bearish chart patterns on the price chart and can hide either side. Optional filters require agreement from fast and slow EMAs, Supertrend, and ADX, plus volume, RSI, nearby pivot support or resistance, and a user-defin

Indicator v6 Candlestick patternsTrend
Updated 4 months ago · MIT GitHub repository of Pro Pattern Al-Sat

This indicator segments recent price bars into defined groups and visual boxes to serve as a visual chart ruler. It includes automated trend detection with configurable angle thresholds to plot uptrend, downtrend, and range segments directly on the price chart

Indicator v6 TrendSupport & resistance
6 stars

Forex Majors 4H Rejection Scanner tracks seven customizable currency pairs on a four-hour timeframe and displays their bias in a table overlay on the main price chart. It checks the last two closed four-hour candles for high or low price rejections, labeling a

Indicator v5 Screeners & dashboardsSupport & resistance Repaints
Updated 7 months ago · None stated GitHub repository of Forex Majors 4H Rejection Scanner
3 stars

Price-Action Candles classifies swing structure from pivot highs and lows and colors chart candles bullish, bearish, or neutral. It marks higher highs, lower highs, higher lows, and lower lows, with a label mode that either offsets labels by the pivot length o

Indicator v5 TrendCandlestick patterns Repaints
Updated 5 months ago · None stated GitHub repository of Price-Action Candles

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