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
Comments (0)
0/2000
Loading comments…
Source code
// ICT IFVG Sweep Strategy — SXVNT SDK (v8)
//
// ZERO-REPAINT / ZERO-SHIFT ARCHITECTURE:
// - ALL detection uses ONLY confirmed (closed) bars — never the live bar
// - Recalculated from scratch each call using CURRENT bar indices
// - No bar indices stored in ctx.state → immune to bar array rotation
// - Confirmed bar data never changes → same bars always produce same signals
// - Multi-pass: FVGs detected first, then inversions (chain filter is accurate)
function calculate(bars, ctx) {
// ═══ Inputs ═══
var lookbackDays = ctx.input('Lookback Days', 1, { 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', 'wick_or_close', { options: ['close_only', 'wick_or_close'] });
var showMitigated = ctx.input('Show Mitigated IFVGs', true);
var showCELine = ctx.input('Display Consequent Encroachment', true);
var maxIFVGs = ctx.input('Max IFVGs Displayed', 10, { min: 1, max: 50, step: 1 });
var showEntrySignals = ctx.input('Show Entry Signals', true);
var onlyPostSweep = ctx.input('Entry Only After Sweep', true);
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', true);
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);
var tickSize = 0.25;
var minSweepPrice = minSweepTicks * tickSize;
var minGapPrice = minGapTicks * tickSize;
var last = bars.length - 1;
var confirmed = last - 1; // last CLOSED bar — live bar is NEVER used for detection
if (confirmed < 2) return;
// ═══ 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; }
// ╔══════════════════════════════════════════════════════════════╗
// ║ 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 (confirmed bars only) ║
// ╚══════════════════════════════════════════════════════════════╝
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 (confirmed bars only) ║
// ╚══════════════════════════════════════════════════════════════╝
var sweeps = []; // array of { bar, side, sessType, level, sessIdx }
var sweepSet = {}; // quick lookup: 'Type-endBar-side' → true
for (var si = 0; si < sessions.length; si++) {
var sess = sessions[si];
for (var i = sess.endBar + 1; i <= confirmed; i++) {
var sk = sess.type + '-' + si;
if (!sweepSet[sk + '-low'] && bars[i].low < sess.low - minSweepPrice) {
sweepSet[sk + '-low'] = true;
sweeps.push({ bar: i, side: 'low', sessType: sess.type, level: sess.low, sessIdx: si });
}
if (!sweepSet[sk + '-high'] && bars[i].high > sess.high + minSweepPrice) {
sweepSet[sk + '-high'] = true;
sweeps.push({ bar: i, side: 'high', sessType: sess.type, level: sess.high, sessIdx: si });
}
}
}
// ╔══════════════════════════════════════════════════════════════╗
// ║ PASS 3: RAW FVG DETECTION (confirmed bars only) ║
// ║ Detect ALL raw FVGs first so chain filter is accurate ║
// ╚══════════════════════════════════════════════════════════════╝
var rawFVGs = {}; // key: 'dir-c1' → { dir, c1, c3, top, bottom }
for (var i = cutoffBar + 2; i <= confirmed; i++) {
var c1 = i - 2;
// Bearish FVG: gap between c1.low and c3.high (price dropped through gap)
var beTop = bars[c1].low;
var beBot = bars[i].high;
if (beTop - beBot >= minGapPrice) {
rawFVGs['be-' + c1] = { dir: 'bear', c1: c1, c3: i, top: beTop, bottom: beBot };
}
// Bullish FVG: gap between c3.low and c1.high (price rose through gap)
var buTop = bars[i].low;
var buBot = bars[c1].high;
if (buTop - buBot >= minGapPrice) {
rawFVGs['bu-' + c1] = { dir: 'bull', c1: c1, c3: i, top: buTop, bottom: buBot };
}
}
// ╔══════════════════════════════════════════════════════════════╗
// ║ PASS 4: INVERSION + ENTRY DETECTION (confirmed bars only) ║
// ║ Chain filter uses complete rawFVGs from Pass 3 ║
// ╚══════════════════════════════════════════════════════════════╝
var ifvgs = {}; // key → { type, startBar, endBar, invBar, top, bottom, postSweep }
var entries = {}; // key → { bar, type, price, sl, tp, risk }
for (var fk in rawFVGs) {
var raw = rawFVGs[fk];
// Chain filter: skip if a same-direction FVG exists at c1+1 or c1+2
// Only the LAST in a consecutive chain should invert
var prefix = raw.dir === 'bear' ? 'be-' : 'bu-';
if (rawFVGs[prefix + (raw.c1 + 1)] || rawFVGs[prefix + (raw.c1 + 2)]) continue;
// Scan the inversion window: bars from c3+1 to c3+maxBarsForInversion
var scanEnd = Math.min(raw.c3 + maxBarsForInversion, confirmed);
for (var j = raw.c3 + 1; j <= scanEnd; j++) {
// Check inversion
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';
// Check if any sweep preceded this inversion
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; }
}
// Lock IFVG
ifvgs[fk] = {
type: ifvgType, startBar: raw.c1, endBar: raw.c3,
invBar: j, top: raw.top, bottom: raw.bottom, postSweep: postSweep
};
// Lock entry (uses confirmed bar j's close — frozen value)
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 (confirmed bars only) ║
// ╚══════════════════════════════════════════════════════════════╝
var mitigated = {}; // key → { mitBar }
var retested = {}; // key → { retestBar }
for (var fk2 in ifvgs) {
var fvg = ifvgs[fk2];
var ce = (fvg.top + fvg.bottom) / 2;
for (var j = fvg.invBar + 1; j <= confirmed; j++) {
if (fvg.type === 'bull') {
if (!retested[fk2] && bars[j].low <= fvg.top && bars[j].low >= fvg.bottom) {
retested[fk2] = { retestBar: j };
}
if (bars[j].close < ce) {
mitigated[fk2] = { mitBar: j };
break; // once mitigated, stop scanning
}
} else {
if (!retested[fk2] && bars[j].high >= fvg.bottom && bars[j].high <= fvg.top) {
retested[fk2] = { retestBar: j };
}
if (bars[j].close > ce) {
mitigated[fk2] = { mitBar: j };
break;
}
}
}
}
// ╔══════════════════════════════════════════════════════════════╗
// ║ PASS 6: INTERNAL PIVOTS (confirmed bars only) ║
// ╚══════════════════════════════════════════════════════════════╝
var pivotH = {}; // 'iH-bar' → { bar, price }
var pivotL = {}; // 'iL-bar' → { bar, price }
var pivotHSwept = {}; // 'iH-bar' → { sweptBar }
var pivotLSwept = {}; // 'iL-bar' → { sweptBar }
if (showInternalHL) {
// Detect pivots (need pivotStrength bars on each side)
var pStart = cutoffBar + pivotStrength;
var pEnd = confirmed - pivotStrength;
for (var p = pStart; p <= pEnd; p++) {
// Swing high
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 };
// Swing low
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 };
}
// Pivot sweep detection
for (var hk in pivotH) {
var ph = pivotH[hk];
for (var j = ph.bar + pivotStrength + 1; j <= confirmed; j++) {
if (bars[j].high > ph.price) {
pivotHSwept[hk] = { sweptBar: j };
break;
}
}
}
for (var lk in pivotL) {
var pl = pivotL[lk];
for (var j2 = pl.bar + pivotStrength + 1; j2 <= confirmed; j2++) {
if (bars[j2].low < pl.price) {
pivotLSwept[lk] = { sweptBar: j2 };
break;
}
}
}
}
// ╔══════════════════════════════════════════════════════════════╗
// ║ RENDER — Uses fresh indices computed above ║
// ║ No stale state, no shifting — all indices are current ║
// ╚══════════════════════════════════════════════════════════════╝
// ── Session Levels ──
if (showLevels) {
for (var ri = 0; ri < sessions.length; ri++) {
var sess = sessions[ri];
var sk = sess.type + '-' + ri;
var highSwept = !!sweepSet[sk + '-high'];
ctx.line(sess.highBar, sess.high, last, sess.high, {
color: highSwept ? bearColor : levelColor, lineWidth: 1,
lineStyle: highSwept ? 'solid' : 'dashed',
text: dt(sess.type) + ' High' + (highSwept ? ' \u2713' : '')
});
var lowSwept = !!sweepSet[sk + '-low'];
ctx.line(sess.lowBar, sess.low, last, sess.low, {
color: lowSwept ? bullColor : levelColor, lineWidth: 1,
lineStyle: lowSwept ? 'solid' : 'dashed',
text: dt(sess.type) + ' Low' + (lowSwept ? ' \u2713' : '')
});
}
}
// ── 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] });
}
renderList.sort(function(a, b) { return a.fvg.startBar - b.fvg.startBar; });
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);
// Label
var label = '';
if (showLabels) {
label = 'IFVG ' + (isBull ? '\u25B2' : '\u25BC') + ' ' + gapTicks + 't'
+ (fvg3.postSweep ? ' [SW]' : '') + (isRet && !isMit2 ? ' [RET]' : '') + (isMit2 ? ' [MIT]' : '');
}
// Box
ctx.box(fvg3.startBar, fvg3.top, fvg3.invBar, fvg3.bottom, {
fillColor: hexRGBA(color, isMit2 ? 0.08 : 0.2),
borderColor: color, extend: 'right', text: label
});
// Retest arrow
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'
});
}
// CE line
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)
});
}
// Entry signal
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 ? '\uD83D\uDD35 LONG' : '\uD83D\uDD34 SHORT') + ' @ ' + entry.price.toFixed(2)
});
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 ? ' \u2717' : '')
});
}
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 ? ' \u2717' : '')
});
}
}
// ╔══════════════════════════════════════════════════════════════╗
// ║ STATUS TABLE ║
// ║ Bias uses live data — that's correct (bias is a live ║
// ║ indicator, not a frozen signal) ║
// ╚══════════════════════════════════════════════════════════════╝
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.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.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;
}
// ── Sweep Count ──
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; }
// ── IFVG Status ──
var ifvgStatus = 'None', ifvgStatusColor = '#888';
if (latestActiveIFVG) {
var isRetested = !!retested[latestActiveIFVG.key];
ifvgStatus = isRetested ? 'Retested \u2713' : (latestActiveIFVG.fvg.postSweep ? 'Active [Post-Sweep]' : 'Active');
ifvgStatusColor = latestActiveIFVG.fvg.type === 'bull' ? bullColor : bearColor;
}
var ifvgDir = '\u2014', ifvgDirColor = '#888';
if (latestActiveIFVG) {
ifvgDir = latestActiveIFVG.fvg.type === 'bull' ? 'Bullish \u25B2' : 'Bearish \u25BC';
ifvgDirColor = latestActiveIFVG.fvg.type === 'bull' ? bullColor : bearColor;
}
// ── Target ──
var targetText = '\u2014', targetColor = '#888';
var currentPrice = bars[last].close;
if (bias === 'Bullish') {
var nd = Infinity;
for (var ti = 0; ti < sessions.length; ti++) {
var tSess = sessions[ti];
var tsk = tSess.type + '-' + ti;
if (!sweepSet[tsk + '-high'] && tSess.high > currentPrice) {
var tDist = tSess.high - currentPrice;
if (tDist < nd) { nd = tDist; targetText = dt(tSess.type) + ' High (' + tSess.high.toFixed(2) + ')'; targetColor = bullColor; }
}
}
} else if (bias === 'Bearish') {
var nd2 = Infinity;
for (var ti2 = 0; ti2 < sessions.length; ti2++) {
var tSess2 = sessions[ti2];
var tsk2 = tSess2.type + '-' + ti2;
if (!sweepSet[tsk2 + '-low'] && tSess2.low < currentPrice) {
var tDist2 = currentPrice - tSess2.low;
if (tDist2 < nd2) { nd2 = tDist2; targetText = dt(tSess2.type) + ' Low (' + tSess2.low.toFixed(2) + ')'; targetColor = bearColor; }
}
}
}
// ── Breakeven ──
var beText = '\u2014', beColor = '#888';
if (latestActiveIFVG && showInternalHL) {
var beFlagged = false;
if (latestActiveIFVG.fvg.type === 'bull') {
for (var blk in pivotL) {
var bpl = pivotL[blk];
if (bpl.bar > latestActiveIFVG.fvg.invBar && pivotLSwept[blk]) { beFlagged = true; break; }
}
} else {
for (var bhk in pivotH) {
var bph = pivotH[bhk];
if (bph.bar > latestActiveIFVG.fvg.invBar && pivotHSwept[bhk]) { beFlagged = true; break; }
}
}
if (beFlagged) { beText = 'Move to BE \u26A0'; beColor = '#ff9800'; }
else { beText = 'Hold'; beColor = latestActiveIFVG.fvg.type === 'bull' ? bullColor : bearColor; }
}
// ── Entry ──
var entryText = '\u2014', entryTextColor = '#888', slText = '\u2014', tpText = '\u2014';
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, 11, {
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 SWEEP', { textColor: '#fff', bgcolor: 'rgba(255,255,255,0.06)', fontSize: 12 });
ctx.cell(t, 0, 1, 'STATUS', { textColor: '#fff', bgcolor: 'rgba(255,255,255,0.06)', fontSize: 12 });
ctx.cell(t, 1, 0, 'Sweeps', { textColor: '#aaa' });
ctx.cell(t, 1, 1, sweepCount > 0 ? 'Yes (' + sweepCount + ')' : 'No', { textColor: sweepCount > 0 ? bullColor : '#888' });
ctx.cell(t, 2, 0, 'Bias', { textColor: '#aaa' });
ctx.cell(t, 2, 1, bias, { textColor: biasColor2 });
ctx.cell(t, 3, 0, 'IFVG', { textColor: '#aaa' });
ctx.cell(t, 3, 1, ifvgStatus, { textColor: ifvgStatusColor });
ctx.cell(t, 4, 0, 'Direction', { textColor: '#aaa' });
ctx.cell(t, 4, 1, ifvgDir, { textColor: ifvgDirColor });
ctx.cell(t, 5, 0, 'Post-Sweep', { textColor: '#aaa' });
var psText = latestActiveIFVG ? (latestActiveIFVG.fvg.postSweep ? 'Yes \u2713' : 'No') : '\u2014';
var psColor = latestActiveIFVG && latestActiveIFVG.fvg.postSweep ? bullColor : '#888';
ctx.cell(t, 5, 1, psText, { textColor: psColor });
ctx.cell(t, 6, 0, '\u2500\u2500 ENTRY \u2500\u2500', { textColor: entryColor, bgcolor: 'rgba(255,255,255,0.04)', fontSize: 11 });
ctx.cell(t, 6, 1, entryText, { textColor: entryTextColor, bgcolor: 'rgba(255,255,255,0.04)', fontSize: 11 });
ctx.cell(t, 7, 0, 'Stop Loss', { textColor: '#aaa' });
ctx.cell(t, 7, 1, slText, { textColor: bearColor });
ctx.cell(t, 8, 0, 'Take Profit', { textColor: '#aaa' });
ctx.cell(t, 8, 1, tpText, { textColor: bullColor });
ctx.cell(t, 9, 0, 'Breakeven', { textColor: '#aaa' });
ctx.cell(t, 9, 1, beText, { textColor: beColor });
ctx.cell(t, 10, 0, 'Target', { textColor: '#aaa' });
ctx.cell(t, 10, 1, targetText, { textColor: targetColor });
}