PropValue

Pine Script v6 · v1.0

MNQ Toolkit

Six intraday tools in one overlay: 5m trend bias with strength grading and position sizing, key levels, anchored VWAP, volume profile, S&R channel and bias-filtered fair value gaps.

MNQ Toolkit on a 30-minute MNQ chart: trend shading, key levels, volume profile, the support and resistance channel, fair value gaps and the sizing table
MNQ, 30-minute chart, with every module switched on.Chart by TradingView

Install

  1. Open TradingView on a 1-minute or 5-minute MNQ chart (NQ, MES and ES work too), then open the Pine Editor at the bottom.
  2. Paste the script, press Save, then Add to chart.
  3. In the settings, set "Risk per trade" and "Stop distance" under module 1, then switch off any modules you don't use in the "▶ Modules" group.
  4. Alerts for trend changes, key-level crosses, VWAP crosses and fair value gaps are available in the Alerts dialog.

Check sizes by hand with the position sizer.

What it does

Trend + Sizing: a 5-minute EMA 21/55 bias with an optional EMA 200 filter shades the chart long or short. A table shows the stop distance in points and ticks, the risk per contract and the number of contracts that keeps a stop-out within your risk.

Trend Strength grades the bias STRONG or TENTATIVE. A trend earns STRONG after a confirmed pullback: price clears the EMA, comes back to tag it, then makes a new extreme. A pullback that closes through the EMA 55 resets the count.

Key Levels draws previous day, week and month highs, lows and closes, period opens, the overnight (Globex) range and the opening range. VWAP anchors at 18:00 New York by default, matching the futures trading day, with σ bands.

Fair Value Gaps are found on the chart or a higher timeframe and kept only when they agree with the trend bias. The most recent gap in the bias direction is outlined as the active one.

The S&R Power Channel module is adapted from ChartPrime's "Support and Resistance Power Channel" and the Volume Profile module from kv4coins' "Volume Profile", both under the Mozilla Public License 2.0. Those portions remain under MPL-2.0.

Script

// ═══════════════════════════════════════════════════════════════════════════════════════════════
// MNQ Toolkit · v1.0 · October 2026
// by David J Kaplan · https://propvalue.co/indicators/mnq-toolkit/
// ═══════════════════════════════════════════════════════════════════════════════════════════════
// One overlay for intraday index-futures trading (built on MNQ; works on NQ, MES, ES and others).
// Six modules, each switchable from the "▶ Modules" group at the top of the settings:
//
//   1  Trend + Sizing      5m EMA 21/55 bias with an optional EMA 200 macro filter, a 1m EMA 20
//                          pullback guide, stop-distance bands and a position-size table.
//   1b Trend Strength      Grades the bias STRONG or TENTATIVE from confirmed pullbacks
//                          (clear of the EMA, tag it, then a new trend extreme) and, optionally,
//                          how far apart the 21 and 55 are in 5m ATR units.
//   2  S&R Power Channel   Lookback high/low channel with ATR-width zones, buy/sell power counts
//                          and rejection markers.
//   3  Volume Profile      Fixed-range or visible-range profile with Point of Control and Value Area.
//   4  Key Levels          Previous day/week/month highs, lows and closes, period opens, overnight
//                          (Globex) high/low and opening range, with cross alerts.
//   5  VWAP                Anchored VWAP (custom clock time, session, RTH, week or month) with
//                          σ bands and an optional weekly VWAP.
//   6  Fair Value Gaps     Three-bar gaps from the chart or a higher timeframe, filtered by the
//                          module 1 bias, with forming and confirmed states.
//
// Quick start: add to a 1-minute or 5-minute MNQ chart, set "Risk per trade" and "Stop distance" in
// module 1, then switch off the modules you don't use.
//
// Credits and licences
//   Module 2 is adapted from "Support and Resistance Power Channel" © ChartPrime, and module 3 from
//   "Volume Profile" © kv4coins. Both are published under the Mozilla Public License 2.0; those
//   portions remain under MPL-2.0 (https://mozilla.org/MPL/2.0/).
//   All other modules are original work by David J Kaplan.
//
// Free to use and modify. Not financial advice: test on a simulated account before trading live.
// ═══════════════════════════════════════════════════════════════════════════════════════════════

//@version=6
indicator("MNQ Toolkit (Trend/Size + S&R + VP + Key Levels + FVG)", "MNQ Toolkit", overlay = true,
     max_bars_back = 5000, max_lines_count = 500, max_labels_count = 500, max_boxes_count = 500)

// ═══════════════════════════════════════════════════════════════════════════════════════════════
// ▶ MODULES  (master switches)
// ═══════════════════════════════════════════════════════════════════════════════════════════════
G_MOD = "▶ Modules"
mnq_enable = input.bool(true,  "MNQ Trend + Sizing",           group = G_MOD, tooltip = "5m EMA 21/55 bias, 5m EMA 200 macro filter, 1m EMA 20 pullback guide, stop-distance bands and position-size table.")
sr_enable  = input.bool(true,  "S&R Power Channel",            group = G_MOD, tooltip = "ChartPrime lookback high/low channel with ATR bands, buy/sell power and rejection signals.")
vp_enable  = input.bool(true,  "Volume Profile",               group = G_MOD, tooltip = "kv4coins fixed-range / visible-range volume profile with Point of Control and Value Area.")
kl_enable  = input.bool(true,  "Key Levels",                   group = G_MOD, tooltip = "Previous day / week / month levels, period opens, overnight session range and opening range.")
vw_enable  = input.bool(true,  "VWAP",                         group = G_MOD, tooltip = "Anchored VWAP (custom time / session / RTH / week / month) with standard-deviation bands and optional weekly VWAP.")
fvg_enable = input.bool(true,  "Fair Value Gaps",              group = G_MOD, tooltip = "FVG boxes (optionally from a higher timeframe), filtered by the 5m bias.")

// ═══════════════════════════════════════════════════════════════════════════════════════════════
// 1 · MNQ TREND + SIZING — inputs
// ═══════════════════════════════════════════════════════════════════════════════════════════════
G_MNQ = "1 · MNQ Trend + Sizing"
riskUsd        = input.float(300, "Risk per trade ($)", step = 10, minval = 0, group = G_MNQ,
                   tooltip = "The most you want to lose if the stop is hit. The table sizes the position to stay within it.")
distUnit       = input.string("Points", "Stop distance unit", options = ["Points", "Ticks"], group = G_MNQ,
                   tooltip = "TradingView's Long/Short position tool uses TICKS. MNQ tick = 0.25 pt.")
distIn         = input.float(50, "Stop distance", step = 1, minval = 0.25, group = G_MNQ,
                   tooltip = "Distance from entry to stop, in the unit chosen above. Drawn as the green/red bands.")
anchorMode     = input.string("Current price", "Anchor", options = ["Current price", "Previous bar close", "Manual price"], group = G_MNQ,
                   tooltip = "Where the stop-distance bands are centred: the live price, the last closed bar, or a price you type in.")
manualAnchor   = input.float(0.0, "Manual anchor price (0 = unused)", step = 0.25, group = G_MNQ)
pointValueOvr  = input.float(0.0, "Point value override ($/pt, 0 = auto)", step = 0.5, group = G_MNQ,
                   tooltip = "Leave at 0 to use syminfo.pointvalue. MNQ should be 2, NQ 20, MES 5, ES 50.")
useMacro200    = input.bool(true, "Use 5m EMA200 macro filter", group = G_MNQ)
mnq_showEmas   = input.bool(true, "Show EMAs",                   group = G_MNQ)
mnq_showBg     = input.bool(true, "Shade background by bias",    group = G_MNQ)
mnq_showBands  = input.bool(true, "Show stop-distance bands",    group = G_MNQ)
extendRight    = input.bool(true, "Extend bands right",          group = G_MNQ)
showTable      = input.bool(true, "Show sizing table",           group = G_MNQ)
tablePos       = input.string("Top right", "Table position", options = ["Top right", "Top left", "Bottom right", "Bottom left", "Middle right"], group = G_MNQ)

// ═══════════════════════════════════════════════════════════════════════════════════════════════
// 1b · TREND STRENGTH — inputs
// ═══════════════════════════════════════════════════════════════════════════════════════════════
G_TS = "1b · Trend Strength"
ts_enable    = input.bool(true, "Classify trend as STRONG / TENTATIVE", group = G_TS)
ts_pbSrc     = input.string("5m EMA 21", "Pullback EMA", options = ["5m EMA 21", "1m EMA 20"], group = G_TS,
                 tooltip = "The average a pullback has to tag. 5m EMA 21 gives fewer, more meaningful pullbacks. 1m EMA 20 confirms much faster.")
ts_pbNeeded  = input.int(1, "Confirmed pullbacks needed", minval = 0, maxval = 5, group = G_TS,
                 tooltip = "A confirmed pullback = price trades fully clear of the EMA, comes back to tag it, then makes a new high (long) / new low (short) beyond the pre-pullback extreme. 0 = structure not required.")
ts_tolTicks  = input.int(4, "Touch tolerance (ticks)", minval = 0, group = G_TS,
                 tooltip = "How close to the EMA counts as a tag. Also the clearance needed before a pullback can start.")
ts_resetDeep = input.bool(true, "Reset count if a pullback closes through 5m EMA 55", group = G_TS)
ts_useSep    = input.bool(true, "Also require 21/55 separation", inline = "tssep", group = G_TS)
ts_sepMin    = input.float(0.5, "Min (× 5m ATR)", minval = 0, step = 0.05, inline = "tssep", group = G_TS)
ts_atrLen    = input.int(14, "ATR length (5m bars)", minval = 1, group = G_TS)
ts_showMarks = input.bool(true, "Mark confirmed pullbacks (PB)", group = G_TS)

// ═══════════════════════════════════════════════════════════════════════════════════════════════
// 2 · S&R POWER CHANNEL — inputs
// ═══════════════════════════════════════════════════════════════════════════════════════════════
G_SR = "2 · S&R Power Channel"
int   sr_length = input.int(130, "Lookback length", minval = 10, maxval = 490, group = G_SR)
int   sr_extend = input.int(30,  "Extend (bars)", minval = 20, maxval = 100, group = G_SR)
color sr_t_col  = input.color(color.fuchsia, "Top",    inline = "srcol", group = G_SR)
color sr_b_col  = input.color(color.lime,    "Bottom", inline = "srcol", group = G_SR)
bool  sr_showSignals = input.bool(true, "Show rejection signals (◈)", group = G_SR)
bool  sr_showPower   = input.bool(true, "Show buy/sell power text",  group = G_SR)

// ═══════════════════════════════════════════════════════════════════════════════════════════════
// 3 · VOLUME PROFILE — inputs
// ═══════════════════════════════════════════════════════════════════════════════════════════════
G_VP1 = "3 · Volume Profile · Lookback"
G_VP2 = "3 · Volume Profile · Bars"
G_VP3 = "3 · Volume Profile · PoC / Value Area"
vp_use_visible_range  = input.bool(false, "Use Visible Range", group = G_VP1, tooltip = "Calculate Volume Profile using all visible bars in the current chart")
vp_lookback_depth     = input.int(200, "Fixed Range Lookback Depth [10-3000]", minval = 10, maxval = 3000, group = G_VP1, tooltip = "Number of historical bars to include. Ignored if \"Use Visible Range\" is enabled")
vp_num_bars           = input.int(200, "Number of Volume Bars [10-400]", minval = 10, maxval = 400, group = G_VP2)
vp_bar_thickness      = input.int(1,   "Volume Bar Thickness [1-30]", minval = 1, maxval = 30, group = G_VP2)
vp_bar_len_mult       = input.int(20,  "Bar Length Multiplier [1-50]", minval = 1, maxval = 50, group = G_VP2)
vp_right_offset       = input.int(70,  "Right Offset [0-400]", minval = 0, maxval = 400, group = G_VP2)
vp_volume_type        = input.string("Both", "Volume Type", options = ["Both", "Bullish", "Bearish"], group = G_VP2)
vp_bar_color          = input.color(color.new(color.gray, 50), "Volume Bar Color", group = G_VP2)
vp_display_poc        = input.bool(true, "Display Point of Control (PoC)", group = G_VP3)
vp_poc_line_thickness = input.int(1, "PoC Line Thickness [1-30]", minval = 1, maxval = 30, group = G_VP3)
vp_poc_line_color     = input.color(color.red, "PoC Line Color", group = G_VP3)
vp_display_va         = input.bool(true, "Display Value Area", group = G_VP3)
vp_va_percent         = input.int(68, "Value Area Percentage(%) [5-95]", minval = 5, maxval = 95, group = G_VP3)
vp_va_bar_color       = input.color(color.blue, "Value Area Bar Color", group = G_VP3)
vp_display_va_lines   = input.bool(true, "Display Value Area Lines", group = G_VP3, tooltip = "Ignored if \"Display Value Area\" is disabled")
vp_va_lines_thickness = input.int(1, "Value Area Lines Thickness [1-20]", minval = 1, maxval = 20, group = G_VP3)

// ═══════════════════════════════════════════════════════════════════════════════════════════════
// 4 · KEY LEVELS — inputs
// ═══════════════════════════════════════════════════════════════════════════════════════════════
G_KL1 = "4 · Key Levels · Daily"
G_KL2 = "4 · Key Levels · Weekly / Monthly"
G_KL3 = "4 · Key Levels · Sessions"
G_KL4 = "4 · Key Levels · Style"

kl_pdh  = input.bool(true,  "Prev day high (PDH)",   inline = "pd1", group = G_KL1)
kl_pdl  = input.bool(true,  "Prev day low (PDL)",    inline = "pd1", group = G_KL1)
kl_pdc  = input.bool(true,  "Prev day close (PDC)",  inline = "pd2", group = G_KL1)
kl_pdm  = input.bool(false, "Prev day mid (PDM)",    inline = "pd2", group = G_KL1)
kl_cdo  = input.bool(true,  "Today's open (DO)",     inline = "pd3", group = G_KL1)
kl_cdh  = input.bool(false, "Today's high/low (HOD/LOD)", inline = "pd3", group = G_KL1)
kl_pdCol = input.color(color.new(color.yellow, 0), "Daily colour", group = G_KL1)

kl_pwh  = input.bool(true,  "Prev week high (PWH)",  inline = "pw1", group = G_KL2)
kl_pwl  = input.bool(true,  "Prev week low (PWL)",   inline = "pw1", group = G_KL2)
kl_pwc  = input.bool(false, "Prev week close (PWC)", inline = "pw2", group = G_KL2)
kl_cwo  = input.bool(true,  "Week open (WO)",        inline = "pw2", group = G_KL2)
kl_pwCol = input.color(color.new(color.aqua, 0), "Weekly colour", group = G_KL2)
kl_pmh  = input.bool(false, "Prev month high (PMH)", inline = "pm1", group = G_KL2)
kl_pml  = input.bool(false, "Prev month low (PML)",  inline = "pm1", group = G_KL2)
kl_cmo  = input.bool(false, "Month open (MO)",       inline = "pm2", group = G_KL2)
kl_pmCol = input.color(color.new(color.purple, 0), "Monthly colour", group = G_KL2)

kl_on    = input.bool(true, "Overnight session high/low (ONH/ONL)", group = G_KL3)
kl_onSess = input.session("1800-0930", "Overnight session", group = G_KL3, tooltip = "Globex overnight for CME index futures is 18:00–09:30 New York. For stocks use pre-market e.g. 0400-0930.")
kl_onCol = input.color(color.new(color.orange, 0), "Overnight colour", group = G_KL3)
kl_or    = input.bool(true, "Opening range high/low (ORH/ORL)", group = G_KL3)
kl_orMid = input.bool(false, "Opening range midpoint (ORM)", group = G_KL3)
kl_orSess = input.session("0930-0945", "Opening range session", group = G_KL3, tooltip = "First N minutes of the regular session, e.g. 0930-0935, 0930-0945, 0930-1000.")
kl_orCol = input.color(color.new(color.teal, 0), "Opening range colour", group = G_KL3)
kl_tz    = input.string("America/New_York", "Session timezone", group = G_KL3)

kl_style    = input.string("Solid", "Line style", options = ["Solid", "Dashed", "Dotted"], group = G_KL4)
kl_width    = input.int(1, "Line width", minval = 1, maxval = 4, group = G_KL4)
kl_fromStart = input.bool(true, "Start line at period/session start", group = G_KL4, tooltip = "Off = start the line a fixed number of bars back.")
kl_maxBars  = input.int(300, "Max line length (bars)", minval = 10, maxval = 4000, group = G_KL4)
kl_extend   = input.bool(false, "Extend lines right (infinite)", group = G_KL4)
kl_labelOff = input.int(15, "Label offset (bars right of price)", minval = 1, maxval = 200, group = G_KL4)
kl_labelSize = input.string("small", "Label size", options = ["tiny", "small", "normal", "large"], group = G_KL4)
kl_showPrice = input.bool(true, "Show price in label", group = G_KL4)
kl_alerts   = input.bool(true, "Enable key-level cross alerts", group = G_KL4, tooltip = "Creates alert conditions for close crossing PDH / PDL / PDC / ONH / ONL / ORH / ORL. Add via the Alerts dialog.")

// ═══════════════════════════════════════════════════════════════════════════════════════════════
// 5 · VWAP — inputs
// ═══════════════════════════════════════════════════════════════════════════════════════════════
G_VW = "5 · VWAP"
vw_anchor  = input.string("Custom time", "Anchor", options = ["Session (daily)", "Custom time", "RTH session", "Week", "Month"], group = G_VW,
               tooltip = "Custom time resets VWAP at the clock time below, in the Key Levels session timezone (default 18:00 New York = Globex open). Session (daily) resets at the daily bar boundary. RTH session resets at the start of the RTH session below.")
vw_anchorH = input.int(18, "Custom anchor time  H", minval = 0, maxval = 23, inline = "vwt", group = G_VW)
vw_anchorM = input.int(0,  "M",                     minval = 0, maxval = 59, inline = "vwt", group = G_VW)
vw_rthSess = input.session("0930-1600", "RTH session (for RTH anchor)", group = G_VW)
vw_src     = input.source(hlc3, "Source", group = G_VW)
vw_col     = input.color(color.new(color.white, 0), "VWAP colour", inline = "vwl", group = G_VW)
vw_width   = input.int(2, "Width", minval = 1, maxval = 4, inline = "vwl", group = G_VW)
vw_b1      = input.bool(true,  "Band 1", inline = "vwb1", group = G_VW)
vw_m1      = input.float(1.0, "×σ", minval = 0.1, step = 0.1, inline = "vwb1", group = G_VW)
vw_b2      = input.bool(true,  "Band 2", inline = "vwb2", group = G_VW)
vw_m2      = input.float(2.0, "×σ", minval = 0.1, step = 0.1, inline = "vwb2", group = G_VW)
vw_b3      = input.bool(false, "Band 3", inline = "vwb3", group = G_VW)
vw_m3      = input.float(3.0, "×σ", minval = 0.1, step = 0.1, inline = "vwb3", group = G_VW)
vw_bandCol = input.color(color.new(color.silver, 0), "Band colour", inline = "vwbc", group = G_VW)
vw_fill    = input.bool(true, "Fill bands", inline = "vwbc", group = G_VW)
vw_showW   = input.bool(false, "Also show weekly VWAP", inline = "vww", group = G_VW)
vw_wCol    = input.color(color.new(color.fuchsia, 0), "", inline = "vww", group = G_VW)
vw_alerts  = input.bool(true, "Enable VWAP cross alert", group = G_VW)

// ═══════════════════════════════════════════════════════════════════════════════════════════════
// SHARED HELPERS
// ═══════════════════════════════════════════════════════════════════════════════════════════════
roundToTick(float x) =>
    math.round(x / syminfo.mintick) * syminfo.mintick

// Global-scope ta.* calls (kept out of conditional blocks so their series stay consistent).
// "5m" EMAs: on charts below 5 minutes the length is scaled up (e.g. a 21-period 5m EMA becomes a
// 105-period EMA on a 1m chart). This tracks the true 5m EMA closely, updates every chart bar and
// avoids request.security repaint issues. On 5m and higher charts the plain lengths are used.
tfRatio   = math.max(1, math.round(300 / timeframe.in_seconds()))
ema21_5m  = ta.ema(close, 21  * tfRatio)
ema55_5m  = ta.ema(close, 55  * tfRatio)
ema200_5m = ta.ema(close, 200 * tfRatio)
pullbackEma_1m = ta.ema(close, 20)
srAtrHalf      = ta.atr(200) * 0.5

// 5m ATR for the trend-strength separation check (uses the chart timeframe if the chart is above 5m)
ts_atrTf = timeframe.in_seconds() <= 300 ? "5" : timeframe.period
atr5m    = request.security(syminfo.tickerid, ts_atrTf, ta.atr(ts_atrLen))

// ═══════════════════════════════════════════════════════════════════════════════════════════════
// 1 · MNQ TREND + SIZING — logic
// ═══════════════════════════════════════════════════════════════════════════════════════════════
// Bias: LONG when the 5m EMA 21 is above the 55 (and price is above the EMA 200 if the macro filter
// is on); SHORT for the reverse; neutral otherwise. The background shading and the table follow it.
// Sizing: contracts = floor(risk per trade ÷ (stop distance in points × point value)).
bullBias = ema21_5m > ema55_5m
bearBias = ema21_5m < ema55_5m
macroBullOk = not useMacro200 or close > ema200_5m
macroBearOk = not useMacro200 or close < ema200_5m
trendUp   = mnq_enable and bullBias and macroBullOk
trendDown = mnq_enable and bearBias and macroBearOk

// ── 1b · Trend strength
// A small state machine per trend leg:
//   armed  → price has traded fully clear of the pullback EMA (by the touch tolerance)
//   inPb   → price came back and tagged the EMA
//   confirm→ price then made a new high (long) / low (short) beyond the pre-pullback extreme
// Each confirmation adds one to the count. A pullback that closes through the 5m EMA 55 is "deep"
// and resets the count. STRONG = enough confirmed pullbacks (and enough 21/55 separation, if used).
// Direction uses the same rule as the table bias (21/55 + optional EMA200), independent of the module switch.
trendDir = bullBias and macroBullOk ? 1 : bearBias and macroBearOk ? -1 : 0
dirFlip  = trendDir != nz(trendDir[1])
pbEma    = ts_pbSrc == "1m EMA 20" ? pullbackEma_1m : ema21_5m
pbTol    = ts_tolTicks * syminfo.mintick

var int   ts_pbCount = 0      // confirmed pullbacks in the current trend
var bool  ts_armed   = false  // price has traded fully clear of the EMA since the last pullback
var bool  ts_inPb    = false  // EMA tagged, waiting for a new trend extreme
var bool  ts_deep    = false  // the current pullback closed through the 5m EMA 55
var float ts_extreme = na     // trend high (long) / low (short) before the current pullback
bool ts_pbConfirmed  = false  // true on the bar a pullback is confirmed (for markers)

if dirFlip
    ts_pbCount := 0
    ts_armed   := false
    ts_inPb    := false
    ts_deep    := false
    ts_extreme := trendDir == 1 ? high : trendDir == -1 ? low : na

if trendDir == 1
    // resumption: new high beyond the pre-pullback high
    if ts_inPb and high > ts_extreme
        ts_pbConfirmed := not ts_deep
        ts_pbCount     := ts_deep ? 0 : ts_pbCount + 1
        ts_inPb        := false
        ts_deep        := false
        ts_armed       := false
    if not ts_inPb
        ts_extreme := math.max(nz(ts_extreme, high), high)
    if not ts_armed and not ts_inPb and low > pbEma + pbTol
        ts_armed := true
    if ts_armed and not ts_inPb and low <= pbEma + pbTol
        ts_inPb  := true
        ts_armed := false
    if ts_inPb and ts_resetDeep and close < ema55_5m
        ts_deep := true
else if trendDir == -1
    // resumption: new low beyond the pre-pullback low
    if ts_inPb and low < ts_extreme
        ts_pbConfirmed := not ts_deep
        ts_pbCount     := ts_deep ? 0 : ts_pbCount + 1
        ts_inPb        := false
        ts_deep        := false
        ts_armed       := false
    if not ts_inPb
        ts_extreme := math.min(nz(ts_extreme, low), low)
    if not ts_armed and not ts_inPb and high < pbEma - pbTol
        ts_armed := true
    if ts_armed and not ts_inPb and high >= pbEma - pbTol
        ts_inPb  := true
        ts_armed := false
    if ts_inPb and ts_resetDeep and close > ema55_5m
        ts_deep := true

ts_sep      = atr5m > 0 ? math.abs(ema21_5m - ema55_5m) / atr5m : 0.0
ts_structOk = ts_pbCount >= ts_pbNeeded
ts_sepOk    = not ts_useSep or ts_sep >= ts_sepMin
ts_strong   = ts_enable and trendDir != 0 and ts_structOk and ts_sepOk
strongLong  = ts_strong and trendDir == 1
strongShort = ts_strong and trendDir == -1
string strengthTxt = not ts_enable or trendDir == 0 ? "" : ts_strong ? "STRONG" : "TENTATIVE"

string side = trendUp ? "LONG" : trendDown ? "SHORT" : "—"
string sideTxt = side + (side != "—" and strengthTxt != "" ? " · " + strengthTxt : "")

mnq_plotEmas = mnq_enable and mnq_showEmas
plot(mnq_plotEmas ? ema21_5m : na,  color = color.orange, linewidth = 2, title = "5m EMA 21")
plot(mnq_plotEmas ? ema55_5m : na,  color = color.blue,   linewidth = 2, title = "5m EMA 55")
plot(mnq_plotEmas and useMacro200 ? ema200_5m : na, color = color.gray, linewidth = 2, title = "5m EMA 200")
plot(mnq_plotEmas ? pullbackEma_1m : na, color = color.white, linewidth = 1, title = "1m EMA 20 (pullback guide)")

// Strong = deeper shade, tentative = faint shade
bgTransp = not ts_enable ? 92 : ts_strong ? 86 : 96
bgcolor(mnq_showBg and trendUp   ? color.new(color.green, bgTransp) : na, title = "Bull bias")
bgcolor(mnq_showBg and trendDown ? color.new(color.red, bgTransp)   : na, title = "Bear bias")

tsMarks = mnq_enable and ts_enable and ts_showMarks
plotshape(tsMarks and ts_pbConfirmed and trendDir == 1,  "Pullback confirmed (long)",  shape.triangleup,   location.belowbar, color.lime, size = size.tiny, text = "PB", textcolor = color.lime)
plotshape(tsMarks and ts_pbConfirmed and trendDir == -1, "Pullback confirmed (short)", shape.triangledown, location.abovebar, color.red,  size = size.tiny, text = "PB", textcolor = color.red)

tsA = mnq_enable and ts_enable
alertcondition(tsA and strongLong and not strongLong[1],   "Trend: LONG now STRONG",  "5m long trend upgraded to STRONG")
alertcondition(tsA and strongShort and not strongShort[1], "Trend: SHORT now STRONG", "5m short trend upgraded to STRONG")
alertcondition(tsA and trendDir != 0 and trendDir == trendDir[1] and ts_strong[1] and not ts_strong, "Trend: downgraded to TENTATIVE", "5m trend downgraded from STRONG to TENTATIVE")

// Anchor
useManual = anchorMode == "Manual price" and manualAnchor != 0.0
anchorRaw = useManual ? manualAnchor : anchorMode == "Previous bar close" ? close[1] : close
anchor    = roundToTick(anchorRaw)
string anchorLabel = useManual ? "Manual " : anchorMode == "Previous bar close" ? "Prev close " : "Live "

// Stop distance -> points and ticks
tickSize   = syminfo.mintick
distPts    = distUnit == "Ticks" ? roundToTick(distIn * tickSize) : roundToTick(distIn)
distTicks  = math.round(distPts / tickSize)
upperLevel = anchor + distPts
lowerLevel = anchor - distPts

// Contract value & sizing
pointValue      = pointValueOvr > 0 ? pointValueOvr : syminfo.pointvalue
tickValue       = pointValue * tickSize
riskPerContract = distPts * pointValue
qty             = riskPerContract > 0 ? math.floor(riskUsd / riskPerContract) : 0
actualRisk      = qty * riskPerContract

// Bands
var line lUpper = na
var line lLower = na
if mnq_enable and mnq_showBands and barstate.islast
    ext = extendRight ? extend.right : extend.none
    if na(lUpper)
        lUpper := line.new(bar_index, upperLevel, bar_index + 1, upperLevel, extend = ext, width = 2, color = color.lime)
    line.set_xy1(lUpper, bar_index, upperLevel)
    line.set_xy2(lUpper, bar_index + 1, upperLevel)
    if na(lLower)
        lLower := line.new(bar_index, lowerLevel, bar_index + 1, lowerLevel, extend = ext, width = 2, color = color.red)
    line.set_xy1(lLower, bar_index, lowerLevel)
    line.set_xy2(lLower, bar_index + 1, lowerLevel)

// Table
tblPos = tablePos == "Top left" ? position.top_left : tablePos == "Bottom right" ? position.bottom_right : tablePos == "Bottom left" ? position.bottom_left : tablePos == "Middle right" ? position.middle_right : position.top_right
var table t = table.new(tblPos, 2, 7)
if mnq_enable and showTable and barstate.islast
    valueBg  = color.rgb(20, 20, 20)
    warnBg   = color.rgb(90, 30, 30)
    textCol  = color.white
    pvLooksWrong = pointValueOvr == 0 and syminfo.pointvalue != 2 and str.contains(syminfo.ticker, "MNQ")
    color sideCol = trendUp ? (ts_enable and not ts_strong ? color.new(color.lime, 45) : color.lime) : trendDown ? (ts_enable and not ts_strong ? color.new(color.red, 45) : color.red) : textCol
    table.cell(t, 0, 0, "Bias (5m 21/55)", bgcolor = valueBg, text_color = textCol)
    table.cell(t, 1, 0, sideTxt, bgcolor = valueBg, text_color = sideCol)
    string tsDetail = not ts_enable ? "off" :
         "PB " + str.tostring(ts_pbCount) + "/" + str.tostring(ts_pbNeeded) + (ts_structOk ? " ✓" : " ✗") +
         (ts_inPb ? (ts_deep ? " (deep pullback)" : " (in pullback)") : "") +
         (ts_useSep ? "  ·  Sep " + str.tostring(ts_sep, "#.##") + "×ATR" + (ts_sepOk ? " ✓" : " ✗") : "")
    table.cell(t, 0, 1, "Strength", bgcolor = valueBg, text_color = textCol)
    table.cell(t, 1, 1, tsDetail, bgcolor = valueBg, text_color = ts_strong ? color.lime : textCol)
    table.cell(t, 0, 2, "Stop distance", bgcolor = valueBg, text_color = textCol)
    table.cell(t, 1, 2, str.tostring(distPts, format.mintick) + " pts = " + str.tostring(distTicks) + " ticks", bgcolor = valueBg, text_color = textCol)
    table.cell(t, 0, 3, "Tick / point value", bgcolor = pvLooksWrong ? warnBg : valueBg, text_color = textCol)
    table.cell(t, 1, 3, "$" + str.tostring(tickValue, "#.##") + " / $" + str.tostring(pointValue, "#.##") + (pointValueOvr > 0 ? " (manual)" : " (auto)"), bgcolor = pvLooksWrong ? warnBg : valueBg, text_color = textCol)
    table.cell(t, 0, 4, "Risk/Contract", bgcolor = valueBg, text_color = textCol)
    table.cell(t, 1, 4, "$" + str.tostring(riskPerContract, "#.##"), bgcolor = valueBg, text_color = textCol)
    table.cell(t, 0, 5, "Recommended Qty", bgcolor = valueBg, text_color = textCol)
    table.cell(t, 1, 5, qty >= 1 ? str.tostring(qty) : "Too small (raise risk / tighter distance)", bgcolor = valueBg, text_color = textCol)
    table.cell(t, 0, 6, "Actual risk at Qty", bgcolor = valueBg, text_color = textCol)
    table.cell(t, 1, 6, "$" + str.tostring(actualRisk, "#.##") + " of $" + str.tostring(riskUsd, "#.##"), bgcolor = valueBg, text_color = textCol)

// ═══════════════════════════════════════════════════════════════════════════════════════════════
// 2 · S&R POWER CHANNEL — logic  (© ChartPrime, MPL-2.0; adapted to v6, renamed identifiers)
// ═══════════════════════════════════════════════════════════════════════════════════════════════
type srData
    line sup      = na
    line res      = na
    box  sup_area = na
    box  res_area = na

sr_max_min(int length) =>
    var max = array.new<float>(length)
    var min = array.new<float>(length)
    if barstate.islast
        for i = 0 to length - 1
            max.set(i, high[i])
            min.set(i, low[i])
        float max_ = max.max()
        float min_ = min.min()
        float mid_ = math.avg(max_, min_)
        label.delete(label.new(bar_index + sr_extend + 15, max_, str.tostring(max_, "🡅 #.##"), color = color(na), style = label.style_label_center, textcolor = chart.fg_color)[1])
        label.delete(label.new(bar_index + sr_extend + 15, min_, str.tostring(min_, "🡇 #.##"), color = color(na), style = label.style_label_center, textcolor = chart.fg_color)[1])
        label.delete(label.new(bar_index + sr_extend, mid_, str.tostring(mid_, "🡆 #.##"), color = color(na), style = label.style_label_left, textcolor = color.gray)[1])
        line.delete(line.new(bar_index - length, mid_, bar_index + sr_extend, mid_, color = color.gray, style = line.style_dotted)[1])
        index_of_max = max.indexof(max_)
        index_of_min = min.indexof(min_)
        label.delete(label.new(bar_index[index_of_max], max_, "✖", color = color(na), textcolor = sr_t_col, size = size.normal, style = label.style_label_center)[1])
        label.delete(label.new(bar_index[index_of_min], min_, "✖", color = color(na), textcolor = sr_b_col, size = size.normal, style = label.style_label_center)[1])
    [max.max(), min.min()]

// v6 forbids history on UDT fields (d.sup[1]); instead the previous drawings are deleted
// just before new ones are created on each tick of the last bar.
sr_delete(srData d) =>
    line.delete(d.sup)
    line.delete(d.res)
    box.delete(d.sup_area)
    box.delete(d.res_area)
    d.sup := line(na)
    d.res := line(na)
    d.sup_area := box(na)
    d.res_area := box(na)

var array<label> sr_sigLabels = array.new_label()

sr_power(srData d) =>
    int buy  = 0
    int sell = 0
    if barstate.islast and sr_showPower
        for i = 0 to sr_length - 1
            buy  += (close[i] > open[i] ? 1 : 0)
            sell += (close[i] < open[i] ? 1 : 0)
        if not na(d.sup_area) and not na(d.res_area)
            d.sup_area.set_text(str.tostring(buy,  "            Buy Power: #"))
            d.res_area.set_text(str.tostring(sell, "            Sell Power: #"))
            d.res_area.set_text_color(chart.fg_color)
            d.sup_area.set_text_color(chart.fg_color)
            d.res_area.set_text_halign(text.align_left)
            d.sup_area.set_text_halign(text.align_left)
            d.res_area.set_text_size(size.normal)
            d.sup_area.set_text_size(size.normal)

sr_signals(srData d) =>
    if barstate.islast and sr_showSignals and not na(d.sup_area) and not na(d.res_area)
        // clear last tick's signal labels so they don't pile up on the realtime bar
        while sr_sigLabels.size() > 0
            label.delete(sr_sigLabels.pop())
        top = d.sup_area.get_top()
        bot = d.res_area.get_bottom()
        for i = 0 to sr_length - 1
            low_1  = low[i]
            low_2  = low[i > 0 ? i + 1 : i]
            high_1 = high[i]
            high_2 = high[i > 0 ? i + 1 : i]
            index  = bar_index - i
            if low_1 > top and low_2 <= top
                sr_sigLabels.push(label.new(index, low_2, "◈", textcolor = sr_b_col, color = color(na), style = label.style_label_up, size = size.large))
            if high_1 < bot and high_2 >= bot
                sr_sigLabels.push(label.new(index, high_2, "◈", textcolor = sr_t_col, color = color(na), style = label.style_label_down, size = size.large))

sr_run() =>
    float atr = srAtrHalf
    var srData d = srData.new(line(na), line(na), box(na), box(na))
    [mx, mn] = sr_max_min(sr_length)
    if barstate.islast
        sr_delete(d)
        d.sup := line.new(bar_index - sr_length, mx + atr, bar_index + sr_extend + 30, mx + atr, color = sr_t_col, style = line.style_solid)
        d.res := line.new(bar_index - sr_length, mn - atr, bar_index + sr_extend + 30, mn - atr, color = sr_b_col, style = line.style_solid)
        d.res_area := box.new(bar_index - sr_length, mx + atr, bar_index + sr_extend, mx - atr, na, bgcolor = color.new(sr_t_col, 80))
        d.sup_area := box.new(bar_index - sr_length, mn + atr, bar_index + sr_extend, mn - atr, na, bgcolor = color.new(sr_b_col, 80))
    sr_signals(d)
    sr_power(d)

if sr_enable
    sr_run()

// ═══════════════════════════════════════════════════════════════════════════════════════════════
// 3 · VOLUME PROFILE — logic  (© kv4coins, MPL-2.0)
// ═══════════════════════════════════════════════════════════════════════════════════════════════
// Splits the lookback's price range into equal bins and adds each bar's volume to every bin it
// traded through. PoC = the bin with most volume. Value Area = bins added outward from the PoC
// (larger neighbour first) until they hold the chosen share of total volume.
var int   vp_tf_ms        = 1000 * timeframe.in_seconds()
var int   vp_lookback     = vp_use_visible_range ? math.max(10, math.round((last_bar_time - chart.left_visible_bar_time) / vp_tf_ms)) : vp_lookback_depth
var line  vp_poc = na
var line  vp_vah = na
var line  vp_val = na
var array<line>  vp_bars    = array.new_line()
var array<float> vp_volumes = array.new_float(vp_num_bars, 0)

float vp_highest = ta.highest(vp_lookback)
float vp_lowest  = ta.lowest(vp_lookback)
float vp_interval = (vp_highest - vp_lowest) / (vp_num_bars - 1)

vp_scale(float vol, float mx) =>
    float d = mx / vp_lookback / (vp_bar_len_mult / 100.0)
    d > 0 ? math.round(vol / d) : 0

vp_calculate() =>
    array.fill(vp_volumes, 0)
    for i = 0 to (vp_lookback - 1)
        bool is_bullish  = close[i] >= open[i]
        bool include_vol = vp_volume_type == 'Both' ? true : (vp_volume_type == 'Bullish' ? is_bullish : not is_bullish)
        if include_vol
            for j = 0 to (vp_num_bars - 1)
                float price_level = vp_lowest + vp_interval * j
                if price_level >= low[i] and price_level < high[i]
                    array.set(vp_volumes, j, vp_volumes.get(j) + volume[i])

vp_calculate_va(int max_idx, float max_vol) =>
    float sum_vol = vp_volumes.sum()
    float va_vol  = sum_vol * vp_va_percent / 100.0
    int va_up = max_idx
    int va_dn = max_idx
    float va_sum = max_vol
    while va_sum < va_vol
        float v_up = (va_up < vp_num_bars - 1) ? vp_volumes.get(va_up + 1) : 0.0
        float v_dn = (va_dn > 0) ? vp_volumes.get(va_dn - 1) : 0.0
        if v_up == 0 and v_dn == 0
            break
        if v_up >= v_dn
            va_sum += v_up
            va_up  += 1
        else
            va_sum += v_dn
            va_dn  -= 1
    [va_dn, va_up]

vp_draw() =>
    float max_vol = vp_volumes.max()
    int   max_idx = math.max(0, vp_volumes.indexof(max_vol))
    int   x2 = bar_index + vp_right_offset
    for i = 0 to (vp_num_bars - 1)
        int vol = vp_scale(vp_volumes.get(i), max_vol)
        int x1  = x2 - vol
        float y = vp_lowest + vp_interval * i
        line.set_xy1(vp_bars.get(i), x1, y)
        line.set_xy2(vp_bars.get(i), x2, y)
        line.set_color(vp_bars.get(i), max_idx == i ? vp_poc_line_color : vp_bar_color)
    if vp_display_poc
        int vol = vp_scale(max_vol, max_vol)
        float y = vp_lowest + vp_interval * max_idx
        line.set_xy1(vp_poc, bar_index - vp_lookback, y)
        line.set_xy2(vp_poc, x2 - vol - 10, y)
    if vp_display_va
        [va_dn, va_up] = vp_calculate_va(max_idx, max_vol)
        for i = va_dn to va_up
            if i != max_idx
                line.set_color(vp_bars.get(i), vp_va_bar_color)
        if vp_display_va_lines
            int vah_vol = vp_scale(vp_volumes.get(va_up), max_vol)
            int val_vol = vp_scale(vp_volumes.get(va_dn), max_vol)
            int va_x1 = bar_index - vp_lookback
            float vah_y = vp_lowest + vp_interval * va_up
            float val_y = vp_lowest + vp_interval * va_dn
            line.set_xy1(vp_vah, va_x1, vah_y)
            line.set_xy2(vp_vah, x2 - vah_vol - 10, vah_y)
            line.set_xy1(vp_val, va_x1, val_y)
            line.set_xy2(vp_val, x2 - val_vol - 10, val_y)

if vp_enable and barstate.isfirst
    for i = 0 to (vp_num_bars - 1)
        array.push(vp_bars, line.new(x1 = bar_index, y1 = close, x2 = bar_index, y2 = close, width = vp_bar_thickness))
    if vp_display_poc
        vp_poc := line.new(x1 = bar_index, y1 = close, x2 = bar_index, y2 = close, color = vp_poc_line_color, width = vp_poc_line_thickness)
    if vp_display_va and vp_display_va_lines
        vp_vah := line.new(x1 = bar_index, y1 = close, x2 = bar_index, y2 = close, color = vp_va_bar_color, width = vp_va_lines_thickness)
        vp_val := line.new(x1 = bar_index, y1 = close, x2 = bar_index, y2 = close, color = vp_va_bar_color, width = vp_va_lines_thickness)

if vp_enable and barstate.islast
    vp_calculate()
    vp_draw()

// ═══════════════════════════════════════════════════════════════════════════════════════════════
// 4 · KEY LEVELS — logic
// ═══════════════════════════════════════════════════════════════════════════════════════════════
// Levels are drawn on the last bar only, so they cost nothing on history. Session highs/lows
// (overnight, opening range) keep their value after the session ends until the next one starts.
// Higher-timeframe values (lookahead_on + [1] = confirmed previous-period values, standard pattern)
[pdh, pdl, pdc, cdo] = request.security(syminfo.tickerid, "D", [high[1], low[1], close[1], open], lookahead = barmerge.lookahead_on)
[pwh, pwl, pwc, cwo] = request.security(syminfo.tickerid, "W", [high[1], low[1], close[1], open], lookahead = barmerge.lookahead_on)
[pmh, pml, cmo]      = request.security(syminfo.tickerid, "M", [high[1], low[1], open],           lookahead = barmerge.lookahead_on)
pdm = math.avg(pdh, pdl)

// Period start bars
var int dayStartBar   = 0
var int weekStartBar  = 0
var int monthStartBar = 0
if timeframe.change("1D")
    dayStartBar := bar_index
if timeframe.change("1W")
    weekStartBar := bar_index
if timeframe.change("1M")
    monthStartBar := bar_index

// Today's running high / low
var float cdh = na
var float cdl = na
if timeframe.change("1D")
    cdh := high
    cdl := low
else
    cdh := math.max(nz(cdh, high), high)
    cdl := math.min(nz(cdl, low), low)

// Session ranges (overnight + opening range) — value persists after the session ends until the next one starts
inOn   = not na(time(timeframe.period, kl_onSess, kl_tz))
inOr   = not na(time(timeframe.period, kl_orSess, kl_tz))
var float onH = na
var float onL = na
var int   onStartBar = 0
var float orH = na
var float orL = na
var int   orStartBar = 0
if inOn and not inOn[1]
    onH := high
    onL := low
    onStartBar := bar_index
else if inOn
    onH := math.max(onH, high)
    onL := math.min(onL, low)
if inOr and not inOr[1]
    orH := high
    orL := low
    orStartBar := bar_index
else if inOr
    orH := math.max(orH, high)
    orL := math.min(orL, low)
orM = math.avg(orH, orL)

klStyle = kl_style == "Dashed" ? line.style_dashed : kl_style == "Dotted" ? line.style_dotted : line.style_solid
klExt   = kl_extend ? extend.right : extend.none

// Draws / updates one level. Each call site gets its own `var` line + label.
kl_level(bool show, float price, int startBar, color col, string name) =>
    var line  ln = na
    var label lb = na
    bool draw = barstate.islast and show and not na(price)
    if draw
        int x1 = kl_fromStart ? math.max(startBar, bar_index - kl_maxBars) : bar_index - kl_maxBars
        x1 := math.max(x1, 0)
        int x2 = bar_index + kl_labelOff
        string txt = name + (kl_showPrice ? "  " + str.tostring(price, format.mintick) : "")
        if na(ln)
            ln := line.new(x1, price, x2, price, xloc.bar_index, klExt, col, klStyle, kl_width)
            lb := label.new(x2, price, txt, xloc.bar_index, yloc.price, color(na), label.style_label_left, col, kl_labelSize)
        line.set_xy1(ln, x1, price)
        line.set_xy2(ln, x2, price)
        line.set_color(ln, col)
        line.set_style(ln, klStyle)
        line.set_width(ln, kl_width)
        line.set_extend(ln, klExt)
        label.set_xy(lb, x2, price)
        label.set_text(lb, txt)
        label.set_textcolor(lb, col)
        label.set_size(lb, kl_labelSize)
    // separate `if` (no else) so both code paths have the same (void) type
    if barstate.islast and not draw and not na(ln)
        line.delete(ln)
        label.delete(lb)
        ln := line(na)
        lb := label(na)

// Daily
kl_level(kl_enable and kl_pdh, pdh, dayStartBar, kl_pdCol, "PDH")
kl_level(kl_enable and kl_pdl, pdl, dayStartBar, kl_pdCol, "PDL")
kl_level(kl_enable and kl_pdc, pdc, dayStartBar, kl_pdCol, "PDC")
kl_level(kl_enable and kl_pdm, pdm, dayStartBar, color.new(kl_pdCol, 40), "PDM")
kl_level(kl_enable and kl_cdo, cdo, dayStartBar, kl_pdCol, "DO")
kl_level(kl_enable and kl_cdh, cdh, dayStartBar, color.new(kl_pdCol, 40), "HOD")
kl_level(kl_enable and kl_cdh, cdl, dayStartBar, color.new(kl_pdCol, 40), "LOD")
// Weekly
kl_level(kl_enable and kl_pwh, pwh, weekStartBar, kl_pwCol, "PWH")
kl_level(kl_enable and kl_pwl, pwl, weekStartBar, kl_pwCol, "PWL")
kl_level(kl_enable and kl_pwc, pwc, weekStartBar, kl_pwCol, "PWC")
kl_level(kl_enable and kl_cwo, cwo, weekStartBar, kl_pwCol, "WO")
// Monthly
kl_level(kl_enable and kl_pmh, pmh, monthStartBar, kl_pmCol, "PMH")
kl_level(kl_enable and kl_pml, pml, monthStartBar, kl_pmCol, "PML")
kl_level(kl_enable and kl_cmo, cmo, monthStartBar, kl_pmCol, "MO")
// Sessions
kl_level(kl_enable and kl_on, onH, onStartBar, kl_onCol, "ONH")
kl_level(kl_enable and kl_on, onL, onStartBar, kl_onCol, "ONL")
kl_level(kl_enable and kl_or, orH, orStartBar, kl_orCol, "ORH")
kl_level(kl_enable and kl_or, orL, orStartBar, kl_orCol, "ORL")
kl_level(kl_enable and kl_or and kl_orMid, orM, orStartBar, color.new(kl_orCol, 40), "ORM")

// Alerts (global scope). Only fire when the module + alerts are enabled.
klA = kl_enable and kl_alerts
xPdhUp = klA and ta.crossover(close, pdh)
xPdlDn = klA and ta.crossunder(close, pdl)
xPdc   = klA and ta.cross(close, pdc)
xOnhUp = klA and ta.crossover(close, onH)
xOnlDn = klA and ta.crossunder(close, onL)
xOrhUp = klA and ta.crossover(close, orH)
xOrlDn = klA and ta.crossunder(close, orL)
alertcondition(xPdhUp, "KL: Close above PDH",  "Close crossed above previous day high")
alertcondition(xPdlDn, "KL: Close below PDL",  "Close crossed below previous day low")
alertcondition(xPdc,   "KL: Close crossed PDC","Close crossed previous day close")
alertcondition(xOnhUp, "KL: Close above ONH",  "Close crossed above overnight high")
alertcondition(xOnlDn, "KL: Close below ONL",  "Close crossed below overnight low")
alertcondition(xOrhUp, "KL: Close above ORH",  "Close crossed above opening-range high")
alertcondition(xOrlDn, "KL: Close below ORL",  "Close crossed below opening-range low")

// ═══════════════════════════════════════════════════════════════════════════════════════════════
// 5 · VWAP — logic
// ═══════════════════════════════════════════════════════════════════════════════════════════════
// Volume-weighted average price since the chosen anchor, with bands at multiples of the
// volume-weighted standard deviation. The default anchor (18:00 New York) matches the CME Globex
// open, so the VWAP covers the full futures trading day.
// Anchor (true on the bar where VWAP resets). All ta.* calls at global scope.
inRth        = not na(time(timeframe.period, vw_rthSess, kl_tz))
rthStart     = inRth and not inRth[1]

// Custom time: shift time so the anchor clock time falls on midnight, then reset when the shifted date changes.
// Uses kl_tz, so America/New_York tracks the EST/EDT switch the same way CME does.
vwShiftMs    = ((1440 - (vw_anchorH * 60 + vw_anchorM)) % 1440) * 60 * 1000
vwCustomDay  = dayofmonth(time + vwShiftMs, kl_tz)
customStart  = vwCustomDay != vwCustomDay[1]

vwAnchor     = vw_anchor == "Custom time" ? customStart :
               vw_anchor == "RTH session" ? rthStart :
               vw_anchor == "Week"        ? timeframe.change("1W") :
               vw_anchor == "Month"       ? timeframe.change("1M") :
               timeframe.change("1D")
vwAnchor    := vwAnchor or barstate.isfirst

// ta.vwap returns [vwap, upper, lower] for one multiplier; derive the other bands from the same σ
[vwV, vwU1, vwL1] = ta.vwap(vw_src, vwAnchor, vw_m1)
vwSd  = (vwU1 - vwV) / vw_m1
vwU2  = vwV + vwSd * vw_m2
vwL2  = vwV - vwSd * vw_m2
vwU3  = vwV + vwSd * vw_m3
vwL3  = vwV - vwSd * vw_m3
[vwW, vwWU, vwWL] = ta.vwap(vw_src, timeframe.change("1W") or barstate.isfirst, 1.0)

// For the RTH anchor, hide the line outside the session so it doesn't drag across the overnight
vwVisible = vw_enable and (vw_anchor != "RTH session" or inRth)

plot(vwVisible ? vwV : na, "VWAP", color = vw_col, linewidth = vw_width)
pU1 = plot(vwVisible and vw_b1 ? vwU1 : na, "VWAP +1σ", color = color.new(vw_bandCol, 30), linewidth = 1)
pL1 = plot(vwVisible and vw_b1 ? vwL1 : na, "VWAP -1σ", color = color.new(vw_bandCol, 30), linewidth = 1)
pU2 = plot(vwVisible and vw_b2 ? vwU2 : na, "VWAP +2σ", color = color.new(vw_bandCol, 50), linewidth = 1)
pL2 = plot(vwVisible and vw_b2 ? vwL2 : na, "VWAP -2σ", color = color.new(vw_bandCol, 50), linewidth = 1)
pU3 = plot(vwVisible and vw_b3 ? vwU3 : na, "VWAP +3σ", color = color.new(vw_bandCol, 65), linewidth = 1)
pL3 = plot(vwVisible and vw_b3 ? vwL3 : na, "VWAP -3σ", color = color.new(vw_bandCol, 65), linewidth = 1)
fill(pU1, pL1, color = vwVisible and vw_fill and vw_b1 ? color.new(vw_bandCol, 92) : na, title = "Band 1 fill")
fill(pU2, pL2, color = vwVisible and vw_fill and vw_b2 ? color.new(vw_bandCol, 95) : na, title = "Band 2 fill")
fill(pU3, pL3, color = vwVisible and vw_fill and vw_b3 ? color.new(vw_bandCol, 97) : na, title = "Band 3 fill")
plot(vw_enable and vw_showW ? vwW : na, "Weekly VWAP", color = vw_wCol, linewidth = 1, style = plot.style_line)

vwA = vw_enable and vw_alerts
xVwUp = vwA and ta.crossover(close, vwV)
xVwDn = vwA and ta.crossunder(close, vwV)
alertcondition(xVwUp, "VWAP: Close above VWAP", "Close crossed above VWAP")
alertcondition(xVwDn, "VWAP: Close below VWAP", "Close crossed below VWAP")

// ═══════════════════════════════════════════════════════════════════════════════════════════════
// 6 · FAIR VALUE GAPS — inputs
// ═══════════════════════════════════════════════════════════════════════════════════════════════
// A fair value gap (FVG) is a three-bar imbalance. Bullish: bar 3's low is above bar 1's high, so
// bar 2 travelled through a price range that no other bar traded. Bearish: the mirror image.
// Gaps are often revisited; this module draws them, filters them by the 5m bias from module 1,
// and outlines the most recent gap in the bias direction as the "active" one.
G_FVG1 = "6 · FVG · Detection"
G_FVG2 = "6 · FVG · Bias Filter"
G_FVG3 = "6 · FVG · Style"
G_FVG4 = "6 · FVG · Forming / Confirmation"

fvg_minAtr = input.float(0.0, "Minimum gap size (× ATR)", minval = 0, step = 0.05, group = G_FVG1,
               tooltip = "Ignore gaps smaller than this many ATRs, measured on the FVG timeframe. 0 = keep every gap.")
fvg_atrLen = input.int(14, "ATR length", minval = 1, group = G_FVG1)
fvg_tf     = input.timeframe("", "Timeframe", group = G_FVG1,
               tooltip = "Blank = chart timeframe. A higher timeframe (e.g. 5 or 15) finds gaps on that timeframe and draws them on this chart.")

fvg_filterMode  = input.string("At formation", "Bias filter", options = ["At formation", "Current bias", "Off"], group = G_FVG2,
                    tooltip = "At formation: an FVG is kept only if it matched the 5m bias on the bar it confirmed. Boxes stay once drawn.\nCurrent bias: every FVG is recorded, but only those matching the bias right now are shown. When the bias flips, the visible set flips with it.\nOff: show every FVG.")
fvg_biasSrc     = input.string("Table bias (21/55 + EMA200)", "Bias source", options = ["Table bias (21/55 + EMA200)", "EMA 21/55 only"], group = G_FVG2,
                    tooltip = "Table bias = the LONG / SHORT shown in the sizing table (21/55 cross, plus the EMA200 macro filter if enabled in module 1). EMA 21/55 only ignores the EMA200.")
fvg_neutralShow = input.bool(false, "Show both directions when bias is neutral", group = G_FVG2,
                    tooltip = "Neutral = 21/55 and EMA200 disagree. Off = no FVGs while neutral.")
fvg_requireStrong = input.bool(false, "Only when trend is STRONG", group = G_FVG2,
                    tooltip = "Adds the module 1b strength check on top of the bias filter: an FVG passes only if the trend in its direction is STRONG. Ignored if Trend Strength is switched off.")

fvg_extend    = input.int(20, "Extend (bars)", minval = 0, group = G_FVG3)
fvg_maxBoxes  = input.int(100, "Max FVG boxes kept", minval = 1, maxval = 450, group = G_FVG3)
fvg_bullCss   = input.color(color.new(#089981, 50), "Bullish FVG", inline = "fvgcol", group = G_FVG3)
fvg_bearCss   = input.color(color.new(#f23645, 50), "Bearish FVG", inline = "fvgcol", group = G_FVG3)
fvg_outline   = input.color(color.yellow, "Active FVG outline", inline = "fvgout", group = G_FVG3)
fvg_outWidth  = input.int(2, "Width", minval = 1, maxval = 4, inline = "fvgout", group = G_FVG3)

fvg_showLive    = input.bool(true, "Show forming FVGs", group = G_FVG4,
                    tooltip = "Realtime only: shades a gap while its third bar is still open. It is locked in or discarded when that bar closes.")
fvg_bullFormCss = input.color(color.new(#089981, 90), "Forming bullish", inline = "fvgform", group = G_FVG4)
fvg_bearFormCss = input.color(color.new(#f23645, 90), "Forming bearish", inline = "fvgform", group = G_FVG4)
fvg_formBorder  = input.bool(false, "Dashed outline on forming", group = G_FVG4)
fvg_showConf    = input.bool(true, "Show confirmation marker", group = G_FVG4)
fvg_confBullCss = input.color(#089981, "Bullish", inline = "fvgconf", group = G_FVG4)
fvg_confBearCss = input.color(#f23645, "Bearish", inline = "fvgconf", group = G_FVG4)

// ═══════════════════════════════════════════════════════════════════════════════════════════════
// 6 · FAIR VALUE GAPS — logic
// ═══════════════════════════════════════════════════════════════════════════════════════════════
// One recorded gap. Kept until price closes back through it twice in a row (mitigation).
type FairValueGap
    float top
    float bottom
    bool  isBull
    int   leftTime

// Looks at the last three bars of whatever series it runs on (chart or FVG timeframe).
// Returns [bullish gap?, bearish gap?, gap top, gap bottom, time of the first of the three bars].
fvg_scan() =>
    float atrV    = ta.atr(fvg_atrLen)
    float minSize = fvg_minAtr * atrV
    bool  bullGap = low > high[2] and close[1] > high[2] and low - high[2] >= minSize
    bool  bearGap = high < low[2] and close[1] < low[2] and low[2] - high >= minSize
    float gapTop  = bullGap ? low : bearGap ? low[2] : na
    float gapBot  = bullGap ? high[2] : bearGap ? high : na
    [bullGap, bearGap, gapTop, gapBot, time[2]]

// The same scan one bar back: only gaps whose third bar has closed. Used with lookahead_on,
// this is the standard non-repainting way to read a completed higher-timeframe bar.
fvg_scanClosed() =>
    [b, s, tp, bt, lt] = fvg_scan()
    [b[1], s[1], tp[1], bt[1], lt[1]]

// Developing (still-open) bar: realtime only, so no lookahead is needed.
[fvgLiveBull, fvgLiveBear, fvgLiveMax, fvgLiveMin, fvgLiveT] = request.security(syminfo.tickerid, fvg_tf, fvg_scan())
// Last completed bar: drives everything that is drawn permanently.
[fvgBullConf, fvgBearConf, fvgConfMax, fvgConfMin, fvgConfT] = request.security(syminfo.tickerid, fvg_tf, fvg_scanClosed(), lookahead = barmerge.lookahead_on)

fvgHtfChange = fvg_tf == "" ? true : timeframe.change(fvg_tf)   // true on the first chart bar of each FVG-timeframe bar
fvgChartMs   = timeframe.in_seconds() * 1000

// 5m bias used by the filter (independent of the module-1 on/off switch)
fvgUseTableBias = fvg_biasSrc == "Table bias (21/55 + EMA200)"
fvgBiasLong     = bullBias and (not fvgUseTableBias or macroBullOk)
fvgBiasShort    = bearBias and (not fvgUseTableBias or macroBearOk)
fvgNeutral      = not fvgBiasLong and not fvgBiasShort

// Does a gap in this direction pass the bias (and optional strength) filter right now?
fvg_allowed(bool isBull) =>
    bool biasOk   = fvg_filterMode == "Off" or (isBull ? fvgBiasLong : fvgBiasShort) or (fvgNeutral and fvg_neutralShow)
    bool strongOk = not fvg_requireStrong or not ts_enable or (isBull ? strongLong : strongShort)
    biasOk and strongOk

var array<FairValueGap> fvg_records = array.new<FairValueGap>()   // unmitigated gaps, newest first
var array<box>          fvg_boxes   = array.new<box>()            // drawn boxes, newest first
var array<bool>         fvg_boxBull = array.new<bool>()           // direction of each drawn box
var box          fvg_liveBox     = na    // the forming box on the realtime bar
var box          fvg_hlBox       = na    // box currently carrying the "active" outline
var FairValueGap fvg_lastBull    = na
var FairValueGap fvg_lastBear    = na
var box          fvg_lastBullBox = na
var box          fvg_lastBearBox = na
var int          fvg_bullMitCount = 0
var int          fvg_bearMitCount = 0

// Bias check on the bar the FVG confirms (drives markers and alerts in every mode)
fvgNewBull = fvg_enable and fvgHtfChange and fvgBullConf and fvg_allowed(true)
fvgNewBear = fvg_enable and fvgHtfChange and fvgBearConf and fvg_allowed(false)

// ── Confirmation: when an FVG-timeframe bar closes, lock in the forming box or discard it
if fvg_enable and fvgHtfChange
    if fvgBullConf or fvgBearConf
        bool isBull = fvgBullConf
        bool passes = fvg_allowed(isBull)
        if passes or fvg_filterMode == "Current bias"
            color col      = isBull ? fvg_bullCss : fvg_bearCss
            int   fvgRight = time + fvg_extend * fvgChartMs
            box   b        = na
            if not na(fvg_liveBox)
                // reuse the forming box so it doesn't flicker
                b := fvg_liveBox
                box.set_lefttop(b, fvgConfT, fvgConfMax)
                box.set_rightbottom(b, fvgRight, fvgConfMin)
                box.set_bgcolor(b, col)
                box.set_border_color(b, color(na))
                box.set_border_style(b, line.style_solid)
                box.set_border_width(b, fvg_outWidth)
                fvg_liveBox := na
            else
                b := box.new(fvgConfT, fvgConfMax, fvgRight, fvgConfMin, border_color = color(na), border_width = fvg_outWidth,
                     xloc = xloc.bar_time, bgcolor = col)
            fvg_boxes.unshift(b)
            fvg_boxBull.unshift(isBull)
            if fvg_boxes.size() > fvg_maxBoxes
                box.delete(fvg_boxes.pop())
                fvg_boxBull.pop()

            FairValueGap rec = FairValueGap.new(fvgConfMax, fvgConfMin, isBull, fvgConfT)
            fvg_records.unshift(rec)
            if fvg_records.size() > 500
                fvg_records.pop()
            if isBull
                fvg_lastBull    := rec
                fvg_lastBullBox := b
            else
                fvg_lastBear    := rec
                fvg_lastBearBox := b
        else if not na(fvg_liveBox)
            box.delete(fvg_liveBox)
            fvg_liveBox := na

        if passes
            alert((isBull ? "Bullish" : "Bearish") + " FVG confirmed (with bias). Low: " + str.tostring(fvgConfMin, format.mintick) + ", High: " + str.tostring(fvgConfMax, format.mintick), alert.freq_once_per_bar)

    else if not na(fvg_liveBox)
        // the bar closed without a valid gap: discard the forming box
        box.delete(fvg_liveBox)
        fvg_liveBox := na

// ── Forming FVG (realtime only, bias-filtered)
fvgLiveActive = fvg_enable and fvg_showLive and barstate.isrealtime and (fvgLiveBull or fvgLiveBear) and fvg_allowed(fvgLiveBull)

if fvgLiveActive
    color fillCol  = fvgLiveBull ? fvg_bullFormCss : fvg_bearFormCss
    color border   = fvg_formBorder ? color.new(fillCol, 0) : color(na)
    int   fvgRight = time + fvg_extend * fvgChartMs
    if na(fvg_liveBox)
        fvg_liveBox := box.new(fvgLiveT, fvgLiveMax, fvgRight, fvgLiveMin, border_color = border, border_width = 1,
                         border_style = line.style_dashed, xloc = xloc.bar_time, bgcolor = fillCol)
        alert((fvgLiveBull ? "Bullish" : "Bearish") + " FVG forming (with bias)", alert.freq_once_per_bar)
    else
        box.set_lefttop(fvg_liveBox, fvgLiveT, fvgLiveMax)
        box.set_rightbottom(fvg_liveBox, fvgRight, fvgLiveMin)
        box.set_border_color(fvg_liveBox, border)
        box.set_border_style(fvg_liveBox, line.style_dashed)
        box.set_bgcolor(fvg_liveBox, fillCol)
else if not na(fvg_liveBox)
    // gap closed mid-bar or the bias flipped: hide it; it reappears if conditions return before the bar closes
    box.set_border_color(fvg_liveBox, color(na))
    box.set_bgcolor(fvg_liveBox, color(na))

// ── Forming FVG alerts: once per FVG-timeframe bar per direction, the moment a gap starts forming.
// varip so the "already alerted" flag survives intrabar ticks instead of rolling back.
varip bool fvgFormBullDone = false
varip bool fvgFormBearDone = false
if barstate.isnew and fvgHtfChange
    fvgFormBullDone := false
    fvgFormBearDone := false

bool fvgFormingBull = fvgLiveActive and fvgLiveBull and not fvgFormBullDone
bool fvgFormingBear = fvgLiveActive and fvgLiveBear and not fvgFormBearDone
if fvgFormingBull
    fvgFormBullDone := true
if fvgFormingBear
    fvgFormBearDone := true

alertcondition(fvgFormingBull, "FVG: Bullish forming (with bias)", "Bullish FVG forming in line with 5m bias")
alertcondition(fvgFormingBear, "FVG: Bearish forming (with bias)", "Bearish FVG forming in line with 5m bias")

// ── Mitigation: a gap is done once two consecutive closes sit beyond its far edge
if fvg_enable and fvg_records.size() > 0
    for i = fvg_records.size() - 1 to 0
        FairValueGap g = fvg_records.get(i)
        bool alertOk = fvg_filterMode != "Current bias" or fvg_allowed(g.isBull)
        if g.isBull and close < g.bottom and close[1] < g.bottom
            fvg_records.remove(i)
            if alertOk
                fvg_bullMitCount += 1
                alert("Bullish FVG mitigated at " + str.tostring(g.bottom, format.mintick), alert.freq_once_per_bar_close)
        else if not g.isBull and close > g.top and close[1] > g.top
            fvg_records.remove(i)
            if alertOk
                fvg_bearMitCount += 1
                alert("Bearish FVG mitigated at " + str.tostring(g.top, format.mintick), alert.freq_once_per_bar_close)

// ── Active FVG = the latest gap in the current bias direction (or the latest overall when unfiltered)
bool fvgLatestIsBull = not na(fvg_lastBull) and (na(fvg_lastBear) or fvg_lastBull.leftTime >= fvg_lastBear.leftTime)
bool fvgAnyFvg       = not na(fvg_lastBull) or not na(fvg_lastBear)
int  fvgActiveDir    = fvg_filterMode == "Off" or (fvgNeutral and fvg_neutralShow) ? (not fvgAnyFvg ? 0 : fvgLatestIsBull ? 1 : -1) :
                       fvgBiasLong ? 1 : fvgBiasShort ? -1 : 0

if fvg_enable and barstate.islast
    // Current-bias mode: show only boxes that match the bias right now
    if fvg_filterMode == "Current bias" and fvg_boxes.size() > 0
        for i = 0 to fvg_boxes.size() - 1
            bool isB = fvg_boxBull.get(i)
            box.set_bgcolor(fvg_boxes.get(i), fvg_allowed(isB) ? (isB ? fvg_bullCss : fvg_bearCss) : color(na))

    // Move the outline to the active FVG
    if not na(fvg_hlBox)
        box.set_border_color(fvg_hlBox, color(na))
    box activeBox = fvgActiveDir == 1 ? fvg_lastBullBox : fvgActiveDir == -1 ? fvg_lastBearBox : box(na)
    if not na(activeBox)
        box.set_border_color(activeBox, fvg_outline)
        box.set_border_width(activeBox, fvg_outWidth)
    fvg_hlBox := activeBox

// ── Confirmation markers + alert conditions
plotshape(fvg_showConf and fvgNewBull, "FVG bullish confirmed", shape.circle, location.abovebar, fvg_confBullCss, offset = -1, size = size.tiny)
plotshape(fvg_showConf and fvgNewBear, "FVG bearish confirmed", shape.circle, location.abovebar, fvg_confBearCss, offset = -1, size = size.tiny)

alertcondition(fvgNewBull, "FVG: Bullish confirmed (with bias)", "Bullish FVG confirmed in line with 5m bias")
alertcondition(fvgNewBear, "FVG: Bearish confirmed (with bias)", "Bearish FVG confirmed in line with 5m bias")
alertcondition(fvg_bullMitCount > fvg_bullMitCount[1], "FVG: Bullish mitigated", "Bullish FVG mitigated")
alertcondition(fvg_bearMitCount > fvg_bearMitCount[1], "FVG: Bearish mitigated", "Bearish FVG mitigated")