Indicator · Pine Script v6 · On the chart

Market Structure Dashboard | Flux Charts

by eddiebelaval· code credited to fluxchart

This indicator overlays a market structure dashboard on the price chart.

View on GitHub

GitHub stars7on the repository
Pine versionv6current
Lines of code1,468
Settings40inputs
LicenceNone statedfile: MPL-2.0
Last update3 months ago24 Jul 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

1,468 lines of Pine Script v6 from pinescript/popular/market-structure-dashboard-flux-charts.pine, published under MPL-2.0 (file header). Keep the author's credit and licence lines when you reuse it.

market-structure-dashboard-flux-charts.pine
// This Pine Script™ code is subject to the terms of the Mozilla Public License 2.0 at https://mozilla.org/MPL/2.0/
// © fluxchart

//@version=6
indicator("Market Structure Dashboard | Flux Charts", overlay = true, max_boxes_count = 500, max_labels_count = 500, max_bars_back = 1000)

//#region CONSTANTS

// Base colors
GRAY                    = #787b86
ORANGE                  = #f97316
YELLOW                  = #fbbf24

// Trend colors (defaults)
BULL_COLOR              = #089981
BEAR_COLOR              = #f23645
NEUT_COLOR              = #b8b8b8

// Session colors (defaults)
SESSION_ASIAN           = #3b82f6
SESSION_LONDON          = #22c55e
SESSION_NY              = #f97316
SESSION_LUNCH           = #6b7280
SESSION_PM              = #a855f7

// Dashboard theme colors
BG_DARK_DARK            = color.rgb(30, 30, 30)
BG_DARK_LIGHT           = color.rgb(245, 245, 245)
BG_HEADER_DARK          = color.rgb(30, 30, 40)
BG_HEADER_LIGHT         = color.rgb(230, 230, 240)
BG_ROW_DARK             = #1a2332
BG_ROW_LIGHT            = #e8edf5
TEXT_DARK               = color.white
TEXT_LIGHT              = color.rgb(30, 30, 30)


// Group Headers
gTF                     = "Timeframes"
gMsd                    = "Market Structure Dashboard"
gCtfd                   = "Current Timeframe Dashboard"
gMcd                    = "Market Context Dashboard"
gHtf                    = "HTF Levels Dashboard"
gBias                   = "Trend Bias Settings"
gVO                     = "Visual Overlays"
gSettings               = "Settings"
//#endregion CONSTANTS

//#region INPUTS

// Line style enum
enum lineStyle
    solid  = "────"
    dashed = "- - -"
    dotted = "····"

// Timeframes
bool    tf1Enabled = input.bool(true,       "",               group = gTF, inline = "tf1", display = display.none)
string  tf1        = input.timeframe("1",   "",               group = gTF, inline = "tf1", active = tf1Enabled, display = display.none)
int     tf1Weight  = input.int(1,           "Trend Weight",   group = gTF, inline = "tf1", minval = 0, maxval = 10, active = tf1Enabled, display = display.none)

bool    tf2Enabled = input.bool(true,       "",               group = gTF, inline = "tf2", display = display.none)
string  tf2        = input.timeframe("5",   "",               group = gTF, inline = "tf2", active = tf2Enabled, display = display.none)
int     tf2Weight  = input.int(1,           "Trend Weight",   group = gTF, inline = "tf2", minval = 0, maxval = 10, active = tf2Enabled, display = display.none)

bool    tf3Enabled = input.bool(true,       "",               group = gTF, inline = "tf3", display = display.none)
string  tf3        = input.timeframe("15",  "",               group = gTF, inline = "tf3", active = tf3Enabled, display = display.none)
int     tf3Weight  = input.int(2,           "Trend Weight",   group = gTF, inline = "tf3", minval = 0, maxval = 10, active = tf3Enabled, display = display.none)

bool    tf4Enabled = input.bool(true,       "",               group = gTF, inline = "tf4", display = display.none)
string  tf4        = input.timeframe("60",  "",               group = gTF, inline = "tf4", active = tf4Enabled, display = display.none)
int     tf4Weight  = input.int(2,           "Trend Weight",   group = gTF, inline = "tf4", minval = 0, maxval = 10, active = tf4Enabled, display = display.none)

bool    tf5Enabled = input.bool(true,       "",               group = gTF, inline = "tf5", display = display.none)
string  tf5        = input.timeframe("240", "",               group = gTF, inline = "tf5", active = tf5Enabled, display = display.none)
int     tf5Weight  = input.int(2,           "Trend Weight",   group = gTF, inline = "tf5", minval = 0, maxval = 10, active = tf5Enabled, display = display.none)

bool    tf6Enabled = input.bool(true,       "",               group = gTF, inline = "tf6", display = display.none)
string  tf6        = input.timeframe("D",   "",               group = gTF, inline = "tf6", active = tf6Enabled, display = display.none)
int     tf6Weight  = input.int(3,           "Trend Weight",   group = gTF, inline = "tf6", minval = 0, maxval = 10, active = tf6Enabled, display = display.none)

bool    tf7Enabled = input.bool(true,       "",               group = gTF, inline = "tf7", display = display.none)
string  tf7        = input.timeframe("W",   "",               group = gTF, inline = "tf7", active = tf7Enabled, display = display.none)
int     tf7Weight  = input.int(4,           "Trend Weight",   group = gTF, inline = "tf7", minval = 0, maxval = 10, active = tf7Enabled, display = display.none)

// Market Structure Dashboard
bool    showMSD        = input.bool(true, "Enable",         group = gMsd, display = display.none)
bool    showColEma     = input.bool(true, "EMA Trend",      group = gMsd, inline = "msdCols1", active = showMSD, display = display.none)
bool    showColSwing   = input.bool(true, "Swing H/L",      group = gMsd, inline = "msdCols1", active = showMSD, display = display.none)
bool    showColStruct  = input.bool(true, "Structure",      group = gMsd, inline = "msdCols1", active = showMSD, display = display.none)
bool    showColOB      = input.bool(true, "Order Block",    group = gMsd, inline = "msdCols2", active = showMSD, display = display.none)
bool    showColFVG     = input.bool(true, "FVG",            group = gMsd, inline = "msdCols2", active = showMSD, display = display.none)

// Current Timeframe Dashboard
bool    showCurrTF     = input.bool(true, "Enable",         group = gCtfd, display = display.none)
bool    showCurrSwing  = input.bool(true, "Swing H/L",      group = gCtfd, inline = "currCols", active = showCurrTF, display = display.none)
bool    showCurrVol    = input.bool(true, "Volume",         group = gCtfd, inline = "currCols", active = showCurrTF, display = display.none)
bool    showCurrAtr    = input.bool(true, "Volatility",     group = gCtfd, inline = "currCols", active = showCurrTF, display = display.none)

// Market Context Dashboard
bool    showContext    = input.bool(true, "Enable",         group = gMcd, display = display.none)
bool    showSession    = input.bool(true, "Session",        group = gMcd, inline = "ctxCols", active = showContext, display = display.none)
bool    showKillzone   = input.bool(true, "Killzone",       group = gMcd, inline = "ctxCols", active = showContext, display = display.none)
bool    showBias       = input.bool(true, "Trend Bias",     group = gMcd, inline = "ctxCols", active = showContext, display = display.none)

// HTF Levels Dashboard
bool    showHTFLevels  = input.bool(false, "Enable",        group = gHtf, display = display.none)
bool    showPDHL       = input.bool(true,  "PDH/L",         group = gHtf, inline = "htfCols", active = showHTFLevels, display = display.none)
bool    showPWHL       = input.bool(true,  "PWH/L",         group = gHtf, inline = "htfCols", active = showHTFLevels, display = display.none)
bool    showPMHL       = input.bool(false, "PMH/L",         group = gHtf, inline = "htfCols", active = showHTFLevels, display = display.none)

// Trend Bias Settings
bool    biasUseStruct  = input.bool(true,  "Structure",     group = gBias, inline = "biasFactors1", display = display.none)
bool    biasUseOB      = input.bool(true,  "Order Block",   group = gBias, inline = "biasFactors1", display = display.none)
bool    biasUseFVG     = input.bool(true,  "FVG",           group = gBias, inline = "biasFactors1", display = display.none)
bool    biasUseEMA     = input.bool(false, "EMA Trend",     group = gBias, inline = "biasFactors2", display = display.none)
bool    biasUseSwing   = input.bool(true,  "Swing H/L",     group = gBias, inline = "biasFactors2", display = display.none)

// Settings
int     emaLength      = input.int(9,                   "EMA Length",           group = gSettings, minval = 1, display = display.none)
int     swingLength    = input.int(5,                   "Swing Length",         group = gSettings, minval = 1, tooltip = "Used for Swing H/L detection, Order Blocks, and Structure", display = display.none)
int     atrAvgLength   = input.int(20,                  "Volatility Lookback",  group = gSettings, minval = 1, tooltip = "Period for averaging ATR to determine volatility state", display = display.none)
string  distanceMode   = input.string("Percent",        "Distance Display",     group = gSettings, options = ["Percent", "Price", "Pips"], tooltip = "Show distances as percentage, price, or pips (forex)", display = display.none)
string  dashboardPos   = input.string("Top Right",      "Dashboard Position",   group = gSettings, options = ["Top Left", "Top Center", "Top Right", "Middle Left", "Middle Center", "Middle Right", "Bottom Left", "Bottom Center", "Bottom Right"], display = display.none)
string  dashboardTheme = input.string("Dark Mode",      "Dashboard Colors",     group = gSettings, options = ["Dark Mode", "Light Mode"], display = display.none)
color   bullColor      = input.color(BULL_COLOR,        "Trend Colors   ",      group = gSettings, inline = "trendColors", display = display.none)
color   bearColor      = input.color(BEAR_COLOR,        "",                     group = gSettings, inline = "trendColors", display = display.none)
color   neutColor      = input.color(NEUT_COLOR,        "",                     group = gSettings, inline = "trendColors", display = display.none)
color   sessAsianColor  = input.color(SESSION_ASIAN,    "Session Colors ",      group = gSettings, inline = "sessionColors", tooltip = "Colors for Asian, London, NY sessions", display = display.none)
color   sessLondonColor = input.color(SESSION_LONDON,   "",                     group = gSettings, inline = "sessionColors", display = display.none)
color   sessNyColor     = input.color(SESSION_NY,       "",                     group = gSettings, inline = "sessionColors", display = display.none)
color   kzAsianColor    = input.color(SESSION_ASIAN,    "Killzone Colors ",     group = gSettings, inline = "kzColors", tooltip = "Colors for Asian, London, NY AM, NY Lunch, NY PM killzones", display = display.none)
color   kzLondonColor   = input.color(SESSION_LONDON,   "",                     group = gSettings, inline = "kzColors", display = display.none)
color   kzNyAmColor     = input.color(SESSION_NY,       "",                     group = gSettings, inline = "kzColors", display = display.none)
color   kzLunchColor    = input.color(SESSION_LUNCH,    "",                     group = gSettings, inline = "kzColors", display = display.none)
color   kzPmColor       = input.color(SESSION_PM,       "",                     group = gSettings, inline = "kzColors", display = display.none)
string  tblTextSize    = size.small
// Dashboard position
tablePos = switch dashboardPos
    "Top Left"      => position.top_left
    "Top Center"    => position.top_center
    "Top Right"     => position.top_right
    "Middle Left"   => position.middle_left
    "Middle Center" => position.middle_center
    "Middle Right"  => position.middle_right
    "Bottom Left"   => position.bottom_left
    "Bottom Center" => position.bottom_center
    "Bottom Right"  => position.bottom_right

// Dashboard theme colors
color   bgDark   = dashboardTheme == "Dark Mode" ? BG_DARK_DARK   : BG_DARK_LIGHT
color   bgHeader = dashboardTheme == "Dark Mode" ? BG_HEADER_DARK : BG_HEADER_LIGHT
color   bgRow    = dashboardTheme == "Dark Mode" ? BG_ROW_DARK    : BG_ROW_LIGHT
color   textCol  = dashboardTheme == "Dark Mode" ? TEXT_DARK      : TEXT_LIGHT

// Visual Overlays
bool    showOB             = input.bool(false,              "Order Blocks",             group = gVO, inline = "ob", tooltip = "Display Order Block zones on chart", display = display.none)
color   obBullColor        = input.color(BULL_COLOR,        "",                         group = gVO, inline = "ob", active = showOB, display = display.none)
color   obBearColor        = input.color(BEAR_COLOR,        "",                         group = gVO, inline = "ob", active = showOB, display = display.none)
int     obLookback         = input.int(6,                   "└ Show Last",              group = gVO, inline = "obOpts", minval = 1, maxval = 20, active = showOB, tooltip = "Number of recent Order Blocks to display", display = display.none)
int     obExtend           = input.int(20,                  "Extend",                   group = gVO, inline = "obOpts", minval = 1, active = showOB, tooltip = "Bars to extend Order Block zones into the future", display = display.none)
bool    showOBLabels       = input.bool(false,              "└ Labels",                 group = gVO, active = showOB, tooltip = "Show OB labels inside Order Block zones", display = display.none)

bool    showFVG            = input.bool(false,              "FVGs       ",              group = gVO, inline = "fvg", tooltip = "Display Fair Value Gap zones on chart", display = display.none)
color   fvgBullColor       = input.color(BULL_COLOR,        "",                         group = gVO, inline = "fvg", active = showFVG, display = display.none)
color   fvgBearColor       = input.color(BEAR_COLOR,        "",                         group = gVO, inline = "fvg", active = showFVG, display = display.none)
int     fvgLookback        = input.int(6,                   "└ Show Last",              group = gVO, inline = "fvgOpts", minval = 1, maxval = 20, active = showFVG, tooltip = "Number of recent FVGs to display", display = display.none)
int     fvgExtend          = input.int(20,                  "Extend",                   group = gVO, inline = "fvgOpts", minval = 1, active = showFVG, tooltip = "Bars to extend FVG zones into the future", display = display.none)
bool    showFVGLabels      = input.bool(false,              "└ Labels",                 group = gVO, active = showFVG, tooltip = "Show FVG labels inside Fair Value Gap zones", display = display.none)

bool    showSwingLabels    = input.bool(false,              "Swing Labels",             group = gVO, inline = "swingLabels", tooltip = "Display HH/HL/LH/LL labels at swing points", display = display.none)
color   swingBullColor     = input.color(BULL_COLOR,        "",                         group = gVO, inline = "swingLabels", active = showSwingLabels, display = display.none)
color   swingBearColor     = input.color(BEAR_COLOR,        "",                         group = gVO, inline = "swingLabels", active = showSwingLabels, display = display.none)
int     swingLabelLookback = input.int(10,                  "└ Show Last",              group = gVO, minval = 1, maxval = 500, active = showSwingLabels, tooltip = "Number of recent swing labels to display", display = display.none)

bool    showSwingLines     = input.bool(false,              "Swing Lines ",             group = gVO, inline = "swingLines", tooltip = "Draw horizontal lines at recent swing high/low levels", display = display.none)
color   swingLineLowColor  = input.color(BULL_COLOR,        "",                         group = gVO, inline = "swingLines", active = showSwingLines, display = display.none)
color   swingLineHighColor = input.color(BEAR_COLOR,        "",                         group = gVO, inline = "swingLines", active = showSwingLines, display = display.none)
lineStyle swingLineStyle   = input.enum(lineStyle.dashed,   "",                         group = gVO, inline = "swingLines", active = showSwingLines, display = display.none)
int     swingLinesExtend   = input.int(20,                  "└ Extend",                 group = gVO, minval = 1, active = showSwingLines, tooltip = "Bars to extend swing lines into the future", display = display.none)

bool    plotPDHL           = input.bool(false,              "Previous Day High/Low  ",  group = gVO, inline = "pdhl", display = display.none)
color   pdlColor           = input.color(BULL_COLOR,        "",                         group = gVO, inline = "pdhl", active = plotPDHL, display = display.none)
color   pdhColor           = input.color(BEAR_COLOR,        "",                         group = gVO, inline = "pdhl", active = plotPDHL, display = display.none)
lineStyle pdhlLineStyle    = input.enum(lineStyle.dashed,   "",                         group = gVO, inline = "pdhl", active = plotPDHL, display = display.none)

bool    plotPWHL           = input.bool(false,              "Previous Week High/Low  ", group = gVO, inline = "pwhl", display = display.none)
color   pwlColor           = input.color(BULL_COLOR,        "",                         group = gVO, inline = "pwhl", active = plotPWHL, display = display.none)
color   pwhColor           = input.color(BEAR_COLOR,        "",                         group = gVO, inline = "pwhl", active = plotPWHL, display = display.none)
lineStyle pwhlLineStyle    = input.enum(lineStyle.dashed,   "",                         group = gVO, inline = "pwhl", active = plotPWHL, display = display.none)

bool    plotPMHL           = input.bool(false,              "Previous Month High/Low",  group = gVO, inline = "pmhl", tooltip = "Plot previous month high and low levels", display = display.none)
color   pmlColor           = input.color(BULL_COLOR,        "",                         group = gVO, inline = "pmhl", active = plotPMHL, display = display.none)
color   pmhColor           = input.color(BEAR_COLOR,        "",                         group = gVO, inline = "pmhl", active = plotPMHL, display = display.none)
lineStyle pmhlLineStyle    = input.enum(lineStyle.dashed,   "",                         group = gVO, inline = "pmhl", active = plotPMHL, display = display.none)

int     htfLevelExtend     = input.int(20,               "└ HTF Extend", group = gVO, minval = 1, active = plotPDHL or plotPWHL or plotPMHL, tooltip = "Bars to extend HTF level lines into the future. Labels auto-combine when levels are close.", display = display.none)

bool    showEMA            = input.bool(false,       "EMA",          group = gVO, inline = "ema", display = display.none)
color   emaColor           = input.color(ORANGE,     "",             group = gVO, inline = "ema", active = showEMA, display = display.none)
//#endregion INPUTS

//#region GLOBALS
atrLength = 14
emaValue  = ta.ema(close, emaLength)
pivotHigh = ta.pivothigh(high, swingLength, swingLength)
pivotLow  = ta.pivotlow(low, swingLength, swingLength)
_green    = close > open or close > close[1]
_red      = close < open or close < close[1]
bullFVG   = low[1]  > high[3] and _green[2] and low[1] < high[2] and low[2] < high[3]
bearFVG   = high[1] < low[3]  and _red[2]   and high[1] > low[2] and high[2] > low[3]
//#endregion GLOBALS

//#region TYPES
type ZoneBlock
    int   dir
    float top
    float bottom
    int   barIdx

type SwingData
    float prevH
    float currH
    float prevL
    float currL

type PivotData
    float lastPH
    int   lastPHBar
    float lastPL
    int   lastPLBar

type SwingLineData
    line  highLine
    line  lowLine
    int   lastHBar
    int   lastLBar

type CandleSearch
    float price
    float otherPrice
    int   barIdx
    int   barsBack

type ZoneResult
    int   dir
    float top
    float bottom
    int   barIdx
    bool  isNew

type SwingLabel
    int   x
    float y
    string txt
    bool  isHigh
    color clr
    color textClr
//#endregion TYPES

//#region FUNCTIONS

getLineStyle(lineStyle ls) =>
    switch ls
        lineStyle.solid  => line.style_solid
        lineStyle.dashed => line.style_dashed
        lineStyle.dotted => line.style_dotted

pct(value, base)      => (value / base) * 100
priceToPct(priceVal)  => (priceVal / close) * 100
getPipSize()          => syminfo.mintick * (str.contains(syminfo.ticker, "JPY") ? 100 : 10)
priceToPips(priceVal) => priceVal / getPipSize()
isBreakAbove(level)   => not na(level) and close > level and close[1] <= level
isBreakBelow(level)   => not na(level) and close < level and close[1] >= level

calcTrend() =>
    trendDist = close - emaValue
    dir       = close > emaValue ? 1 : -1
    [dir, trendDist]

calcSwingHL() =>
    var SwingData swing = SwingData.new()
    var bool lowBroken = false
    var bool highBroken = false
    
    newSwingH = not na(pivotHigh)
    newSwingL = not na(pivotLow)
    
    if newSwingH
        swing.prevH := swing.currH
        swing.currH := pivotHigh
        highBroken := false  // Reset on new swing
    if newSwingL
        swing.prevL := swing.currL
        swing.currL := pivotLow
        lowBroken := false   // Reset on new swing
    
    // Track breaks
    if low < swing.currL
        lowBroken := true
    if high > swing.currH
        highBroken := true
    
    // Detect reclaims (broke and came back inside)
    reclaimedLow  = lowBroken and close > swing.currL
    reclaimedHigh = highBroken and close < swing.currH
    
    var int swingBias = 0
    if newSwingH
        swingBias := 1
    if newSwingL
        swingBias := -1
    
    [swing.currH, swing.currL, swingBias, newSwingH, newSwingL, swing.prevH, swing.prevL, reclaimedLow, reclaimedHigh]

// 3. Market Structure - tracks last 3 HH/HL/H/LL sequence
calcStructure() =>
    var SwingData swing = SwingData.new()
    
    // Track last 3 structure labels
    var string struct1  = "--"
    var string struct2  = "--"
    var string struct3  = "--"
    
    var int highType    = 0
    var int lowType     = 0
    
    if not na(pivotHigh)
        swing.prevH := swing.currH
        swing.currH := pivotHigh
        if not na(swing.prevH)
            highType := swing.currH > swing.prevH ? 1 : -1
            struct3 := struct2
            struct2 := struct1
            struct1 := highType > 0 ? "HH" : "LH"
    
    if not na(pivotLow)
        swing.prevL := swing.currL
        swing.currL := pivotLow
        if not na(swing.prevL)
            lowType := swing.currL > swing.prevL ? 1 : -1
            struct3 := struct2
            struct2 := struct1
            struct1 := lowType > 0 ? "HL" : "LL"
    
    structureBias = 0
    if highType == 1 and lowType == 1
        structureBias := 1
    else if highType == -1 and lowType == -1
        structureBias := -1
    
    [highType, lowType, structureBias, struct1, struct2, struct3]

findNearestZone(array<ZoneBlock> zones) =>
    nearestBullDist = 100000.0
    nearestBearDist = 100000.0
    
    zoneCount = zones.size()
    if zoneCount > 0
        for i = 0 to zoneCount - 1
            zone = zones.get(i)
            if zone.dir == 1
                dist = zone.top - close
                if math.abs(dist) < math.abs(nearestBullDist)
                    nearestBullDist := dist
            else
                dist = zone.bottom - close
                if math.abs(dist) < math.abs(nearestBearDist)
                    nearestBearDist := dist
    
    nearestDir  = 0
    nearestDist = 0.0
    
    if math.abs(nearestBullDist) < math.abs(nearestBearDist) and math.abs(nearestBullDist) < 100000.0
        nearestDir  := 1
        nearestDist := nearestBullDist
    else if math.abs(nearestBearDist) < 100000.0
        nearestDir  := -1
        nearestDist := nearestBearDist
    
    [nearestDir, nearestDist]

mitigateOBs(array<ZoneBlock> obs) =>
    obCount = obs.size()
    if obCount > 0
        for i = obCount - 1 to 0
            ob = obs.get(i)
            if ob.dir == 1 and close < ob.bottom
                obs.remove(i)
            else if ob.dir == -1 and close > ob.top
                obs.remove(i)

mitigateFVGs(array<ZoneBlock> fvgs) =>
    fvgCount = fvgs.size()
    if fvgCount > 0
        for i = fvgCount - 1 to 0
            fvg = fvgs.get(i)
            if fvg.dir == 1 and low < fvg.bottom
                fvgs.remove(i)
            else if fvg.dir == -1 and high > fvg.top
                fvgs.remove(i)

limitArraySize(array<ZoneBlock> arr, int maxSize) =>
    if arr.size() > maxSize
        arr.pop()

limitSwingLabels(array<SwingLabel> arr, int maxSize) =>
    if arr.size() > maxSize
        arr.shift()

findLowestCandle(int barsBack) =>
    search = CandleSearch.new(low, high, bar_index, barsBack)
    for i = 0 to math.max(0, math.min(barsBack, 50))
        if low[i] < search.price
            search.price      := low[i]
            search.otherPrice := high[i]
            search.barIdx     := bar_index - i
    search

findHighestCandle(int barsBack) =>
    search = CandleSearch.new(high, low, bar_index, barsBack)
    for i = 0 to math.max(0, math.min(barsBack, 50))
        if high[i] > search.price
            search.price      := high[i]
            search.otherPrice := low[i]
            search.barIdx     := bar_index - i
    search

hasOverlappingOB(int dir, float top, float bottom, array<ZoneBlock> obs) =>
    overlaps = false
    for ob in obs
        if ob.dir == dir and top >= ob.bottom and bottom <= ob.top
            overlaps := true
            break
    overlaps

detectOB(int dir, int pivotBar, array<ZoneBlock> obs, int maxOBs, ZoneResult result) =>
    barsBack = bar_index - pivotBar - 1
    search   = dir > 0 ? findLowestCandle(barsBack) : findHighestCandle(barsBack)
    
    newTop    = dir > 0 ? search.otherPrice : search.price
    newBottom = dir > 0 ? search.price      : search.otherPrice
    
    if not hasOverlappingOB(dir, newTop, newBottom, obs)
        result.dir    := dir
        result.top    := newTop
        result.bottom := newBottom
        result.barIdx := search.barIdx
        result.isNew  := true
        
        obs.unshift(ZoneBlock.new(dir, newTop, newBottom, search.barIdx))
        limitArraySize(obs, maxOBs)

detectOBSimple(int dir, int pivotBar, array<ZoneBlock> obs, int maxOBs) =>
    barsBack = bar_index - pivotBar - 1
    search   = dir > 0 ? findLowestCandle(barsBack) : findHighestCandle(barsBack)
    top      = dir > 0 ? search.otherPrice : search.price
    bottom   = dir > 0 ? search.price      : search.otherPrice
    
    if not hasOverlappingOB(dir, top, bottom, obs)
        obs.unshift(ZoneBlock.new(dir, top, bottom, search.barIdx))
        limitArraySize(obs, maxOBs)


trackPivotBreaks(PivotData pivot) =>
    bullBreak = false
    bearBreak = false
    bullBar   = 0
    bearBar   = 0
    
    // Track new pivots
    if not na(pivotHigh)
        pivot.lastPH    := pivotHigh
        pivot.lastPHBar := bar_index - swingLength
    
    if not na(pivotLow)
        pivot.lastPL    := pivotLow
        pivot.lastPLBar := bar_index - swingLength
    

    if isBreakAbove(pivot.lastPH) and not na(pivot.lastPHBar)
        bullBreak := true
        bullBar   := pivot.lastPHBar
        pivot.lastPH    := na
        pivot.lastPHBar := na
    
    if isBreakBelow(pivot.lastPL) and not na(pivot.lastPLBar)
        bearBreak := true
        bearBar   := pivot.lastPLBar
        pivot.lastPL    := na
        pivot.lastPLBar := na
    
    [bullBreak, bullBar, bearBreak, bearBar]


calcOrderBlock(maxOBs) =>
    var PivotData        pivot  = PivotData.new()
    var array<ZoneBlock> obs    = array.new<ZoneBlock>()
    var ZoneResult       result = ZoneResult.new()
    
    [bullBreak, bullBar, bearBreak, bearBar] = trackPivotBreaks(pivot)
    
    result.isNew := false
    
    if bullBreak
        detectOB(1, bullBar, obs, maxOBs, result)
    
    if bearBreak
        detectOB(-1, bearBar, obs, maxOBs, result)
    
    mitigateOBs(obs)
    [nearestDir, nearestDist] = findNearestZone(obs)
    
    [result.dir, result.top, result.bottom, result.isNew, nearestDir, nearestDist] 


detectFVGSignal() =>
    fvgDir    = 0
    fvgTop    = 0.0
    fvgBottom = 0.0
    fvgBar    = 0
    
    
    if bullFVG
        fvgDir    := 1
        fvgTop    := low[1]
        fvgBottom := high[3]
        fvgBar    := bar_index - 3
    
    // Bear FVG: gap down
    if bearFVG
        fvgDir    := -1
        fvgTop    := low[3]
        fvgBottom := high[1]
        fvgBar    := bar_index - 3
    
    [fvgDir, fvgTop, fvgBottom, fvgBar]

calcFVG(maxFVGs) =>
    var array<ZoneBlock> fvgs = array.new<ZoneBlock>()
    
    [fvgDir, fvgTop, fvgBottom, fvgBar] = detectFVGSignal()
    
    if fvgDir != 0
        fvgs.unshift(ZoneBlock.new(fvgDir, fvgTop, fvgBottom, fvgBar))
        limitArraySize(fvgs, maxFVGs)
    
    mitigateFVGs(fvgs)
    [nearestDir, nearestDist] = findNearestZone(fvgs)
    
    [fvgDir, fvgTop, fvgBottom, fvgDir != 0, nearestDir, nearestDist]

[currSwingH, currSwingL, _currSwingBias, currNewSwingH,     currNewSwingL,      currPrevSwingH,     currPrevSwingL, currReclaimLow, currReclaimHigh  ]   = calcSwingHL()
[currObDir,  currObTop,  currObBottom,   currNewOB,         _currObNearDir,     _currObNearDist ]                       = calcOrderBlock(obLookback)
[currFvgDir, currFvgTop, currFvgBottom,  currNewFVG,        _currFvgNearDir,    _currFvgNearDist]                       = calcFVG(fvgLookback)

var array<ZoneBlock> currTfOBs   = array.new<ZoneBlock>()
var array<box>       obBoxes     = array.new<box>()
var PivotData        currTfPivot = PivotData.new()

[currBullBreak, currBullBar, currBearBreak, currBearBar] = trackPivotBreaks(currTfPivot)

if currBullBreak
    detectOBSimple(1, currBullBar, currTfOBs, obLookback)

if currBearBreak
    detectOBSimple(-1, currBearBar, currTfOBs, obLookback)

mitigateOBs(currTfOBs)

var array<ZoneBlock> currTfFvgs = array.new<ZoneBlock>()
var array<box>       fvgBoxes   = array.new<box>()

[currFvgSigDir, currFvgSigTop, currFvgSigBottom, currFvgSigBar] = detectFVGSignal()

if currFvgSigDir != 0
    currTfFvgs.unshift(ZoneBlock.new(currFvgSigDir, currFvgSigTop, currFvgSigBottom, currFvgSigBar))
    limitArraySize(currTfFvgs, fvgLookback)

mitigateFVGs(currTfFvgs)

// Track swing labels
var SwingLineData     swingLines  = SwingLineData.new()
var array<SwingLabel> swingLabels = array.new<SwingLabel>()

if currNewSwingH
    swingLines.lastHBar := bar_index - swingLength
    
    labelText  = "HH"
    labelColor = color.new(swingBullColor, 80)
    labelTextColor = swingBullColor
    if not na(currPrevSwingH) and currSwingH < currPrevSwingH
        labelText  := "LH"
        labelColor := color.new(swingBearColor, 80)
        labelTextColor := swingBearColor
    swingLabels.push(SwingLabel.new(bar_index - swingLength, currSwingH, labelText, true, labelColor, labelTextColor))
    limitSwingLabels(swingLabels, swingLabelLookback)

if currNewSwingL
    swingLines.lastLBar := bar_index - swingLength
    
    labelText  = "HL"
    labelColor = color.new(swingBullColor, 80)
    labelTextColor = swingBullColor
    if not na(currPrevSwingL) and currSwingL < currPrevSwingL
        labelText  := "LL"
        labelColor := color.new(swingBearColor, 80)
        labelTextColor := swingBearColor
    swingLabels.push(SwingLabel.new(bar_index - swingLength, currSwingL, labelText, false, labelColor, labelTextColor))
    limitSwingLabels(swingLabels, swingLabelLookback)

calcAll() =>
    [trendDir, trendDist]                                                                               = calcTrend()
    [swingH, swingL, _swingBias, _newSwingH, _newSwingL, _prevSwingH, _prevSwingL, reclaimLow, reclaimHigh] = calcSwingHL()
    [_highType, _lowType, structBias, struct1, struct2, struct3]                                        = calcStructure()
    [_obDir,  _obTop,  _obBottom,  _newOB,  obNearDir,  obNearDist]                                     = calcOrderBlock(obLookback)
    [_fvgDir, _fvgTop, _fvgBottom, _newFVG, fvgNearDir, fvgNearDist]                                    = calcFVG(fvgLookback)
    [trendDir, trendDist, swingH, swingL, structBias, struct1, struct2, struct3, obNearDir, obNearDist, fvgNearDir, fvgNearDist, reclaimLow, reclaimHigh]

tfActive(enabled, tf) => enabled and timeframe.in_seconds() <= timeframe.in_seconds(tf)

// MTF data requests
[t1_trendDir, t1_trendDist, t1_swingH, t1_swingL, t1_structBias, t1_struct1, t1_struct2, t1_struct3, t1_obNearDir, t1_obNearDist, t1_fvgNearDir, t1_fvgNearDist, t1_reclaimLow, t1_reclaimHigh] = request.security(syminfo.tickerid, tfActive(tf1Enabled, tf1) ? tf1 : na, calcAll())
[t2_trendDir, t2_trendDist, t2_swingH, t2_swingL, t2_structBias, t2_struct1, t2_struct2, t2_struct3, t2_obNearDir, t2_obNearDist, t2_fvgNearDir, t2_fvgNearDist, t2_reclaimLow, t2_reclaimHigh] = request.security(syminfo.tickerid, tfActive(tf2Enabled, tf2) ? tf2 : na, calcAll())
[t3_trendDir, t3_trendDist, t3_swingH, t3_swingL, t3_structBias, t3_struct1, t3_struct2, t3_struct3, t3_obNearDir, t3_obNearDist, t3_fvgNearDir, t3_fvgNearDist, t3_reclaimLow, t3_reclaimHigh] = request.security(syminfo.tickerid, tfActive(tf3Enabled, tf3) ? tf3 : na, calcAll())
[t4_trendDir, t4_trendDist, t4_swingH, t4_swingL, t4_structBias, t4_struct1, t4_struct2, t4_struct3, t4_obNearDir, t4_obNearDist, t4_fvgNearDir, t4_fvgNearDist, t4_reclaimLow, t4_reclaimHigh] = request.security(syminfo.tickerid, tfActive(tf4Enabled, tf4) ? tf4 : na, calcAll())
[t5_trendDir, t5_trendDist, t5_swingH, t5_swingL, t5_structBias, t5_struct1, t5_struct2, t5_struct3, t5_obNearDir, t5_obNearDist, t5_fvgNearDir, t5_fvgNearDist, t5_reclaimLow, t5_reclaimHigh] = request.security(syminfo.tickerid, tfActive(tf5Enabled, tf5) ? tf5 : na, calcAll())
[t6_trendDir, t6_trendDist, t6_swingH, t6_swingL, t6_structBias, t6_struct1, t6_struct2, t6_struct3, t6_obNearDir, t6_obNearDist, t6_fvgNearDir, t6_fvgNearDist, t6_reclaimLow, t6_reclaimHigh] = request.security(syminfo.tickerid, tfActive(tf6Enabled, tf6) ? tf6 : na, calcAll())
[t7_trendDir, t7_trendDist, t7_swingH, t7_swingL, t7_structBias, t7_struct1, t7_struct2, t7_struct3, t7_obNearDir, t7_obNearDist, t7_fvgNearDir, t7_fvgNearDist, t7_reclaimLow, t7_reclaimHigh] = request.security(syminfo.tickerid, tfActive(tf7Enabled, tf7) ? tf7 : na, calcAll())

dirScore(dir) => dir > 0 ? 1 : dir < 0 ? -1 : 0

swingBiasDir(swingH, swingL) =>
    swingRange = math.max(swingH - swingL, syminfo.mintick)
    swingPct   = (close - swingL) / swingRange * 100
    swingPct < 30 ? 1 : swingPct > 70 ? -1 : 0

calcBiasForTf(structBias, obDir, fvgDir, trendDir, swingDir, weight) =>
    score = 0
    if biasUseStruct
        score += dirScore(structBias)
    if biasUseOB
        score += dirScore(obDir)
    if biasUseFVG
        score += dirScore(fvgDir)
    if biasUseEMA
        score += dirScore(trendDir)
    if biasUseSwing
        score += swingDir
    score * weight

// Count active factors for max score calculation
biasFactorCount() =>
    count = 0
    if biasUseStruct
        count += 1
    if biasUseOB
        count += 1
    if biasUseFVG
        count += 1
    if biasUseEMA
        count += 1
    if biasUseSwing
        count += 1
    count

dirColor(dir) => dir > 0 ? bullColor : dir < 0 ? bearColor : neutColor

fmtTF(tf) =>
    switch tf
        "1"   => "1M"
        "3"   => "3M"
        "5"   => "5M"
        "15"  => "15M"
        "30"  => "30M"
        "45"  => "45M"
        "60"  => "1H"
        "120" => "2H"
        "180" => "3H"
        "240" => "4H"
        "D"   => "D"
        "W"   => "W"
        "M"   => "M"
        => tf

fmtPct(dir, priceDist) =>
    arrow = dir > 0 ? " ↑" : " ↓"
    switch distanceMode
        "Price"   => str.tostring(math.abs(priceDist), "#.##") + arrow
        "Pips"    => str.tostring(math.abs(priceToPips(priceDist)), "#.#") + " pips" + arrow
        => str.tostring(priceToPct(priceDist), "#.##") + "%" + arrow

fmtSwingBar(swingH, swingL, reclaimLow, reclaimHigh) =>
    swingRange  = math.max(swingH - swingL, syminfo.mintick)
    rawPct      = pct(close - swingL, swingRange)
    bars        = 9
    
    barText     = ""
    barColor    = neutColor
    
    if rawPct < 0
        // Price below swing low - broke swing low
        barText  := "↓ L " + str.repeat("─", bars) + " H  "
        barColor := bearColor
    else if rawPct > 100
        // Price above swing high - broke swing high
        barText  := "  L " + str.repeat("─", bars) + " H ↑"
        barColor := bullColor
    else if reclaimLow
        // Swept low, back inside - bullish signal (liquidity grab)
        pos      = math.round(rawPct / 100 * (bars - 1))
        left     = str.repeat("─", pos)
        right    = str.repeat("─", bars - 1 - pos)
        barText  := "⤴ L " + left + "⬤" + right + " H  "
        barColor := bullColor
    else if reclaimHigh
        // Swept high, back inside - bearish signal (liquidity grab)
        pos      = math.round(rawPct / 100 * (bars - 1))
        left     = str.repeat("─", pos)
        right    = str.repeat("─", bars - 1 - pos)
        barText  := "  L " + left + "⬤" + right + " H ⤵"
        barColor := bearColor
    else
        // Price inside range
        pos      = math.round(rawPct / 100 * (bars - 1))
        left     = str.repeat("─", pos)
        right    = str.repeat("─", bars - 1 - pos)
        barText  := "  L " + left + "⬤" + right + " H  "
        barColor := rawPct >= 50 ? bearColor : bullColor
    
    [barText, barColor, rawPct]

fmtStructure(struct1, struct2, struct3, structBias) =>
    arrow = structBias == 1 ? " ↑" : structBias == -1 ? " ↓" : " →"
    struct3 + "-" + struct2 + "-" + struct1 + arrow

fmtZone(zoneType, nearestDir, nearestDist) =>
    if nearestDir == 0
        "NONE"
    else if nearestDir == 1
        if nearestDist >= 0
            "IN BULL " + zoneType + " ↑"
        else
            distStr = switch distanceMode
                "Price" => str.tostring(math.abs(nearestDist), "#.##")
                "Pips"  => str.tostring(math.abs(priceToPips(nearestDist)), "#.#") + " pips"
                => str.tostring(math.abs(priceToPct(nearestDist)), "#.#") + "%"
            "BULL " + zoneType + " (" + distStr + ") ↑"
    else
        if nearestDist <= 0
            "IN BEAR " + zoneType + " ↓"
        else
            distStr = switch distanceMode
                "Price" => str.tostring(nearestDist, "#.##")
                "Pips"  => str.tostring(priceToPips(nearestDist), "#.#") + " pips"
                => str.tostring(priceToPct(nearestDist), "#.#") + "%"
            "BEAR " + zoneType + " (+" + distStr + ") ↓"

fmtOB(nearestDir, nearestDist)  => fmtZone("OB", nearestDir, nearestDist)
fmtFVG(nearestDir, nearestDist) => fmtZone("FVG", nearestDir, nearestDist)


tipZone(zoneType, nearestDir, nearestDist, tf) =>
    tfStr = fmtTF(tf)
    distStr = switch distanceMode
        "Price" => str.tostring(math.abs(nearestDist), "#.##")
        "Pips"  => str.tostring(math.abs(priceToPips(nearestDist)), "#.#") + " pips"
        => str.tostring(math.abs(priceToPct(nearestDist)), "#.##") + "%"
    if nearestDir == 0
        "No unmitigated " + zoneType + "s on " + tfStr
    else if nearestDir == 1
        if nearestDist >= 0
            "Price is currently in bullish " + zoneType + " on " + tfStr
        else
            "Nearest " + zoneType + " is bullish, " + distStr + " away on " + tfStr
    else
        if nearestDist <= 0
            "Price is currently in bearish " + zoneType + " on " + tfStr
        else
            "Nearest " + zoneType + " is bearish, " + distStr + " away on " + tfStr

tipOB(nearestDir, nearestDist, tf)  => tipZone("OB", nearestDir, nearestDist, tf)
tipFVG(nearestDir, nearestDist, tf) => tipZone("FVG", nearestDir, nearestDist, tf)


tipEma(trendDir, trendDist, tf) =>
    tfStr = fmtTF(tf)
    distStr = switch distanceMode
        "Price" => str.tostring(math.abs(trendDist), "#.##")
        "Pips"  => str.tostring(math.abs(priceToPips(trendDist)), "#.#") + " pips"
        => str.tostring(math.abs(priceToPct(trendDist)), "#.##") + "%"
    if trendDir > 0
        "Price is " + distStr + " above EMA on " + tfStr
    else if trendDir < 0
        "Price is " + distStr + " below EMA on " + tfStr
    else
        "Price is at EMA on " + tfStr


tipSwing(swingPct, reclaimLow, reclaimHigh, tf) =>
    tfStr = fmtTF(tf)
    if reclaimLow
        "Liquidity sweep - price swept swing low and reclaimed on " + tfStr
    else if reclaimHigh
        "Liquidity sweep - price swept swing high and reclaimed on " + tfStr
    else if swingPct < 0
        "Price below swing low on " + tfStr + " - swing low broken"
    else if swingPct > 100
        "Price above swing high on " + tfStr + " - swing high broken"
    else if swingPct <= 30
        "Price nearing swing low on " + tfStr
    else if swingPct >= 70
        "Price nearing swing high on " + tfStr
    else
        "Price in mid-range on " + tfStr


tipStructure(structBias, tf) =>
    tfStr = fmtTF(tf)
    if structBias == 1
        "Bullish market structure on " + tfStr
    else if structBias == -1
        "Bearish market structure on " + tfStr
    else
        "Mixed market structure on " + tfStr


getSession(int hr) =>
    sessName  = ""
    sessColor = GRAY
    sessStart = 0
    sessDur   = 0
    
    if hr >= 19 or hr < 3
        sessName  := "ASIAN"
        sessColor := sessAsianColor
        sessStart := 19
        sessDur   := 8
    else if hr >= 8 and hr < 17
        sessName  := "NEW YORK"
        sessColor := sessNyColor
        sessStart := 8
        sessDur   := 9
    else if hr >= 3 and hr < 8
        sessName  := "LONDON"
        sessColor := sessLondonColor
        sessStart := 3
        sessDur   := 9//9 for visual purpose only
    else
        sessName := "OFF HOURS"
    
    [sessName, sessColor, sessStart, sessDur]


getKillzone(int hr, int mins, color neutralColor) =>
    kzName    = ""
    kzColor   = neutralColor
    nextText  = ""
    nextColor = neutralColor
    
    currentMins   = hr * 60 + mins
    
    // ICT Killzones
    asianKzStart  = 20 * 60           // 20:00
    asianKzEnd    = 24 * 60           // 00:00
    londonKzStart = 2 * 60            // 02:00
    londonKzEnd   = 5 * 60            // 05:00
    nyAmKzStart   = 9 * 60 + 30       // 09:30
    nyAmKzEnd     = 11 * 60           // 11:00
    nyLunchStart  = 12 * 60           // 12:00
    nyLunchEnd    = 13 * 60           // 13:00
    nyPmKzStart   = 13 * 60 + 30      // 13:30
    nyPmKzEnd     = 16 * 60           // 16:00
    
    if currentMins >= asianKzStart or currentMins < 0
        kzName    := "ASIAN KZ"
        kzColor   := kzAsianColor
        nextColor := kzAsianColor
    else if currentMins >= londonKzStart and currentMins < londonKzEnd
        kzName    := "LONDON KZ"
        kzColor   := kzLondonColor
        nextColor := kzLondonColor
    else if currentMins >= nyAmKzStart and currentMins < nyAmKzEnd
        kzName    := "NY AM KZ"
        kzColor   := kzNyAmColor
        nextColor := kzNyAmColor
    else if currentMins >= nyLunchStart and currentMins < nyLunchEnd
        kzName    := "NY LUNCH"
        kzColor   := kzLunchColor
        nextColor := kzLunchColor
    else if currentMins >= nyPmKzStart and currentMins < nyPmKzEnd
        kzName    := "NY PM KZ"
        kzColor   := kzPmColor
        nextColor := kzPmColor
    else
        kzName  := "NO KILLZONE"
        kzColor := neutColor
        
        nextKzName    = ""
        minutesToNext = 0
        
        if currentMins < londonKzStart
            nextKzName    := "LONDON KZ"
            minutesToNext := londonKzStart - currentMins
            nextColor     := kzLondonColor
        else if currentMins < nyAmKzStart
            nextKzName    := "NY AM KZ"
            minutesToNext := nyAmKzStart - currentMins
            nextColor     := kzNyAmColor
        else if currentMins < nyLunchStart
            nextKzName    := "NY LUNCH"
            minutesToNext := nyLunchStart - currentMins
            nextColor     := kzLunchColor
        else if currentMins < nyPmKzStart
            nextKzName    := "NY PM KZ"
            minutesToNext := nyPmKzStart - currentMins
            nextColor     := kzPmColor
        else if currentMins < asianKzStart
            nextKzName    := "ASIAN KZ"
            minutesToNext := asianKzStart - currentMins
            nextColor     := kzAsianColor
        
        nextKzHours = math.floor(minutesToNext / 60)
        nextKzMins  = minutesToNext % 60
        nextText    := nextKzName + " in " + str.tostring(nextKzHours) + "h:" + str.tostring(nextKzMins, "00") + "m"
    
    [kzName, kzColor, nextText, nextColor]


drawMsdRow(table tbl, int row, string tfStr, int trendDir, float trendDist, string swingBar, color swingColor, float swingPct, bool reclaimLow, bool reclaimHigh, int structBias, string struct1, string struct2, string struct3, int obNearDir, float obNearDist, int fvgNearDir, float fvgNearDist, string tf) =>
    col = 0
    table.cell(tbl, col, row, tfStr, text_color = textCol, text_size = tblTextSize, bgcolor = color.new(bgRow, 15))
    if showColSwing
        col += 1
        table.cell(tbl, col, row, swingBar,                                             text_color = swingColor,           text_size = tblTextSize, tooltip = tipSwing(swingPct, reclaimLow, reclaimHigh, tf))
    if showColStruct
        col += 1
        table.cell(tbl, col, row, fmtStructure(struct1, struct2, struct3, structBias),  text_color = dirColor(structBias), text_size = tblTextSize, tooltip = tipStructure(structBias, tf))
    if showColOB
        col += 1
        table.cell(tbl, col, row, fmtOB(obNearDir, obNearDist),                         text_color = dirColor(obNearDir),  text_size = tblTextSize, tooltip = tipOB(obNearDir, obNearDist, tf))
    if showColFVG
        col += 1
        table.cell(tbl, col, row, fmtFVG(fvgNearDir, fvgNearDist),                      text_color = dirColor(fvgNearDir), text_size = tblTextSize, tooltip = tipFVG(fvgNearDir, fvgNearDist, tf))
    if showColEma
        col += 1
        table.cell(tbl, col, row, fmtPct(trendDir, trendDist),                           text_color = dirColor(trendDir),   text_size = tblTextSize, tooltip = tipEma(trendDir, trendDist, tf))


drawBottomHeaders(table tbl, int row) =>
    col = 0
    if showCurrTF
        if showCurrVol
            table.cell(tbl, col, row, "VOLUME",     text_color = textCol, text_size = size.normal, tooltip = "Current volume")
            col += 1
        if showCurrSwing
            table.cell(tbl, col, row, "SWING H/L",  text_color = textCol, text_size = size.normal, tooltip = "Price position within current swing range")
            col += 1
        if showCurrAtr
            table.cell(tbl, col, row, "VOLATILITY", text_color = textCol, text_size = size.normal, tooltip = "Current volatility")
            col += 1
    if showContext
        if showSession
            table.cell(tbl, col, row, "SESSION",    text_color = textCol, text_size = size.normal, tooltip = "Current trading session")
            col += 1
        if showKillzone
            table.cell(tbl, col, row, "KILLZONE",   text_color = textCol, text_size = size.normal, tooltip = "Current killzone")
            col += 1
        if showBias
            table.cell(tbl, col, row, "TREND BIAS", text_color = textCol, text_size = size.normal, tooltip = "Current trend bias")
            col += 1


drawBottomData1(table tbl, int row, string swingText, color swingColor, string tipSwing, string volBar, color volColor, string tipVol, string volState, color volStateColor, string tipVolState, string sessName, color sessColor, string tipSess, string kzName, color kzColor, string tipKz, string biasLbl, color biasClr, string tipBias) =>
    col = 0
    if showCurrTF
        if showCurrVol
            table.cell(tbl, col, row, volBar,    text_color = volColor,       text_size = tblTextSize, tooltip = tipVol)
            col += 1
        if showCurrSwing
            table.cell(tbl, col, row, swingText, text_color = swingColor,     text_size = tblTextSize, tooltip = tipSwing)
            col += 1
        if showCurrAtr
            table.cell(tbl, col, row, volState,  text_color = volStateColor,  text_size = tblTextSize, tooltip = tipVolState)
            col += 1
    if showContext
        if showSession
            table.cell(tbl, col, row, sessName,  text_color = sessColor,      text_size = tblTextSize, tooltip = tipSess)
            col += 1
        if showKillzone
            table.cell(tbl, col, row, kzName,    text_color = kzColor,        text_size = tblTextSize, tooltip = tipKz)
            col += 1
        if showBias
            table.cell(tbl, col, row, biasLbl,   text_color = biasClr,        text_size = tblTextSize, tooltip = tipBias)
            col += 1


drawBottomData2(table tbl, int row, color swingColor, string volState, color volColor, string sessProgress, color sessColor, string tipSessProg, string nextKz, color nextKzClr, string tipNextKz, string biasScr, color biasClr) =>
    col = 0
    if showCurrTF
        if showCurrVol
            table.cell(tbl, col, row, volState,     text_color = volColor,   text_size = tblTextSize)
            col += 1
        if showCurrSwing
            table.cell(tbl, col, row, "",           text_color = swingColor, text_size = tblTextSize)
            col += 1
        if showCurrAtr
            table.cell(tbl, col, row, "",           text_color = volColor,   text_size = tblTextSize)
            col += 1
    if showContext
        if showSession
            table.cell(tbl, col, row, sessProgress, text_color = sessColor,  text_size = tblTextSize, tooltip = tipSessProg)
            col += 1
        if showKillzone
            table.cell(tbl, col, row, nextKz,       text_color = nextKzClr,  text_size = tblTextSize, tooltip = tipNextKz)
            col += 1
        if showBias
            table.cell(tbl, col, row, biasScr,      text_color = biasClr,    text_size = tblTextSize)
            col += 1


drawHTFRows(table tbl, int startRow, string pdBar, color pdColor, string tipPD, string pwBar, color pwColor, string tipPW, string pmBar, color pmColor, string tipPM) =>
    row = startRow
    if showHTFLevels
        if showPDHL
            table.cell(tbl, 0, row, "PDH/L", text_color = textCol, text_size = tblTextSize, bgcolor = color.new(bgRow, 15))
            table.cell(tbl, 1, row, pdBar, text_color = pdColor, text_size = tblTextSize, tooltip = tipPD)
            row += 1
        if showPWHL
            table.cell(tbl, 0, row, "PWH/L", text_color = textCol, text_size = tblTextSize, bgcolor = color.new(bgRow, 15))
            table.cell(tbl, 1, row, pwBar, text_color = pwColor, text_size = tblTextSize, tooltip = tipPW)
            row += 1
        if showPMHL
            table.cell(tbl, 0, row, "PMH/L", text_color = textCol, text_size = tblTextSize, bgcolor = color.new(bgRow, 15))
            table.cell(tbl, 1, row, pmBar, text_color = pmColor, text_size = tblTextSize, tooltip = tipPM)
//#endregion FUNCTIONS

//#region CALCULATIONS

[t1_swingBar, t1_swingColor, t1_swingPct] = fmtSwingBar(t1_swingH, t1_swingL, t1_reclaimLow, t1_reclaimHigh)
[t2_swingBar, t2_swingColor, t2_swingPct] = fmtSwingBar(t2_swingH, t2_swingL, t2_reclaimLow, t2_reclaimHigh)
[t3_swingBar, t3_swingColor, t3_swingPct] = fmtSwingBar(t3_swingH, t3_swingL, t3_reclaimLow, t3_reclaimHigh)
[t4_swingBar, t4_swingColor, t4_swingPct] = fmtSwingBar(t4_swingH, t4_swingL, t4_reclaimLow, t4_reclaimHigh)
[t5_swingBar, t5_swingColor, t5_swingPct] = fmtSwingBar(t5_swingH, t5_swingL, t5_reclaimLow, t5_reclaimHigh)
[t6_swingBar, t6_swingColor, t6_swingPct] = fmtSwingBar(t6_swingH, t6_swingL, t6_reclaimLow, t6_reclaimHigh)
[t7_swingBar, t7_swingColor, t7_swingPct] = fmtSwingBar(t7_swingH, t7_swingL, t7_reclaimLow, t7_reclaimHigh)


// Current TF swing bar
currSwingRange    = math.max(currSwingH - currSwingL, syminfo.mintick)
currSwingPct      = pct(close - currSwingL, currSwingRange)
currBars          = 9

currSwingBarText  = ""
currSwingBarColor = neutColor

if currSwingPct < 0
    // Price below swing low - swing low broken
    currSwingBarText  := "↓ L " + str.repeat("─", currBars) + " H  "
    currSwingBarColor := bearColor
else if currSwingPct > 100
    // Price above swing high - swing high broken
    currSwingBarText  := "  L " + str.repeat("─", currBars) + " H ↑"
    currSwingBarColor := bullColor
else if currReclaimLow
    // Swept low, back inside - bullish signal (liquidity grab)
    currPos           = math.round(currSwingPct / 100 * (currBars - 1))
    currLeft          = str.repeat("─", currPos)
    currRight         = str.repeat("─", currBars - 1 - currPos)
    currSwingBarText  := "⤴ L " + currLeft + "⬤" + currRight + " H  "
    currSwingBarColor := bullColor
else if currReclaimHigh
    // Swept high, back inside - bearish signal (liquidity grab)
    currPos           = math.round(currSwingPct / 100 * (currBars - 1))
    currLeft          = str.repeat("─", currPos)
    currRight         = str.repeat("─", currBars - 1 - currPos)
    currSwingBarText  := "  L " + currLeft + "⬤" + currRight + " H ⤵"
    currSwingBarColor := bearColor
else
    // Price inside range
    currPos           = math.round(currSwingPct / 100 * (currBars - 1))
    currLeft          = str.repeat("─", currPos)
    currRight         = str.repeat("─", currBars - 1 - currPos)
    currSwingBarText  := "  L " + currLeft + "⬤" + currRight + " H  "
    currSwingBarColor := currSwingPct >= 50 ? bearColor : bullColor

// Volume calculations
volumeLength    = 20
avgVolume       = ta.sma(volume, volumeLength)
volumePct       = pct(volume, avgVolume)
volumeState     = volumePct > 200 ? "EXTREME" : volumePct > 120 ? "HIGH" : volumePct > 80 ? "NORMAL" : volumePct > 50 ? "LOW" : "VERY LOW"
volumeBars      = volumePct > 200 ? 5 : volumePct > 120 ? 4 : volumePct > 80 ? 3 : volumePct > 50 ? 2 : 1
volumeBarString = str.repeat("█", volumeBars) + str.repeat("░", math.max(0, 5 - volumeBars))
volumeColor     = volumePct > 200 ? YELLOW : volumePct > 120 ? ORANGE : neutColor

// Volatility calculations
currentATR      = ta.atr(atrLength)
avgATR          = ta.sma(currentATR, atrAvgLength)
atrPct          = pct(currentATR, avgATR)
volatilityState = atrPct > 130 ? "HIGH"    : atrPct > 70 ? "NORMAL"  : "LOW"
volatilityColor = atrPct > 130 ? bearColor : atrPct > 70 ? neutColor : bullColor

// HTF Levels (PDH/L, PWH/L, PMH/L)
[pdHigh, pdLow] = request.security(syminfo.tickerid, "D", [high[1], low[1]], lookahead = barmerge.lookahead_on)
[pwHigh, pwLow] = request.security(syminfo.tickerid, "W", [high[1], low[1]], lookahead = barmerge.lookahead_on)
[pmHigh, pmLow] = request.security(syminfo.tickerid, "M", [high[1], low[1]], lookahead = barmerge.lookahead_on)

// HTF Levels reclaim tracking
// Track HTF level breaks and reclaims
trackHTFLevel(float levelHigh, float levelLow, simple string tf) =>
    var bool lowBroken  = false
    var bool highBroken = false
    
    if timeframe.change(tf)
        lowBroken  := false
        highBroken := false
    
    if low < levelLow
        lowBroken := true
    if high > levelHigh
        highBroken := true
    
    reclaimLow  = lowBroken and close > levelLow
    reclaimHigh = highBroken and close < levelHigh
    
    [reclaimLow, reclaimHigh]

// HTF Levels reclaim detection
[pdReclaimLow, pdReclaimHigh] = trackHTFLevel(pdHigh, pdLow, "D")
[pwReclaimLow, pwReclaimHigh] = trackHTFLevel(pwHigh, pwLow, "W")
[pmReclaimLow, pmReclaimHigh] = trackHTFLevel(pmHigh, pmLow, "M")

// Format HTF level bars
[pdSwingBar, pdSwingColor, pdSwingPct] = fmtSwingBar(pdHigh, pdLow, pdReclaimLow, pdReclaimHigh)
[pwSwingBar, pwSwingColor, pwSwingPct] = fmtSwingBar(pwHigh, pwLow, pwReclaimLow, pwReclaimHigh)
[pmSwingBar, pmSwingColor, pmSwingPct] = fmtSwingBar(pmHigh, pmLow, pmReclaimLow, pmReclaimHigh)

// HTF Levels tooltips
tipPD = pdReclaimLow ? "Liquidity sweep - price swept PDL and reclaimed" : pdReclaimHigh ? "Liquidity sweep - price swept PDH and reclaimed" : pdSwingPct < 0 ? "Price below PDL - previous day low broken" : pdSwingPct > 100 ? "Price above PDH - previous day high broken" : pdSwingPct <= 30 ? "Price nearing PDL" : pdSwingPct >= 70 ? "Price nearing PDH" : "Price in previous day range"
tipPW = pwReclaimLow ? "Liquidity sweep - price swept PWL and reclaimed" : pwReclaimHigh ? "Liquidity sweep - price swept PWH and reclaimed" : pwSwingPct < 0 ? "Price below PWL - previous week low broken" : pwSwingPct > 100 ? "Price above PWH - previous week high broken" : pwSwingPct <= 30 ? "Price nearing PWL" : pwSwingPct >= 70 ? "Price nearing PWH" : "Price in previous week range"
tipPM = pmReclaimLow ? "Liquidity sweep - price swept PML and reclaimed" : pmReclaimHigh ? "Liquidity sweep - price swept PMH and reclaimed" : pmSwingPct < 0 ? "Price below PML - previous month low broken" : pmSwingPct > 100 ? "Price above PMH - previous month high broken" : pmSwingPct <= 30 ? "Price nearing PML" : pmSwingPct >= 70 ? "Price nearing PMH" : "Price in previous month range"

// Session and killzone detection (with replay mode support)
isReplay        = (timenow - time) > timeframe.in_seconds() * 1000 and barstate.islast
sessionTime     = isReplay ? time : timenow
currentHour     = hour(sessionTime,   "America/New_York")
currentMinute   = minute(sessionTime, "America/New_York")

[sessionName, sessionColor, sessionStartHour, sessionDuration] = getSession(currentHour)
[killzoneName, killzoneColor, nextKzText, nextKzColor]         = getKillzone(currentHour, currentMinute, neutColor)

// Session progress bar (adds bar at 30 min mark)
hoursElapsed       = 0
sessionProgressBar = ""
if sessionName != "OFF HOURS"
    minsIntoSession    = sessionName == "ASIAN" and currentHour < 3 ? ((currentHour + 24 - sessionStartHour) * 60 + currentMinute) : ((currentHour - sessionStartHour) * 60 + currentMinute)
    hoursElapsed       := math.floor((minsIntoSession + 30) / 60)
    hoursElapsed       := math.min(hoursElapsed, sessionDuration)
    sessionProgressBar := str.repeat("█", hoursElapsed) + str.repeat("░", math.max(0, sessionDuration - hoursElapsed))

// Trend Bias Calculation (User Weighted)
tfWeights    = array.from(tf1Weight, tf2Weight, tf3Weight, tf4Weight, tf5Weight, tf6Weight, tf7Weight)
tfActives    = array.from(tfActive(tf1Enabled, tf1),    tfActive(tf2Enabled, tf2),  tfActive(tf3Enabled, tf3),  tfActive(tf4Enabled, tf4),  tfActive(tf5Enabled, tf5),  tfActive(tf6Enabled, tf6),  tfActive(tf7Enabled, tf7))
structBiases = array.from(t1_structBias, t2_structBias, t3_structBias, t4_structBias, t5_structBias, t6_structBias, t7_structBias)
obDirs       = array.from(t1_obNearDir,  t2_obNearDir,  t3_obNearDir,  t4_obNearDir,  t5_obNearDir,  t6_obNearDir,  t7_obNearDir)
fvgDirs      = array.from(t1_fvgNearDir, t2_fvgNearDir, t3_fvgNearDir, t4_fvgNearDir, t5_fvgNearDir, t6_fvgNearDir, t7_fvgNearDir)
trendDirs    = array.from(t1_trendDir,   t2_trendDir,   t3_trendDir,   t4_trendDir,   t5_trendDir,   t6_trendDir,   t7_trendDir)
swingDirs    = array.from(swingBiasDir(t1_swingH, t1_swingL), swingBiasDir(t2_swingH, t2_swingL), swingBiasDir(t3_swingH, t3_swingL), swingBiasDir(t4_swingH, t4_swingL), swingBiasDir(t5_swingH, t5_swingL), swingBiasDir(t6_swingH, t6_swingL), swingBiasDir(t7_swingH, t7_swingL))

// Calculate max possible score and bias points
factorCount = biasFactorCount()
maxScore    = 0
biasPoints  = 0
for i = 0 to 6
    weight = tfWeights.get(i)
    if tfActives.get(i) and weight > 0
        maxScore   += weight * factorCount
        biasPoints += calcBiasForTf(structBiases.get(i), obDirs.get(i), fvgDirs.get(i), trendDirs.get(i), swingDirs.get(i), weight)

// Format bias display
biasPct     = maxScore > 0 ? (biasPoints / maxScore) * 100 : 0
biasLabel   = maxScore == 0 ? "N/A" : biasPct > 50 ? "BULLISH ↑" : biasPct > 20 ? "LEAN BULL ↑" : biasPct < -50 ? "BEARISH ↓" : biasPct < -20 ? "LEAN BEAR ↓" : "NEUTRAL →"
biasScore   = maxScore == 0 ? "--" : (biasPoints >= 0 ? "+" : "") + str.tostring(biasPoints) + "/" + str.tostring(maxScore)
biasColor   = maxScore == 0 ? neutColor : biasPct > 20 ? bullColor : biasPct < -20 ? bearColor : neutColor

// Dynamic tooltips
tipKillzone    = killzoneName == "NO KILLZONE" ? "No active killzone"                     : "Currently in " + killzoneName
tipNextKz      = killzoneName == "NO KILLZONE" ? "Next killzone: " + nextKzText           : "Currently in " + killzoneName
tipSession     = sessionName  == "OFF HOURS"   ? "Market is currently not in any session" : "Currently in " + sessionName + " session"
tipSessionProg = sessionName  == "OFF HOURS"   ? "Market is currently not in any session" : str.tostring(hoursElapsed) + " of " + str.tostring(sessionDuration) + " hours elapsed in " + sessionName + " session"
tipBiasLabel   = "Current Trend Bias: " + (biasPct > 50 ? "Bullish" : biasPct > 20 ? "Leaning Bullish" : biasPct < -50 ? "Bearish" : biasPct < -20 ? "Leaning Bearish" : "Neutral") + " based on HTF analysis"
tipVolume      = "Volume is currently at " + str.tostring(volumePct, "#") + "% of average"
tipVolatility  = "Volatility is currently " + (atrPct > 130 ? "high" : atrPct > 70 ? "normal" : "low")
tipCurrSwing   = currReclaimLow ? "Liquidity sweep - price swept swing low and reclaimed" : currReclaimHigh ? "Liquidity sweep - price swept swing high and reclaimed" : currSwingPct < 0 ? "Price below swing low - swing low broken" : currSwingPct > 100 ? "Price above swing high - swing high broken" : currSwingPct <= 30 ? "Price is nearing swing low" : currSwingPct >= 70 ? "Price is nearing swing high" : "Price is in mid-range"
//#endregion CALCULATIONS

//#region VISUALIZATION
var table dashboard = table.new(tablePos, 7, 18, 
    bgcolor      = bgDark, 
    border_color = #3d3d3d, 
    border_width = 1)

if barstate.islast
    // Market Structure Dashboard section
    if showMSD
        // Calculate total columns for MSD
        msdCols = 1 + (showColEma ? 1 : 0) + (showColSwing ? 1 : 0) + (showColStruct ? 1 : 0) + (showColOB ? 1 : 0) + (showColFVG ? 1 : 0)
        
        // Header row
        table.cell(dashboard, 0, 0, "MARKET STRUCTURE DASHBOARD", text_color = textCol, text_size = size.normal, bgcolor = bgHeader, text_halign = text.align_center)
        if msdCols > 1
            table.merge_cells(dashboard, 0, 0, msdCols - 1, 0)
        
        // Column headers (row 1) - EMA moved to last
        col = 0
        table.cell(dashboard, col, 1, "TF", text_color = textCol, text_size = size.normal, width = 3)
        if showColSwing
            col += 1
            table.cell(dashboard, col, 1, "SWING H/L",                                 text_color = textCol, text_size = size.normal, tooltip = "Price position within swing range")
        if showColStruct
            col += 1
            table.cell(dashboard, col, 1, "STRUCTURE",                                 text_color = textCol, text_size = size.normal, tooltip = "Last 3 swing patterns (HH/HL/LH/LL)")
        if showColOB
            col += 1
            table.cell(dashboard, col, 1, "ORDER BLOCK",                               text_color = textCol, text_size = size.normal, tooltip = "Nearest unmitigated order block")
        if showColFVG
            col += 1
            table.cell(dashboard, col, 1, "FVG",                                       text_color = textCol, text_size = size.normal, tooltip = "Nearest unmitigated fair value gap")
        if showColEma
            col += 1
            table.cell(dashboard, col, 1, "EMA-" + str.tostring(emaLength) + " TREND", text_color = textCol, text_size = size.normal, tooltip = "Distance from EMA as percentage")
        
        // TF Rows
        if tfActive(tf1Enabled, tf1)
            drawMsdRow(dashboard, 2, fmtTF(tf1), t1_trendDir, t1_trendDist, t1_swingBar, t1_swingColor, t1_swingPct, t1_reclaimLow, t1_reclaimHigh, t1_structBias, t1_struct1, t1_struct2, t1_struct3, t1_obNearDir, t1_obNearDist, t1_fvgNearDir, t1_fvgNearDist, tf1)
        if tfActive(tf2Enabled, tf2)
            drawMsdRow(dashboard, 3, fmtTF(tf2), t2_trendDir, t2_trendDist, t2_swingBar, t2_swingColor, t2_swingPct, t2_reclaimLow, t2_reclaimHigh, t2_structBias, t2_struct1, t2_struct2, t2_struct3, t2_obNearDir, t2_obNearDist, t2_fvgNearDir, t2_fvgNearDist, tf2)
        if tfActive(tf3Enabled, tf3)
            drawMsdRow(dashboard, 4, fmtTF(tf3), t3_trendDir, t3_trendDist, t3_swingBar, t3_swingColor, t3_swingPct, t3_reclaimLow, t3_reclaimHigh, t3_structBias, t3_struct1, t3_struct2, t3_struct3, t3_obNearDir, t3_obNearDist, t3_fvgNearDir, t3_fvgNearDist, tf3)
        if tfActive(tf4Enabled, tf4)
            drawMsdRow(dashboard, 5, fmtTF(tf4), t4_trendDir, t4_trendDist, t4_swingBar, t4_swingColor, t4_swingPct, t4_reclaimLow, t4_reclaimHigh, t4_structBias, t4_struct1, t4_struct2, t4_struct3, t4_obNearDir, t4_obNearDist, t4_fvgNearDir, t4_fvgNearDist, tf4)
        if tfActive(tf5Enabled, tf5)
            drawMsdRow(dashboard, 6, fmtTF(tf5), t5_trendDir, t5_trendDist, t5_swingBar, t5_swingColor, t5_swingPct, t5_reclaimLow, t5_reclaimHigh, t5_structBias, t5_struct1, t5_struct2, t5_struct3, t5_obNearDir, t5_obNearDist, t5_fvgNearDir, t5_fvgNearDist, tf5)
        if tfActive(tf6Enabled, tf6)
            drawMsdRow(dashboard, 7, fmtTF(tf6), t6_trendDir, t6_trendDist, t6_swingBar, t6_swingColor, t6_swingPct, t6_reclaimLow, t6_reclaimHigh, t6_structBias, t6_struct1, t6_struct2, t6_struct3, t6_obNearDir, t6_obNearDist, t6_fvgNearDir, t6_fvgNearDist, tf6)
        if tfActive(tf7Enabled, tf7)
            drawMsdRow(dashboard, 8, fmtTF(tf7), t7_trendDir, t7_trendDist, t7_swingBar, t7_swingColor, t7_swingPct, t7_reclaimLow, t7_reclaimHigh, t7_structBias, t7_struct1, t7_struct2, t7_struct3, t7_obNearDir, t7_obNearDist, t7_fvgNearDir, t7_fvgNearDist, tf7)
    
    // Current TF & Market Context sections
    if showCurrTF or showContext
        // Calculate column counts
        ctfCols    = (showCurrVol ? 1 : 0) + (showCurrSwing ? 1 : 0) + (showCurrAtr ? 1 : 0)
        ctxCols    = (showSession ? 1 : 0) + (showKillzone ? 1 : 0) + (showBias ? 1 : 0)
        bottomCols = (showCurrTF  ? ctfCols : 0) + (showContext ? ctxCols : 0)
        
        // Row 9: Empty separator (only show if MSD is also visible)
        if showMSD
            table.cell(dashboard, 0, 9, "", bgcolor = bgDark)
            if bottomCols > 0
                table.merge_cells(dashboard, 0, 9, bottomCols - 1, 9)
        
        // Row 10: Section headers (start at col 0)
        col = 0
        if showCurrTF and ctfCols > 0
            table.cell(dashboard, col, 10, "CURRENT TIMEFRAME", text_color = textCol, text_size = size.normal, bgcolor = bgHeader, text_halign = text.align_center)
            if ctfCols > 1
                table.merge_cells(dashboard, col, 10, col + ctfCols - 1, 10)
            col += ctfCols
        
        if showContext and ctxCols > 0
            table.cell(dashboard, col, 10, "MARKET CONTEXT",    text_color = textCol, text_size = size.normal, bgcolor = bgHeader, text_halign = text.align_center)
            if ctxCols > 1
                table.merge_cells(dashboard, col, 10, col + ctxCols - 1, 10)
        
        // Row 11-13: Column headers and data rows
        drawBottomHeaders(dashboard, 11)
        drawBottomData1(dashboard,   12, currSwingBarText, currSwingBarColor, tipCurrSwing, volumeBarString, volumeColor, tipVolume, volatilityState, volatilityColor, tipVolatility, sessionName, sessionColor, tipSession, killzoneName, killzoneColor, tipKillzone, biasLabel, biasColor, tipBiasLabel)
        drawBottomData2(dashboard,   13, currSwingBarColor, volumeState, volumeColor, sessionProgressBar, sessionColor, tipSessionProg, nextKzText, nextKzColor, tipNextKz, biasScore, biasColor)
    
    // HTF Levels section
    if showHTFLevels
        htfRows = (showPDHL ? 1 : 0) + (showPWHL ? 1 : 0) + (showPMHL ? 1 : 0)
        
        if htfRows > 0
            // Row 14: Section header
            table.cell(dashboard, 0, 14, "HTF LEVELS", text_color = textCol, text_size = size.normal, bgcolor = bgHeader, text_halign = text.align_center)
            table.merge_cells(dashboard, 0, 14, 1, 14)
            
            // Rows 15+: HTF data
            drawHTFRows(dashboard, 15, pdSwingBar, pdSwingColor, tipPD, pwSwingBar, pwSwingColor, tipPW, pmSwingBar, pmSwingColor, tipPM)

// Plot EMA
plot(showEMA ? emaValue : na, "EMA", color = emaColor, linewidth = 2)

// Draw HTF Level Lines
var line pdhLine = na, var line pdlLine = na
var line pwhLine = na, var line pwlLine = na
var line pmhLine = na, var line pmlLine = na
var label pdhLabel = na, var label pdlLabel = na
var label pwhLabel = na, var label pwlLabel = na
var label pmhLabel = na, var label pmlLabel = na

// Segment-based threshold for combining labels
htfMaxHigh  = math.max(plotPDHL ? pdHigh : 0, plotPWHL ? pwHigh : 0, plotPMHL ? pmHigh : 0)
htfMinLow   = math.min(plotPDHL ? pdLow : 10e10, plotPWHL ? pwLow : 10e10, plotPMHL ? pmLow : 10e10)
segmentSize = (htfMaxHigh - htfMinLow) / 50
getSegment(price) => segmentSize > 0 ? math.floor((price - htfMinLow) / segmentSize) : 0

// Get segments for all levels (-1 if disabled)
pdhSeg = plotPDHL ? getSegment(pdHigh) : -1, pdlSeg = plotPDHL ? getSegment(pdLow) : -1
pwhSeg = plotPWHL ? getSegment(pwHigh) : -2, pwlSeg = plotPWHL ? getSegment(pwLow) : -2
pmhSeg = plotPMHL ? getSegment(pmHigh) : -3, pmlSeg = plotPMHL ? getSegment(pmLow) : -3

// Build combined labels - highs
pdhInPwh = pdhSeg == pwhSeg and plotPDHL and plotPWHL
pdhInPmh = pdhSeg == pmhSeg and plotPDHL and plotPMHL
pwhInPmh = pwhSeg == pmhSeg and plotPWHL and plotPMHL

pmhLbl = (pdhInPmh ? "PDH/" : "") + (pwhInPmh ? "PWH/" : "") + "PMH"
pwhLbl = (pdhInPwh and not pdhInPmh ? "PDH/" : "") + "PWH"
pdhLbl = "PDH"

// Build combined labels - lows
pdlInPwl = pdlSeg == pwlSeg and plotPDHL and plotPWHL
pdlInPml = pdlSeg == pmlSeg and plotPDHL and plotPMHL
pwlInPml = pwlSeg == pmlSeg and plotPWHL and plotPMHL

pmlLbl = (pdlInPml ? "PDL/" : "") + (pwlInPml ? "PWL/" : "") + "PML"
pwlLbl = (pdlInPwl and not pdlInPml ? "PDL/" : "") + "PWL"
pdlLbl = "PDL"

if barstate.islast
    // Delete all existing
    pdhLine.delete(), pdlLine.delete(), pwhLine.delete(), pwlLine.delete(), pmhLine.delete(), pmlLine.delete()
    pdhLabel.delete(), pdlLabel.delete(), pwhLabel.delete(), pwlLabel.delete(), pmhLabel.delete(), pmlLabel.delete()
    
    // Draw all lines at their actual prices (no merging)
    if plotPMHL
        pmhLine := line.new(bar_index - 1, pmHigh, bar_index + htfLevelExtend, pmHigh, color = pmhColor, style = getLineStyle(pmhlLineStyle), width = 1)
        pmlLine := line.new(bar_index - 1, pmLow, bar_index + htfLevelExtend, pmLow, color = pmlColor, style = getLineStyle(pmhlLineStyle), width = 1)
    if plotPWHL
        pwhLine := line.new(bar_index - 1, pwHigh, bar_index + htfLevelExtend, pwHigh, color = pwhColor, style = getLineStyle(pwhlLineStyle), width = 1)
        pwlLine := line.new(bar_index - 1, pwLow, bar_index + htfLevelExtend, pwLow, color = pwlColor, style = getLineStyle(pwhlLineStyle), width = 1)
    if plotPDHL
        pdhLine := line.new(bar_index - 1, pdHigh, bar_index + htfLevelExtend, pdHigh, color = pdhColor, style = getLineStyle(pdhlLineStyle), width = 1)
        pdlLine := line.new(bar_index - 1, pdLow, bar_index + htfLevelExtend, pdLow, color = pdlColor, style = getLineStyle(pdhlLineStyle), width = 1)
    
    // Draw labels (combined when in same segment, highest TF gets the label)
    if plotPMHL
        pmhLabel := label.new(bar_index + htfLevelExtend, pmHigh, pmhLbl, color = color.new(pmhColor, 100), textcolor = pmhColor, style = label.style_label_left, size = size.small)
        pmlLabel := label.new(bar_index + htfLevelExtend, pmLow, pmlLbl, color = color.new(pmlColor, 100), textcolor = pmlColor, style = label.style_label_left, size = size.small)
    if plotPWHL and not pwhInPmh
        pwhLabel := label.new(bar_index + htfLevelExtend, pwHigh, pwhLbl, color = color.new(pwhColor, 100), textcolor = pwhColor, style = label.style_label_left, size = size.small)
    if plotPWHL and not pwlInPml
        pwlLabel := label.new(bar_index + htfLevelExtend, pwLow, pwlLbl, color = color.new(pwlColor, 100), textcolor = pwlColor, style = label.style_label_left, size = size.small)
    if plotPDHL and not pdhInPmh and not pdhInPwh
        pdhLabel := label.new(bar_index + htfLevelExtend, pdHigh, pdhLbl, color = color.new(pdhColor, 100), textcolor = pdhColor, style = label.style_label_left, size = size.small)
    if plotPDHL and not pdlInPml and not pdlInPwl
        pdlLabel := label.new(bar_index + htfLevelExtend, pdLow, pdlLbl, color = color.new(pdlColor, 100), textcolor = pdlColor, style = label.style_label_left, size = size.small)

// Draw Order Blocks
if showOB and barstate.islast
    for b in obBoxes
        b.delete()
    obBoxes.clear()
    
    obCount = currTfOBs.size()
    for i = 0 to obCount - 1
        ob       = currTfOBs.get(i)
        obColor  = ob.dir > 0 ? color.new(obBullColor, 80) : color.new(obBearColor, 80)
        obBorder = ob.dir > 0 ? obBullColor : obBearColor
        
        newBox = box.new(
            left         = ob.barIdx,
            top          = ob.top,
            right        = bar_index + obExtend,
            bottom       = ob.bottom,
            border_color = obBorder,
            border_width = 1,
            bgcolor      = obColor,
            text         = showOBLabels ? (ob.dir > 0 ? "OB ↑" : "OB ↓") : "",
            text_color   = obBorder,
            text_halign  = text.align_right,
            text_valign  = text.align_center,
            text_size    = size.small
            )
        obBoxes.unshift(newBox)

// Draw FVGs
if showFVG and barstate.islast
    for b in fvgBoxes
        b.delete()
    fvgBoxes.clear()
    
    fvgCount = currTfFvgs.size()
    for i = 0 to fvgCount - 1
        fvg       = currTfFvgs.get(i)
        fvgColorBg  = fvg.dir > 0 ? color.new(fvgBullColor, 85) : color.new(fvgBearColor, 85)
        fvgBorderClr = fvg.dir > 0 ? color.new(fvgBullColor, 60) : color.new(fvgBearColor, 60)
        
        fvgTextClr = fvg.dir > 0 ? fvgBullColor : fvgBearColor
        
        newBox = box.new(
            left         = fvg.barIdx,
            top          = fvg.top,
            right        = bar_index + fvgExtend,
            bottom       = fvg.bottom,
            bgcolor      = fvgColorBg,
            border_width = 1,
            border_style = line.style_dashed,
            border_color = fvgBorderClr,
            text         = showFVGLabels ? (fvg.dir > 0 ? "FVG ↑" : "FVG ↓") : "",
            text_color   = fvgTextClr,
            text_halign  = text.align_right,
            text_valign  = text.align_center,
            text_size    = size.small
            )
        fvgBoxes.unshift(newBox)

// Draw Swing Labels (from array on last bar)
var array<label> swingLabelObjs = array.new<label>()

if showSwingLabels and barstate.islast
    // Clear old labels
    for lbl in swingLabelObjs
        lbl.delete()
    swingLabelObjs.clear()
    
    // Draw labels from array
    for lbl in swingLabels
        newLabel = label.new(
            x         = lbl.x,
            y         = lbl.y,
            text      = lbl.txt,
            yloc      = lbl.isHigh ? yloc.abovebar          : yloc.belowbar,
            color     = lbl.clr,
            style     = lbl.isHigh ? label.style_label_down : label.style_label_up,
            textcolor = lbl.textClr,
            size      = size.small
            )
        swingLabelObjs.push(newLabel)

// Draw Swing Lines
if showSwingLines and barstate.islast
    swingLines.highLine.delete()
    swingLines.lowLine.delete()
    
    if not na(currSwingH) and not na(swingLines.lastHBar)
        swingLines.highLine := line.new(
            x1    = swingLines.lastHBar,
            y1    = currSwingH,
            x2    = bar_index + swingLinesExtend,
            y2    = currSwingH,
            color = color.new(swingLineHighColor, 30),
            style = getLineStyle(swingLineStyle),
            width = 1
            )
    
    if not na(currSwingL) and not na(swingLines.lastLBar)
        swingLines.lowLine := line.new(
            x1    = swingLines.lastLBar,
            y1    = currSwingL,
            x2    = bar_index + swingLinesExtend,
            y2    = currSwingL,
            color = color.new(swingLineLowColor, 30),
            style = getLineStyle(swingLineStyle),
            width = 1
            )
//#endregion VISUALIZATION

Overview

Market Structure Dashboard | Flux Charts reads up to seven user-selected timeframes, each with an on/off switch and a trend weight, and shows columns for EMA trend, swing highs and lows, structure, order blocks, and fair value gaps. A current-timeframe panel can display swing levels and volume. Settings groups also cover market context, higher-timeframe levels, trend bias, and visual overlays. It does not place trades.

Details

Published by

eddiebelaval on GitHub

Code credit

fluxchart, as written in the script's header

Type

Indicator, Pine Script v6

Draws

On the price chart

Category

Screeners & dashboards, SMC / ICT / price action, Trend, Sessions & time

Markets

Any symbol

Timeframes

Any timeframe

Risk Notes

No repainting, lookahead or stacked entries were found in its code or README. That says nothing about how well it works: test it first.

Settings

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

SettingTypeDefaultVariable
tf1Enabledbooltruetf1Enabled
tf1timeframe1tf1
Trend Weightint1tf1Weight
tf2Enabledbooltruetf2Enabled
tf2timeframe5tf2
Trend Weightint1tf2Weight
tf3Enabledbooltruetf3Enabled
tf3timeframe15tf3
Trend Weightint2tf3Weight
tf4Enabledbooltruetf4Enabled
tf4timeframe60tf4
Trend Weightint2tf4Weight
Show all 40 settings
SettingTypeDefaultVariable
tf5Enabledbooltruetf5Enabled
tf5timeframe240tf5
Trend Weightint2tf5Weight
tf6Enabledbooltruetf6Enabled
tf6timeframeDtf6
Trend Weightint3tf6Weight
tf7Enabledbooltruetf7Enabled
tf7timeframeWtf7
Trend Weightint4tf7Weight
EnablebooltrueshowMSD
EMA TrendbooltrueshowColEma
Swing H/LbooltrueshowColSwing
StructurebooltrueshowColStruct
Order BlockbooltrueshowColOB
FVGbooltrueshowColFVG
EnablebooltrueshowCurrTF
Swing H/LbooltrueshowCurrSwing
VolumebooltrueshowCurrVol
VolatilitybooltrueshowCurrAtr
EnablebooltrueshowContext
SessionbooltrueshowSession
KillzonebooltrueshowKillzone
Trend BiasbooltrueshowBias
EnableboolfalseshowHTFLevels
PDH/LbooltrueshowPDHL
PWH/LbooltrueshowPWHL
PMH/LboolfalseshowPMHL
StructurebooltruebiasUseStruct
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

1 stars

This overlay indicator maps smart-money market structure on the price chart. It labels swing points as higher highs, higher lows, lower highs, and lower lows, and draws Break of Structure and Change of Character lines when structure breaks. A multi-timeframe t

Indicator v6 SMC / ICT / price actionScreeners & dashboards
Updated 4 months ago · None stated GitHub repository of SMC Market Structure + MTF
257 stars

This indicator overlays an intraday SPX and ES chart with a midnight session range, regular-trading-hours opening levels, and a VWAP projection box. It draws the midnight high, mid, and low, flags edge rejections within an absolute tolerance, and plots optiona

Indicator v6 TrendSupport & resistance
Updated 6 weeks ago · None stated GitHub repository of SPX Midnight Box + Snag Logic

Golden Bullet Strategy

by TradersPost
218 stars

The Golden Bullet Strategy is an overlay signal indicator based on Smart Money Concepts and time-based liquidity models. It identifies liquidity sweeps of a designated reference session range during a specified hunt window. It then confirms Market Structure Sh

Indicator v6 SMC / ICT / price actionSessions & time
Updated 3 months ago · None stated GitHub repository of Golden Bullet Strategy
92 stars

This indicator classifies the market into gamma regime types (negative gamma trend, positive gamma mean-reversion, or transition) by calculating a composite score based on daily ATR ratios, session VWAP crossover frequencies, and opening range displacement. It

Indicator v5 Volatility & bandsTrend
Updated 8 weeks ago · Custom licence GitHub repository of Moon Dev GEX Regime & Trap
53 stars

Apex Trend & Liquidity Master calculates a trend cloud using customizable moving averages expanded by a volatility multiplier directly on the main price chart. It identifies dynamic support and resistance liquidity zones from swing high and low pivots, extendi

Indicator v6 TrendSupport & resistance
Updated 2 months ago · MPL-2.0 GitHub repository of Apex Trend & Liquidity Master

Confluence Suite

by TamaraKGreen
46 stars

The Confluence Suite indicator draws multiple trading signals and tools directly on the price chart. It generates entry and exit signals based on selected modes and filters, with an AI signal classifier rating quality from 1-4. Additional features include smar

Indicator v5 TrendSMC / ICT / price action
Updated 7 weeks ago · None stated GitHub repository of Confluence Suite

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