IFVG - DodgyDD and ICT

Description

the famous ifvg, to detect sweeps of key session levels, with lookback periods, plus more. made free for the community. free for community use, to build off of.

Categories & Tags

Smart Money#ict#ifvg

Comments (0)

0/2000

Loading comments…

Source code

// ═══════════════════════════════════════════════════════════════════
// ICT IFVG Sweep Strategy — AUTOMATED (v11) — RR-based
// ═══════════════════════════════════════════════════════════════════
// Full-automation build of the v8 indicator. Detection, internal H/L
// pivots, bias table, and rendering are preserved verbatim; a live-
// trading execution layer is added on top that fires
// ctx.strategy.entry() with bracket orders when a signal locks in on
// the just-confirmed bar.
//
// SL/TP is RR-BASED (matches v8):
//   Bull  → SL = FVG bottom − stopBuffer, TP = entry + risk × R:R
//   Bear  → SL = FVG top    + stopBuffer, TP = entry − risk × R:R
//
// Defaults reflect the settings from the strategy-settings panel you
// screenshotted. "Enable Live Trading" defaults OFF for safety; flip
// it to ON in the strategy panel when you're ready. Backtests bypass
// that flag so historical runs always produce trades.
//
// ZERO-REPAINT: every detection uses only the CONFIRMED bar. Live bar
// is never referenced in signal generation, and entries fire only on
// the exact confirmed bar the inversion locked in — no re-firing of
// historical signals on hot reload.
// ═══════════════════════════════════════════════════════════════════

function calculate(bars, ctx) {
  // ═══════════════════════════════════════════════════════════════════
  // EXECUTION INPUTS
  // ═══════════════════════════════════════════════════════════════════
  var enableTrading      = ctx.input('Enable Live Trading', false, { liveOnly: true });
  var contractQty        = ctx.input('Contracts', 1, { min: 1, max: 10, step: 1 });
  var oneTradePerSession = ctx.input('One Trade Per Session', true);
  var sessionResetHour   = ctx.input('Session Reset Hour (PT)', 6,  { min: 0, max: 23, step: 1 });
  var sessionResetMin    = ctx.input('Session Reset Minute',   30, { min: 0, max: 59, step: 1 });
  var cooldownHrs        = ctx.input('Rolling Cooldown Hours (0 = off)', 0, { min: 0, max: 48, step: 1 });
  var dailyLossKill      = ctx.input('Daily Loss Kill ($, 0 = off)', 0, { min: 0, max: 10000, step: 50 });
  var flattenAtNYEnd     = ctx.input('Flatten at NY End', false);

  // ═══════════════════════════════════════════════════════════════════
  // DETECTION INPUTS (defaults from your settings screenshots)
  // ═══════════════════════════════════════════════════════════════════
  var lookbackDays = ctx.input('Lookback Days', 3, { min: 1, max: 30, step: 1 });

  var sweepAsian   = ctx.input('Sweep Asian H/L', true);
  var sweepLondon  = ctx.input('Sweep London H/L', true);
  var sweepNY      = ctx.input('Sweep NY H/L', true);

  var asianStart  = ctx.input('Asian Start (ET)', 20, { min: 0, max: 23, step: 1 });
  var asianEnd    = ctx.input('Asian End (ET)', 0, { min: 0, max: 23, step: 1 });
  var londonStart = ctx.input('London Start (ET)', 2, { min: 0, max: 23, step: 1 });
  var londonEnd   = ctx.input('London End (ET)', 5, { min: 0, max: 23, step: 1 });
  var nyStart     = ctx.input('NY Start (ET)', 9, { min: 0, max: 23, step: 1 });
  var nyEnd       = ctx.input('NY End (ET)', 12, { min: 0, max: 23, step: 1 });

  var minSweepTicks       = ctx.input('Min Sweep Past Level (ticks)', 2, { min: 0, max: 50, step: 1 });
  var minGapTicks         = ctx.input('Min FVG Size (ticks)', 3, { min: 1, max: 100, step: 1 });
  var maxBarsForInversion = ctx.input('Max Bars For Inversion', 20, { min: 1, max: 50, step: 1 });
  var inversionMode       = ctx.input('Inversion Mode', 'close_only', { options: ['close_only', 'wick_or_close'] });
  var showMitigated       = ctx.input('Show Mitigated IFVGs', false);
  var showCELine          = ctx.input('Display Consequent Encroachment', true);
  var maxIFVGs            = ctx.input('Max IFVGs Displayed', 20, { min: 1, max: 50, step: 1 });

  var showEntrySignals = ctx.input('Show Entry Signals', true);
  var onlyPostSweep    = ctx.input('Entry Only After Sweep', false);
  var entryRR          = ctx.input('Target R:R', 2, { min: 0.5, max: 10, step: 0.5 });
  var stopBuffer       = ctx.input('Stop Buffer (ticks)', 2, { min: 0, max: 20, step: 1 });
  var showEntryLines   = ctx.input('Show Entry/SL/TP Lines', true);
  var entryColor       = ctx.input('Entry Signal Color', '#00e5ff');

  var useIFVGTimeFilter = ctx.input('Filter IFVG By Time', true);
  var ifvgStartHour     = ctx.input('IFVG Scan Start Hour (PT)', 6, { min: 0, max: 23, step: 1 });
  var ifvgStartMin      = ctx.input('IFVG Scan Start Minute', 30, { min: 0, max: 59, step: 1 });
  var ifvgEndHour       = ctx.input('IFVG Scan End Hour (PT)', 12, { min: 0, max: 23, step: 1 });
  var ifvgEndMin        = ctx.input('IFVG Scan End Minute', 0, { min: 0, max: 59, step: 1 });

  var showInternalHL = ctx.input('Show Internal H/L', false);
  var pivotStrength  = ctx.input('Pivot Strength (bars L/R)', 3, { min: 2, max: 10, step: 1 });
  var internalColor  = ctx.input('Internal H/L Color', '#b388ff');

  var biasHTF      = ctx.input('Bias HTF Timeframe', '1H');
  var biasDeltaLen = ctx.input('Bias Delta Lookback', 20, { min: 5, max: 100, step: 1 });
  var biasRSILen   = ctx.input('Bias RSI Period', 14, { min: 5, max: 50, step: 1 });

  var bullColor      = ctx.input('Bullish IFVG Color', '#26a69a');
  var bearColor      = ctx.input('Bearish IFVG Color', '#ef5350');
  var mitigatedColor = ctx.input('Mitigated IFVG Color', '#555555');
  var neutralColor   = '#ffeb3b';
  var levelColor     = ctx.input('Session Level Color', '#ffeb3b');
  var showLevels       = ctx.input('Show Session Levels', true);
  var showSweepArrows  = ctx.input('Show Sweep Arrows', true);
  var showLabels       = ctx.input('Show IFVG Labels', true);
  var showTable        = ctx.input('Show Status Table', true);

  // ═══════════════════════════════════════════════════════════════════
  // CONSTANTS
  // ═══════════════════════════════════════════════════════════════════
  var tickSize      = (ctx.syminfo && ctx.syminfo.tickSize) ? ctx.syminfo.tickSize : 0.25;
  var minSweepPrice = minSweepTicks * tickSize;
  var minGapPrice   = minGapTicks * tickSize;
  var cooldownMs    = cooldownHrs * 60 * 60 * 1000;
  var last          = bars.length - 1;
  var confirmed     = last - 1; // last CLOSED bar — live bar is NEVER used for detection

  if (confirmed < 2) return;

  // ═══════════════════════════════════════════════════════════════════
  // STATE INIT (persists across calls, per-strategy-instance)
  // ═══════════════════════════════════════════════════════════════════
  if (!ctx.state.firedIFVGs)                       ctx.state.firedIFVGs = {};
  if (ctx.state.lastEntryTs === undefined)         ctx.state.lastEntryTs = 0;
  if (ctx.state.lastEntrySessionKey === undefined) ctx.state.lastEntrySessionKey = '';
  if (ctx.state.killSwitchTripped === undefined)   ctx.state.killSwitchTripped = false;

  // ═══════════════════════════════════════════════════════════════════
  // HELPERS
  // ═══════════════════════════════════════════════════════════════════
  function inSession(barIndex, startHour, endHour) {
    var h = ctx.time.hourIn(barIndex, 'America/New_York');
    if (startHour < endHour) return h >= startHour && h < endHour;
    return h >= startHour || h < endHour;
  }

  function inIFVGWindow(barIndex) {
    if (!useIFVGTimeFilter) return true;
    var h = ctx.time.hourIn(barIndex, 'America/Los_Angeles');
    var m = ctx.time.minuteIn(barIndex, 'America/Los_Angeles');
    var t = h * 60 + m;
    var s = ifvgStartHour * 60 + ifvgStartMin;
    var e = ifvgEndHour * 60 + ifvgEndMin;
    if (s < e) return t >= s && t < e;
    return t >= s || t < e;
  }

  function hexRGBA(hex, alpha) {
    var r = parseInt(hex.slice(1, 3), 16);
    var g = parseInt(hex.slice(3, 5), 16);
    var b = parseInt(hex.slice(5, 7), 16);
    return 'rgba(' + r + ',' + g + ',' + b + ',' + alpha + ')';
  }

  function dt(type) { return type === 'NY' ? 'PD' : type; }

  // Session key for once-per-session dedup — a "session" runs from
  // sessionResetHour:Min PT one day to the same time the next day.
  function sessionKeyFor(barIndex) {
    var ts = +new Date(bars[barIndex].timestamp);
    var h  = ctx.time.hourIn(barIndex, 'America/Los_Angeles');
    var m  = ctx.time.minuteIn(barIndex, 'America/Los_Angeles');
    var localMin = h * 60 + m;
    var resetMin = sessionResetHour * 60 + sessionResetMin;
    var localMidnightMs = ts - localMin * 60 * 1000;
    if (localMin < resetMin) localMidnightMs -= 24 * 60 * 60 * 1000;
    return String(Math.floor(localMidnightMs / 86400000));
  }

  // ═══════════════════════════════════════════════════════════════════
  // KILL SWITCH — day-P&L floor and NY-close flatten
  // ═══════════════════════════════════════════════════════════════════
  var acct = ctx.strategy.account;
  if (dailyLossKill > 0 && acct && acct.dayPnl < -Math.abs(dailyLossKill) && !ctx.state.killSwitchTripped) {
    ctx.state.killSwitchTripped = true;
    ctx.log('🚨 DAILY LOSS KILL — flattening. dayPnl=' + acct.dayPnl);
    ctx.strategy.flattenAll();
  }
  if (flattenAtNYEnd && ctx.strategy.position && ctx.strategy.position.side !== 'flat') {
    var lastHour = ctx.time.hourIn(last, 'America/New_York');
    if (lastHour >= nyEnd) {
      ctx.log('NY end — flattening');
      ctx.strategy.flattenAll();
    }
  }

  // ╔══════════════════════════════════════════════════════════════╗
  // ║  LOOKBACK CUTOFF                                             ║
  // ╚══════════════════════════════════════════════════════════════╝
  var dayCount = 0, cutoffBar = 0;
  for (var i = confirmed; i >= 1; i--) {
    if (ctx.time.isNewSession(i)) {
      dayCount++;
      if (dayCount > lookbackDays) { cutoffBar = i; break; }
    }
  }

  // ╔══════════════════════════════════════════════════════════════╗
  // ║  PASS 1: SESSION BUILDING                                    ║
  // ╚══════════════════════════════════════════════════════════════╝
  var sessions = [];
  var curA = null, curL = null, curN = null;

  for (var i = cutoffBar; i <= confirmed; i++) {
    var inA  = inSession(i, asianStart, asianEnd);
    var inLn = inSession(i, londonStart, londonEnd);
    var inNy = inSession(i, nyStart, nyEnd);

    if (inA) {
      if (!curA) curA = { high: bars[i].high, low: bars[i].low, highBar: i, lowBar: i };
      else {
        if (bars[i].high > curA.high) { curA.high = bars[i].high; curA.highBar = i; }
        if (bars[i].low < curA.low)   { curA.low  = bars[i].low;  curA.lowBar  = i; }
      }
    } else if (curA) {
      if (sweepAsian) sessions.push({ type: 'Asia', high: curA.high, low: curA.low, endBar: i - 1, highBar: curA.highBar, lowBar: curA.lowBar });
      curA = null;
    }

    if (inLn) {
      if (!curL) curL = { high: bars[i].high, low: bars[i].low, highBar: i, lowBar: i };
      else {
        if (bars[i].high > curL.high) { curL.high = bars[i].high; curL.highBar = i; }
        if (bars[i].low < curL.low)   { curL.low  = bars[i].low;  curL.lowBar  = i; }
      }
    } else if (curL) {
      if (sweepLondon) sessions.push({ type: 'London', high: curL.high, low: curL.low, endBar: i - 1, highBar: curL.highBar, lowBar: curL.lowBar });
      curL = null;
    }

    if (inNy) {
      if (!curN) curN = { high: bars[i].high, low: bars[i].low, highBar: i, lowBar: i };
      else {
        if (bars[i].high > curN.high) { curN.high = bars[i].high; curN.highBar = i; }
        if (bars[i].low < curN.low)   { curN.low  = bars[i].low;  curN.lowBar  = i; }
      }
    } else if (curN) {
      if (sweepNY) sessions.push({ type: 'NY', high: curN.high, low: curN.low, endBar: i - 1, highBar: curN.highBar, lowBar: curN.lowBar });
      curN = null;
    }
  }

  // ╔══════════════════════════════════════════════════════════════╗
  // ║  PASS 2: SWEEP DETECTION                                     ║
  // ╚══════════════════════════════════════════════════════════════╝
  var sweeps = [];
  var sweepSet = {};

  for (var si = 0; si < sessions.length; si++) {
    var sess = sessions[si];
    for (var i2 = sess.endBar + 1; i2 <= confirmed; i2++) {
      var sk = sess.type + '-' + si;
      if (!sweepSet[sk + '-low']  && bars[i2].low  < sess.low  - minSweepPrice) {
        sweepSet[sk + '-low']  = true;
        sweeps.push({ bar: i2, side: 'low',  sessType: sess.type, level: sess.low,  sessIdx: si });
      }
      if (!sweepSet[sk + '-high'] && bars[i2].high > sess.high + minSweepPrice) {
        sweepSet[sk + '-high'] = true;
        sweeps.push({ bar: i2, side: 'high', sessType: sess.type, level: sess.high, sessIdx: si });
      }
    }
  }

  // ╔══════════════════════════════════════════════════════════════╗
  // ║  PASS 3: RAW FVG DETECTION                                   ║
  // ║  Detect ALL raw FVGs first so chain filter is accurate.      ║
  // ╚══════════════════════════════════════════════════════════════╝
  var rawFVGs = {};
  for (var i3 = cutoffBar + 2; i3 <= confirmed; i3++) {
    var c1 = i3 - 2;
    var beTop = bars[c1].low;
    var beBot = bars[i3].high;
    if (beTop - beBot >= minGapPrice) {
      rawFVGs['be-' + c1] = { dir: 'bear', c1: c1, c3: i3, top: beTop, bottom: beBot };
    }
    var buTop = bars[i3].low;
    var buBot = bars[c1].high;
    if (buTop - buBot >= minGapPrice) {
      rawFVGs['bu-' + c1] = { dir: 'bull', c1: c1, c3: i3, top: buTop, bottom: buBot };
    }
  }

  // ╔══════════════════════════════════════════════════════════════╗
  // ║  PASS 4: INVERSION + ENTRY (RR-based SL/TP)                  ║
  // ╚══════════════════════════════════════════════════════════════╝
  var ifvgs = {};
  var entries = {};

  for (var fk in rawFVGs) {
    var raw = rawFVGs[fk];
    var prefix = raw.dir === 'bear' ? 'be-' : 'bu-';
    // Chain filter — only the LAST FVG in a consecutive same-dir chain inverts.
    if (rawFVGs[prefix + (raw.c1 + 1)] || rawFVGs[prefix + (raw.c1 + 2)]) continue;

    var scanEnd = Math.min(raw.c3 + maxBarsForInversion, confirmed);
    for (var j = raw.c3 + 1; j <= scanEnd; j++) {
      var inverted = false;
      if (raw.dir === 'bear') {
        inverted = inversionMode === 'close_only' ? bars[j].close > raw.top : bars[j].high > raw.top;
      } else {
        inverted = inversionMode === 'close_only' ? bars[j].close < raw.bottom : bars[j].low < raw.bottom;
      }

      if (inverted && inIFVGWindow(j)) {
        var ifvgType = raw.dir === 'bear' ? 'bull' : 'bear';

        var postSweep = false;
        for (var swi = 0; swi < sweeps.length; swi++) {
          var sw = sweeps[swi];
          if (ifvgType === 'bull' && sw.side === 'low'  && sw.bar <= j && sw.bar >= raw.c1 - 20) { postSweep = true; break; }
          if (ifvgType === 'bear' && sw.side === 'high' && sw.bar <= j && sw.bar >= raw.c1 - 20) { postSweep = true; break; }
        }

        ifvgs[fk] = {
          type: ifvgType, startBar: raw.c1, endBar: raw.c3,
          invBar: j, top: raw.top, bottom: raw.bottom, postSweep: postSweep
        };

        if (showEntrySignals && (!onlyPostSweep || postSweep)) {
          var ep = bars[j].close;
          var sBuf = stopBuffer * tickSize;
          if (ifvgType === 'bull') {
            var sl = raw.bottom - sBuf;
            var rk = ep - sl;
            entries[fk] = { bar: j, type: 'bull', price: ep, sl: sl, tp: ep + (rk * entryRR), risk: rk };
          } else {
            var sl2 = raw.top + sBuf;
            var rk2 = sl2 - ep;
            entries[fk] = { bar: j, type: 'bear', price: ep, sl: sl2, tp: ep - (rk2 * entryRR), risk: rk2 };
          }
        }

        break; // first inversion wins for this FVG
      }
    }
  }

  // ╔══════════════════════════════════════════════════════════════╗
  // ║  PASS 5: RETEST + MITIGATION (display only)                  ║
  // ╚══════════════════════════════════════════════════════════════╝
  var mitigated = {};
  var retested = {};

  for (var fk2 in ifvgs) {
    var fvg = ifvgs[fk2];
    var ce = (fvg.top + fvg.bottom) / 2;
    for (var j2 = fvg.invBar + 1; j2 <= confirmed; j2++) {
      if (fvg.type === 'bull') {
        if (!retested[fk2] && bars[j2].low <= fvg.top && bars[j2].low >= fvg.bottom) retested[fk2] = { retestBar: j2 };
        if (bars[j2].close < ce) { mitigated[fk2] = { mitBar: j2 }; break; }
      } else {
        if (!retested[fk2] && bars[j2].high >= fvg.bottom && bars[j2].high <= fvg.top) retested[fk2] = { retestBar: j2 };
        if (bars[j2].close > ce) { mitigated[fk2] = { mitBar: j2 }; break; }
      }
    }
  }

  // ╔══════════════════════════════════════════════════════════════╗
  // ║  PASS 6: INTERNAL PIVOTS                                     ║
  // ╚══════════════════════════════════════════════════════════════╝
  var pivotH = {}, pivotL = {}, pivotHSwept = {}, pivotLSwept = {};

  if (showInternalHL) {
    var pStart = cutoffBar + pivotStrength;
    var pEnd   = confirmed - pivotStrength;
    for (var p = pStart; p <= pEnd; p++) {
      var isH = true;
      for (var s = 1; s <= pivotStrength; s++) {
        if (bars[p - s].high >= bars[p].high || bars[p + s].high >= bars[p].high) { isH = false; break; }
      }
      if (isH) pivotH['iH-' + p] = { bar: p, price: bars[p].high };

      var isLo = true;
      for (var s2 = 1; s2 <= pivotStrength; s2++) {
        if (bars[p - s2].low <= bars[p].low || bars[p + s2].low <= bars[p].low) { isLo = false; break; }
      }
      if (isLo) pivotL['iL-' + p] = { bar: p, price: bars[p].low };
    }

    for (var hk in pivotH) {
      var ph = pivotH[hk];
      for (var jp = ph.bar + pivotStrength + 1; jp <= confirmed; jp++) {
        if (bars[jp].high > ph.price) { pivotHSwept[hk] = { sweptBar: jp }; break; }
      }
    }
    for (var lk in pivotL) {
      var pl = pivotL[lk];
      for (var jq = pl.bar + pivotStrength + 1; jq <= confirmed; jq++) {
        if (bars[jq].low < pl.price) { pivotLSwept[lk] = { sweptBar: jq }; break; }
      }
    }
  }

  // ═══════════════════════════════════════════════════════════════════
  // EXECUTION — fires ctx.strategy.entry() on the confirmed bar
  // ═══════════════════════════════════════════════════════════════════
  var canEnter    = true;
  var blockReason = '';

  if (!enableTrading)                                                              { canEnter = false; blockReason = 'Trading disabled'; }
  else if (ctx.state.killSwitchTripped)                                            { canEnter = false; blockReason = 'Kill switch tripped'; }
  else if (ctx.strategy.position && ctx.strategy.position.side !== 'flat')         { canEnter = false; blockReason = 'Position open'; }

  var nowTs = +new Date(bars[last].timestamp);
  var currentSessionKey = sessionKeyFor(confirmed);
  if (canEnter && oneTradePerSession && ctx.state.lastEntrySessionKey === currentSessionKey) {
    canEnter = false;
    blockReason = 'Already traded this session';
  }
  if (canEnter && cooldownMs > 0 && ctx.state.lastEntryTs > 0) {
    var elapsed = nowTs - ctx.state.lastEntryTs;
    if (elapsed < cooldownMs) {
      canEnter = false;
      var hrsLeft = ((cooldownMs - elapsed) / 3600000).toFixed(1);
      blockReason = 'Cooldown (' + hrsLeft + 'h left)';
    }
  }

  if (canEnter) {
    for (var ek in entries) {
      var e = entries[ek];
      if (e.bar !== confirmed) continue;                    // fire only on the just-confirmed bar
      if (onlyPostSweep && !ifvgs[ek].postSweep) continue;
      if (ctx.state.firedIFVGs[ek]) continue;               // per-IFVG dedup

      var orderId = 'ifvg_' + ek + '_' + confirmed;
      var side = e.type === 'bull' ? 'long' : 'short';

      ctx.strategy.entry(orderId, side, {
        qty: contractQty,
        bracket: {
          stopLoss:   { price: e.sl },
          takeProfit: [{ price: e.tp, qty: contractQty }]
        }
      });

      ctx.state.firedIFVGs[ek]      = { ts: nowTs, side: side, entry: e.price, sl: e.sl, tp: e.tp, risk: e.risk };
      ctx.state.lastEntryTs         = nowTs;
      ctx.state.lastEntrySessionKey = currentSessionKey;

      ctx.log('✅ FIRED ' + side.toUpperCase() + ' @ ' + e.price.toFixed(2) +
              ' SL=' + e.sl.toFixed(2) + ' (' + (e.risk / tickSize).toFixed(0) + 't)' +
              ' TP=' + e.tp.toFixed(2) + ' (' + entryRR + 'R)' +
              ' session=' + currentSessionKey);
      break;
    }
  }

  // ═══════════════════════════════════════════════════════════════════
  // RENDER
  // ═══════════════════════════════════════════════════════════════════

  // ── Session Levels ──
  if (showLevels) {
    for (var ri = 0; ri < sessions.length; ri++) {
      var sess2 = sessions[ri];
      var sk2 = sess2.type + '-' + ri;
      var highSwept = !!sweepSet[sk2 + '-high'];
      ctx.line(sess2.highBar, sess2.high, last, sess2.high, {
        color: highSwept ? bearColor : levelColor, lineWidth: 1,
        lineStyle: highSwept ? 'solid' : 'dashed',
        text: dt(sess2.type) + ' High' + (highSwept ? ' ✓' : '')
      });
      var lowSwept = !!sweepSet[sk2 + '-low'];
      ctx.line(sess2.lowBar, sess2.low, last, sess2.low, {
        color: lowSwept ? bullColor : levelColor, lineWidth: 1,
        lineStyle: lowSwept ? 'solid' : 'dashed',
        text: dt(sess2.type) + ' Low' + (lowSwept ? ' ✓' : '')
      });
    }
  }

  // ── Sweep Arrows ──
  if (showSweepArrows) {
    for (var swi2 = 0; swi2 < sweeps.length; swi2++) {
      var sw2 = sweeps[swi2];
      if (sw2.side === 'low') {
        ctx.shape(sw2.bar, 'arrow_up',   { color: bullColor, location: 'belowBar', text: dt(sw2.sessType) + ' Low Sweep' });
      } else {
        ctx.shape(sw2.bar, 'arrow_down', { color: bearColor, location: 'aboveBar', text: dt(sw2.sessType) + ' High Sweep' });
      }
    }
  }

  // ── IFVG Boxes + CE Lines + Entry Signals ──
  var renderList = [];
  for (var rk in ifvgs) {
    var isMit = !!mitigated[rk];
    if (!showMitigated && isMit) continue;
    renderList.push({ key: rk, fvg: ifvgs[rk], mitigated: isMit, retested: !!retested[rk] });
  }
  // Insertion sort by startBar. Do NOT convert to Array.prototype.sort
  // with an inline callback — the SDK security validator's case-insensitive
  // regex for the Function constructor also flags lowercase callback
  // expressions and rejects the whole script.
  for (var sortI = 1; sortI < renderList.length; sortI++) {
    var sortKey = renderList[sortI];
    var sortJ = sortI - 1;
    while (sortJ >= 0 && renderList[sortJ].fvg.startBar > sortKey.fvg.startBar) {
      renderList[sortJ + 1] = renderList[sortJ];
      sortJ--;
    }
    renderList[sortJ + 1] = sortKey;
  }
  if (renderList.length > maxIFVGs) renderList = renderList.slice(renderList.length - maxIFVGs);

  var latestActiveIFVG = null;

  for (var di = 0; di < renderList.length; di++) {
    var item = renderList[di];
    var fvg3 = item.fvg;
    var fk3 = item.key;
    var isBull = fvg3.type === 'bull';
    var isMit2 = item.mitigated;
    var isRet = item.retested;
    var color = isMit2 ? mitigatedColor : (isBull ? bullColor : bearColor);
    var gapTicks = ((fvg3.top - fvg3.bottom) / tickSize).toFixed(0);
    var wasFired = !!ctx.state.firedIFVGs[fk3];

    var label = '';
    if (showLabels) {
      label = 'IFVG ' + (isBull ? '▲' : '▼') + ' ' + gapTicks + 't'
        + (fvg3.postSweep ? ' [SW]' : '') + (wasFired ? ' [FIRED]' : (isRet && !isMit2 ? ' [RET]' : '')) + (isMit2 ? ' [MIT]' : '');
    }

    ctx.box(fvg3.startBar, fvg3.top, fvg3.invBar, fvg3.bottom, {
      fillColor: hexRGBA(color, isMit2 ? 0.08 : 0.2),
      borderColor: color, extend: 'right', text: label
    });

    if (isRet && !isMit2) {
      var rb = retested[fk3].retestBar;
      ctx.shape(rb, isBull ? 'arrow_up' : 'arrow_down', {
        color: color, location: isBull ? 'belowBar' : 'aboveBar', text: 'IFVG Retest'
      });
    }

    if (showCELine && !isMit2) {
      var ceVal = (fvg3.top + fvg3.bottom) / 2;
      ctx.line(fvg3.startBar, ceVal, last, ceVal, {
        color: color, lineWidth: 1, lineStyle: 'dotted', text: 'CE ' + ceVal.toFixed(2)
      });
    }

    var entry = entries[fk3];
    if (entry && showEntrySignals && !isMit2) {
      var isLong = entry.type === 'bull';
      ctx.shape(entry.bar, 'diamond', {
        color: entryColor, location: isLong ? 'belowBar' : 'aboveBar',
        text: (isLong ? '🔵 LONG' : '🔴 SHORT') + ' @ ' + entry.price.toFixed(2) + (wasFired ? ' [FIRED]' : '')
      });

      if (showEntryLines) {
        var lineEnd = Math.min(entry.bar + 30, last);
        ctx.line(entry.bar, entry.price, lineEnd, entry.price, {
          color: entryColor, lineWidth: 2, lineStyle: 'solid',
          text: 'Entry ' + entry.price.toFixed(2)
        });
        ctx.line(entry.bar, entry.sl, lineEnd, entry.sl, {
          color: bearColor, lineWidth: 1, lineStyle: 'dashed',
          text: 'SL ' + entry.sl.toFixed(2) + ' (' + (entry.risk / tickSize).toFixed(0) + 't)'
        });
        ctx.line(entry.bar, entry.tp, lineEnd, entry.tp, {
          color: isLong ? bullColor : bearColor, lineWidth: 1, lineStyle: 'dashed',
          text: 'TP ' + entry.tp.toFixed(2) + ' (' + entryRR + 'R)'
        });
      }
    }

    if (!isMit2) latestActiveIFVG = { key: fk3, fvg: fvg3 };
  }

  // ── Internal Pivot Lines ──
  if (showInternalHL) {
    for (var hk2 in pivotH) {
      var ph2 = pivotH[hk2];
      var swept = pivotHSwept[hk2];
      var drawEnd = swept ? swept.sweptBar : last;
      var col = swept ? bearColor : internalColor;
      ctx.line(ph2.bar, ph2.price, drawEnd, ph2.price, {
        color: col, lineWidth: 1, lineStyle: swept ? 'solid' : 'dashed',
        text: 'iH ' + ph2.price.toFixed(2) + (swept ? ' ✗' : '')
      });
    }
    for (var lk2 in pivotL) {
      var pl2 = pivotL[lk2];
      var swept2 = pivotLSwept[lk2];
      var drawEnd2 = swept2 ? swept2.sweptBar : last;
      var col2 = swept2 ? bullColor : internalColor;
      ctx.line(pl2.bar, pl2.price, drawEnd2, pl2.price, {
        color: col2, lineWidth: 1, lineStyle: swept2 ? 'solid' : 'dashed',
        text: 'iL ' + pl2.price.toFixed(2) + (swept2 ? ' ✗' : '')
      });
    }
  }

  // ═══════════════════════════════════════════════════════════════════
  // STATUS TABLE
  // ═══════════════════════════════════════════════════════════════════
  if (!showTable) return;

  // ── HTF Bias ──
  var htf = ctx.request(biasHTF);
  var htfBias = 0;
  if (htf && htf.length >= 2) {
    var htfBar = htf[htf.length - 2];
    if (htfBar) {
      if (htfBar.close > htfBar.open) htfBias = 1;
      else if (htfBar.close < htfBar.open) htfBias = -1;
    }
  }

  // ── Delta Bias ──
  var delta = ctx.footprint && ctx.footprint.delta;
  var cumDelta = 0, deltaBias = 0;
  if (delta) {
    var dStart = Math.max(0, last - biasDeltaLen);
    for (var d = dStart; d <= last; d++) { if (delta[d] !== null) cumDelta += delta[d]; }
    if (cumDelta > 0) deltaBias = 1; else if (cumDelta < 0) deltaBias = -1;
  }

  // ── EMA Bias ──
  var ema9 = ctx.ta.ema(ctx.price.close, 9);
  var ema21 = ctx.ta.ema(ctx.price.close, 21);
  var emaBias = 0;
  if (ema9[last] !== null && ema21[last] !== null) {
    if (ema9[last] > ema21[last]) emaBias = 1;
    else if (ema9[last] < ema21[last]) emaBias = -1;
  }

  // ── RSI Bias ──
  var rsi = ctx.ta.rsi(ctx.price.close, biasRSILen);
  var rsiBias = 0;
  if (rsi[last] !== null) {
    if (rsi[last] > 55) rsiBias = 1;
    else if (rsi[last] < 45) rsiBias = -1;
  }

  // ── POC Bias ──
  var poc = ctx.footprint && ctx.footprint.poc;
  var pocBias = 0;
  if (poc && poc[last] !== null) {
    if (bars[last].close > poc[last]) pocBias = 1;
    else if (bars[last].close < poc[last]) pocBias = -1;
  }

  var sweepCount = sweeps.length;

  // ── Composite Bias ──
  var biasScore = htfBias + deltaBias + emaBias + rsiBias + pocBias;
  var bias = 'Neutral', biasColor2 = neutralColor;
  if (biasScore >= 3) { bias = 'Bullish'; biasColor2 = bullColor; }
  else if (biasScore <= -3) { bias = 'Bearish'; biasColor2 = bearColor; }

  // ── Trade Status ──
  var tradeStatus = enableTrading ? 'LIVE' : 'DISABLED';
  var tradeColor  = enableTrading ? (ctx.state.killSwitchTripped ? bearColor : bullColor) : '#888';
  if (ctx.state.killSwitchTripped) tradeStatus = 'KILL SWITCH';

  // ── Position ──
  var pos = ctx.strategy.position;
  var posText = 'Flat', posColor = '#888';
  if (pos && pos.side !== 'flat') {
    posText = pos.side.toUpperCase() + ' ' + pos.size + ' @ ' + pos.avgPrice.toFixed(2);
    posColor = pos.side === 'long' ? bullColor : bearColor;
  }

  // ── Day P&L ──
  var pnlText = '—', pnlColor = '#888';
  if (acct) {
    pnlText = '$' + acct.dayPnl.toFixed(2);
    pnlColor = acct.dayPnl >= 0 ? bullColor : bearColor;
  }

  // ── IFVG Status ──
  var ifvgStatus = 'None', ifvgStatusColor = '#888';
  if (latestActiveIFVG) {
    var isRetested = !!retested[latestActiveIFVG.key];
    ifvgStatus = isRetested ? 'Retested ✓' : (latestActiveIFVG.fvg.postSweep ? 'Active [Post-Sweep]' : 'Active');
    ifvgStatusColor = latestActiveIFVG.fvg.type === 'bull' ? bullColor : bearColor;
  }

  var ifvgDir = '—', ifvgDirColor = '#888';
  if (latestActiveIFVG) {
    ifvgDir = latestActiveIFVG.fvg.type === 'bull' ? 'Bullish ▲' : 'Bearish ▼';
    ifvgDirColor = latestActiveIFVG.fvg.type === 'bull' ? bullColor : bearColor;
  }

  // ── Entry ──
  var entryText = '—', entryTextColor = '#888', slText = '—', tpText = '—';
  if (latestActiveIFVG) {
    var latestEntry = entries[latestActiveIFVG.key];
    if (latestEntry) {
      var isLong2 = latestEntry.type === 'bull';
      entryText = (isLong2 ? 'LONG' : 'SHORT') + ' @ ' + latestEntry.price.toFixed(2);
      entryTextColor = isLong2 ? bullColor : bearColor;
      slText = latestEntry.sl.toFixed(2) + ' (' + (latestEntry.risk / tickSize).toFixed(0) + 't)';
      tpText = latestEntry.tp.toFixed(2) + ' (' + entryRR + 'R)';
    }
  }

  // ── Render Table ──
  var t = ctx.table('top_right', 2, 12, {
    bgcolor: 'rgba(13,17,28,0.92)', borderColor: 'rgba(255,255,255,0.08)',
    fontSize: 11, cellPadding: 6, paddingTop: 80
  });

  ctx.cell(t, 0, 0, 'IFVG AUTO', { textColor: '#fff', bgcolor: 'rgba(255,255,255,0.06)', fontSize: 12 });
  ctx.cell(t, 0, 1, 'v11',       { textColor: '#fff', bgcolor: 'rgba(255,255,255,0.06)', fontSize: 12 });

  ctx.cell(t, 1, 0, 'Trading',   { textColor: '#aaa' });
  ctx.cell(t, 1, 1, tradeStatus, { textColor: tradeColor });

  ctx.cell(t, 2, 0, 'Position',  { textColor: '#aaa' });
  ctx.cell(t, 2, 1, posText,     { textColor: posColor });

  ctx.cell(t, 3, 0, 'Day P&L',   { textColor: '#aaa' });
  ctx.cell(t, 3, 1, pnlText,     { textColor: pnlColor });

  ctx.cell(t, 4, 0, 'Sweeps',    { textColor: '#aaa' });
  ctx.cell(t, 4, 1, sweepCount > 0 ? 'Yes (' + sweepCount + ')' : 'No', { textColor: sweepCount > 0 ? bullColor : '#888' });

  ctx.cell(t, 5, 0, 'Bias',      { textColor: '#aaa' });
  ctx.cell(t, 5, 1, bias,        { textColor: biasColor2 });

  ctx.cell(t, 6, 0, 'IFVG',      { textColor: '#aaa' });
  ctx.cell(t, 6, 1, ifvgStatus,  { textColor: ifvgStatusColor });

  ctx.cell(t, 7, 0, 'Direction', { textColor: '#aaa' });
  ctx.cell(t, 7, 1, ifvgDir,     { textColor: ifvgDirColor });

  ctx.cell(t, 8, 0, '── ENTRY ──', { textColor: entryColor, bgcolor: 'rgba(255,255,255,0.04)', fontSize: 11 });
  ctx.cell(t, 8, 1, entryText,   { textColor: entryTextColor, bgcolor: 'rgba(255,255,255,0.04)', fontSize: 11 });

  ctx.cell(t, 9, 0, 'Stop Loss', { textColor: '#aaa' });
  ctx.cell(t, 9, 1, slText,      { textColor: bearColor });

  ctx.cell(t, 10, 0, 'Take Profit', { textColor: '#aaa' });
  ctx.cell(t, 10, 1, tpText,        { textColor: bullColor });

  ctx.cell(t, 11, 0, 'Block',       { textColor: '#aaa' });
  ctx.cell(t, 11, 1, blockReason || (canEnter ? 'Ready' : '—'), { textColor: canEnter ? bullColor : '#888' });
}