Indicator · Pine Script v6 · On the chart

TheStrat Suite

by 345-strategies

TheStrat Suite classifies real OHLC bars with Rob Smith's TheStrat method and draws labels, targets, and stop levels on the price chart.

View on GitHub

Repaints

GitHub stars5on the repository
Pine versionv6current
Lines of code2,826
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

2,826 lines of Pine Script v6 from pine/TheStratSuite_v180_June16_2026.pine, published under MPL-2.0. Keep the author's credit and licence lines when you reuse it.

TheStratSuite_v180_June16_2026.pine
//@version=6
// TheStrat Suite - v2.2.0-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.0-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"

// -------------------- TIMEFRAMES --------------------
grpTFs = "Timeframes"
tf1Enabled = input.bool(false, "Timeframe 1", inline = "tf1a", group = grpTFs)
tf1 = input.timeframe("15", "", inline = "tf1a", group = grpTFs)
tf1LineWidth = input.int(1, "Width", minval = 1, maxval = 5, inline = "tf1b", group = grpTFs)
tf1ShowOpen = input.bool(false, "Open", inline = "tf1b", group = grpTFs)

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

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

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

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

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

// 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")
// -------------------- 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")
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)")
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: no new bar for 3x chart period or 4 hours, whichever is larger
isHolidayClosed = (timenow - time) > 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)

// -------------------- 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.)")

// ============================================================================
// 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

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

type LineInfo
    float price
    int tfSeconds
    line lineRef

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

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
// ============================================================================

// Line/box cleanup methods - consolidate repeated delete-and-na patterns
method clearHighExtras(TimeframeData this) =>
    if not na(this.magHighLine)
        line.delete(this.magHighLine)
        this.magHighLine := na
    if not na(this.exhHighLine)
        line.delete(this.exhHighLine)
        this.exhHighLine := na
    if not na(this.stopHighLine)
        line.delete(this.stopHighLine)
        this.stopHighLine := na
    if not na(this.actionWindowBullish)
        box.delete(this.actionWindowBullish)
        this.actionWindowBullish := na

method clearLowExtras(TimeframeData this) =>
    if not na(this.magLowLine)
        line.delete(this.magLowLine)
        this.magLowLine := na
    if not na(this.exhLowLine)
        line.delete(this.exhLowLine)
        this.exhLowLine := na
    if not na(this.stopLowLine)
        line.delete(this.stopLowLine)
        this.stopLowLine := na
    if not na(this.actionWindowBearish)
        box.delete(this.actionWindowBearish)
        this.actionWindowBearish := na

method clearHighAll(TimeframeData this) =>
    if not na(this.highLine)
        line.delete(this.highLine)
        this.highLine := na
    this.clearHighExtras()

method clearLowAll(TimeframeData this) =>
    if not na(this.lowLine)
        line.delete(this.lowLine)
        this.lowLine := na
    this.clearLowExtras()

method clearCommon(TimeframeData this) =>
    if not na(this.f2OpenLine)
        line.delete(this.f2OpenLine)
        this.f2OpenLine := na
    if not na(this.openLine)
        line.delete(this.openLine)
        this.openLine := na

// 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) =>
    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

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
        float maxHighSoFar = high[0]
        float minLowSoFar = low[0]
        for i = 1 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 inside_rev_ftfc_blocks
        else if is_momo_cont and showContinuations
            result := not inside_rev_ftfc_blocks
            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)

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

shouldApplyPreview(string tf, bool previewEnabled) =>
    if not previewEnabled
        false
    else
        // Compare last bar's period vs current calendar period
        // Preview should activate when we're in a new calendar period but market hasn't opened
        // This handles: Friday EOD -> Weekend (same period), but also Jan 31 -> Feb 1 weekend (new period)

        // Current calendar time (when user is viewing)
        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

        // Last bar's time
        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"

        // Check if we're in a different period than the last bar
        if isWeekly
            // For weekly: different week OR it's Friday/weekend of same week
            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) =>
    doPreview = shouldApplyPreview(tf, previewEnabled)
    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
        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)
    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)

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))
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))
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))
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))
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))
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))

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
        if 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
    newPeriod = ta.change(time(tf)) != 0

    isInPreview = raw.isPreview
    if newPeriod or (isInPreview and barstate.islast)
        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.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
        if barstate.islast
            [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
            signal_in_force_high = (isInside and c2_closed_down 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 c2_closed_up 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
            threeExp_show_exh_high = cc_is_3 and show3Expansions and show3ExpExhaustion and (not threeExpRequireFTFC or ftfc_up) and cc_above_open_for_mag
            threeExp_show_exh_low = cc_is_3 and show3Expansions 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)
        bool currentlyAboveTrigger = not na(r.high) and not na(r.low) and close > r.high
        bool currentlyBelowTrigger = not na(r.high) and 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>())
var array<label> createdLabels = array.new<label>()

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) =>
    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
                lbl = label.new(labelTime + (labelOffset * tfSeconds), price, displayText, xloc = xloc.bar_time, style = label.style_label_left, color = labelBgColor, textcolor = labelColor, size = lblSize)
                array.push(createdLabels, lbl)
            if showFloatingLabels
                floatLbl = label.new(bar_index + floatingLabelOffset, price, displayText, xloc = xloc.bar_index, style = label.style_label_left, color = labelBgColor, textcolor = labelColor, size = lblSize)
                array.push(createdLabels, floatLbl)

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)
    for lbl in createdLabels
        label.delete(lbl)
    array.clear(createdLabels)

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) - use CC direction
            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, isDebugTF, cfg.tf)
        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
                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()
                    else
                        r.suppressLowFlags()
                    // 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)
    if not na(data.highLine) and not na(data.prevHigh)
        array.push(highLines, LineInfo.new(data.prevHigh, tfSec, data.highLine))
    if not na(data.lowLine) and not na(data.prevLow)
        array.push(lowLines, LineInfo.new(data.prevLow, tfSec, data.lowLine))
    if not na(data.magHighLine) and not na(data.prevMagHigh)
        array.push(highLines, LineInfo.new(data.prevMagHigh, tfSec, data.magHighLine))
    if not na(data.magLowLine) and not na(data.prevMagLow)
        array.push(lowLines, LineInfo.new(data.prevMagLow, tfSec, data.magLowLine))
    if not na(data.exhHighLine) and not na(data.exhHighPrice)
        array.push(highLines, LineInfo.new(data.exhHighPrice, tfSec, data.exhHighLine))
    if not na(data.exhLowLine) and not na(data.exhLowPrice)
        array.push(lowLines, LineInfo.new(data.exhLowPrice, tfSec, data.exhLowLine))
    if not na(data.openLine) and not na(data.currOpen)
        array.push(openLines, LineInfo.new(data.currOpen, tfSec, data.openLine))
    if not na(data.f2OpenLine) and not na(data.currOpen)
        array.push(openLines, LineInfo.new(data.currOpen, tfSec, data.f2OpenLine))
    // Stop lines are intentionally excluded from line suppression.
    //         Stops must always remain visible for risk management regardless of
    //         overlapping trigger, magnitude, or exhaustion levels.

suppressLowerTFLines(array<LineInfo> lines) =>
    numLines = array.size(lines)
    if numLines > 1
        var priceToMaxTF = map.new<float, int>()
        var priceToLine = map.new<float, line>()
        map.clear(priceToMaxTF)
        map.clear(priceToLine)
        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)
                existingLine = map.get(priceToLine, roundedPrice)
                if lineI.tfSeconds > existingTF
                    line.delete(existingLine)
                    map.put(priceToMaxTF, roundedPrice, lineI.tfSeconds)
                    map.put(priceToLine, roundedPrice, lineI.lineRef)
                else
                    line.delete(lineI.lineRef)
            else
                map.put(priceToMaxTF, roundedPrice, lineI.tfSeconds)
                map.put(priceToLine, roundedPrice, lineI.lineRef)
    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)
    consolidateAndCreate(lowLabels)
    consolidateAndCreate(openLabels)

// ============================================================================
// 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) =>
    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
        ccDisplayText = raw.isPreview ? "?" : ccRawText
        ccBgColor = raw.isPreview ? COLOR_BG_BLACK : getBarTypeColor(barType_CC)
        ccTextColor = raw.isPreview ? 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
                            // Bullish TF that's NOT exhausted - blocks bearish setups
                            modFtfcDown := false
                        if not ccClosedUp and not bearExhausted
                            // Bearish TF that's NOT exhausted - blocks bullish setups
                            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
                        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)
                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.white
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) =>
    not na(r) and r.drawHigh and r.isF2d

isF2Bear(ProcessingResult r) =>
    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
        // Enriched alert fields - add separator pipe if any detail toggles are on
        bool anyAlertDetails = alertShowTrigger or alertShowMagnitude or alertShowExhaustion or alertShowStop
        if anyAlertDetails
            result := result + " | "
        if alertShowTrigger
            float trigPrice = isBullish ? r_full.high : r_full.low
            if not na(trigPrice)
                result := result + "@ " + str.tostring(trigPrice, format.mintick) + " "
        if alertShowMagnitude
            float magPrice = isBullish ? r_full.magHigh : r_full.magLow
            if not na(magPrice)
                result := result + "MAG " + str.tostring(magPrice, format.mintick) + " "
        if alertShowExhaustion
            float exhPrice = isBullish ? r_full.exhHigh : r_full.exhLow
            if not na(exhPrice)
                result := result + "EXH " + str.tostring(exhPrice, format.mintick) + " "
        if alertShowStop
            float stopPrice = isBullish ? r_full.stopHigh : r_full.stopLow
            if not na(stopPrice)
                result := result + "STOP " + str.tostring(stopPrice, format.mintick) + " "
    result

isPotentialBull(ProcessingResult r) =>
    bool result = false
    if not na(r) and r.drawHigh
        result := r.ccType == "1" or r.ccType == "1u" or r.ccType == "1d"
    result

isPotentialBear(ProcessingResult r) =>
    bool result = false
    if not na(r) and r.drawLow
        result := r.ccType == "1" or r.ccType == "1u" or r.ccType == "1d"
    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 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 := ""

// 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
        if not alertEnabled
            continue
        r = array.get(tfResults, i)
        d = array.get(tfDataArr, i)
        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)

        // 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
        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 (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)

// Per-TF alertconditions use pre-loop snapshots to detect transitions correctly
tf1_newly_bull = rEN1 and ((isSetupBull(r1) and not wasSetBull0) or (isF2Bull(r1) and not wasF2B0))
tf1_newly_bear = rEN1 and ((isSetupBear(r1) and not wasSetBear0) or (isF2Bear(r1) and not wasF2Br0))
tf2_newly_bull = rEN2 and ((isSetupBull(r2) and not wasSetBull1) or (isF2Bull(r2) and not wasF2B1))
tf2_newly_bear = rEN2 and ((isSetupBear(r2) and not wasSetBear1) or (isF2Bear(r2) and not wasF2Br1))
tf3_newly_bull = rEN3 and ((isSetupBull(r3) and not wasSetBull2) or (isF2Bull(r3) and not wasF2B2))
tf3_newly_bear = rEN3 and ((isSetupBear(r3) and not wasSetBear2) or (isF2Bear(r3) and not wasF2Br2))
tf4_newly_bull = rEN4 and ((isSetupBull(r4) and not wasSetBull3) or (isF2Bull(r4) and not wasF2B3))
tf4_newly_bear = rEN4 and ((isSetupBear(r4) and not wasSetBear3) or (isF2Bear(r4) and not wasF2Br3))
tf5_newly_bull = rEN5 and ((isSetupBull(r5) and not wasSetBull4) or (isF2Bull(r5) and not wasF2B4))
tf5_newly_bear = rEN5 and ((isSetupBear(r5) and not wasSetBear4) or (isF2Bear(r5) and not wasF2Br4))
tf6_newly_bull = rEN6 and ((isSetupBull(r6) and not wasSetBull5) or (isF2Bull(r6) and not wasF2B5))
tf6_newly_bear = rEN6 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 detects inside bars, hammers, shooters, outside bars, and Failed 2 range reclaims, then shows magnitude and exhaustion targets, Full Timeframe Continuity, and Domino sequences of consecutive inside bars. Six timeframe slots, presets, and filters change which signals, lines, and table rows appear, and consolidated alerts report them. Higher-timeframe reads use lookahead, and the tool is not meant for Heiken-Ashi charts.

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

  • Signals can change on the live bar or after it closes (repainting)
  • 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
575 stars

The indicator plots ICT displacement candles on the price chart and can switch to retracement scanning. Displacement requires a move larger than a set share of the 14-bar average high-low range, lasting at least a chosen number of bars, with optional close, pr

Indicator v6 SMC / ICT / price actionCandlestick patterns
Updated 1 year ago · MPL-2.0 GitHub repository of ICT Displacement Candles Indicator

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