Inversion Fair Value Gaps (IFVG) Indicator
TSBuild25
===========
What It Does
The IFVG indicator identifies Fair Value Gaps (price imbalances) that undergo directional inversions – a key ICT trading concept. It tracks how institutional order flow transforms these gaps from bullish to bearish zones (or vice versa), revealing high-probability reversal and continuation areas.
How It Works
Fair Value Gaps occur when price moves so fast it leaves unfilled ranges – areas where institutional traders weren’t efficiently matched.
Inversions happen when price breaks through a gap, completely flipping its directional bias:
- Bullish gap → Price breaks below → Becomes bearish supply zone
- Bearish gap → Price breaks above → Becomes bullish demand zone
Visual Features
- Pre-Inversion: Original gap color (green=bullish, red=bearish)
- Post-Inversion: Opposite color after the flip occurs
- Midlines: Dotted gray lines mark gap centers
- Inversion Dots: Show exact transformation points
- Smart Display: Only shows recent inversions to keep charts clean
Signals
Generates trading alerts when price retests inverted zones:
- Bullish: Price retests top of bullish-inverted zone
- Bearish: Price retests bottom of bearish-inverted zone
Use for entries, alerts, scanners, and automated strategies.
Best Markets & Timeframes
Optimal Performance:
- Futures: NQ (Nasdaq), ES (S&P 500) – excellent institutional footprints
- Crypto: ETH, BTC, SOL – higher timeframes work best
Timeframe Guide:
- Traditional Markets: All timeframes during regular hours
- Crypto: 15M+ preferred due to fragmented liquidity
- Exception: Lower timeframes work during NY session (peak crypto liquidity)
Key Settings
- ATR Multiplier: Filters noise (0.25-0.5 for most markets)
- Wick vs Close Retest: Choose signal sensitivity
- Show Seeds: Optional pre-inversion gap display
Why These Markets?
High institutional participation creates clear FVG patterns and reliable inversions. The algorithm uses ATR filtering to ensure only significant imbalances are tracked, not random noise.
Educational Value
Learn institutional market mechanics through clear visual representation. Practice identifying patterns manually while getting precise algorithmic detection. Best used with confluence from order blocks, liquidity sweeps, and higher timeframe bias.
Open Source
Created by John Wolff
Completely free and open source. Modify, improve, and share as you wish.
Understanding the "why" behind price movements matters more than blindly following signals.
describe_indicator('Inversion Fair Value Gaps (IFVG)', 'price', { shortName: 'IFVG' });
// =============================================================================
// Inputs
const disp_num = input.number('Show Last', 5, { min: 1, max: 100 });
const useWickRetest = input.boolean('Use Wick for Signals', false);
const atr_multi = input.number('ATR Multiplier', 0.25, { min: 0, max: 10, step: 0.25 });
const proj_len = input.number('Projection Bars', 50, { min: 0, max: 200 });
// Optional features
const showSeeds = input.boolean('Show Pre-Inversion Seeds?', false);
const showInversionDots = input.boolean('Show Inversion Points?', true);
const extendZones = input.boolean('Extend Active Zones?', true);
const showMidlines = input.boolean('Show Zone Midlines?', true);
// NEW: optional color mode that paints zones by supply/demand location relative to current price
const colorByLocation = input.boolean('Color Zones by Location (Above Price = Red, Below = Green)?', false);
// Session gap filtering (for stocks)
const filterSessionGaps = input.boolean('Filter Session Gaps?', true);
const sessionGapThreshold = input.number('Session Gap Threshold %', 0.1, { min: 0, max: 5, step: 0.1 });
// Colors
const green = '#08998150'; // Bullish color with transparency
const red = '#f2364550'; // Bearish color with transparency
const midCol = '#787b86'; // Midline color
// Data validation
if (!high || !low || !open || !close || close.length < 3) {
console.error('Insufficient price data - need at least 3 bars');
}
// Body high/low calculations
const c_top = high.map((_, i) => {
if (open[i] == null || close[i] == null) return null;
return Math.max(open[i], close[i]);
});
const c_bot = low.map((_, i) => {
if (open[i] == null || close[i] == null) return null;
return Math.min(open[i], close[i]);
});
// ATR calculation
const atr200 = atr(200);
const minWidth = series_of(null);
let cumulativeRange = 0;
for (let i = 0; i < close.length; i++) {
if (high[i] != null && low[i] != null && high[i] >= low[i]) {
cumulativeRange += (high[i] - low[i]);
}
const atrValue = (atr200[i] !== null && atr200[i] !== undefined && atr200[i] > 0) ?
atr200[i] :
(cumulativeRange / (i + 1));
minWidth[i] = Math.max(0, atrValue * atr_multi);
}
// Data structures
const BUFFER = 100;
let bulls = [];
let bears = [];
let bullInv = [];
let bearInv = [];
// Signal series
const bullRet = series_of(false);
const bearRet = series_of(false);
const inversionDots = series_of(null);
// Optional seed visualization
const seedTop = series_of(null);
const seedBot = series_of(null);
// Simple drawing approach - pre-allocate series for zones
const maxZones = Math.min(disp_num * 2, 15); // Reasonable limit
const preTopSeries = [];
const preBotSeries = [];
const postTopSeries = [];
const postBotSeries = [];
const midSeries = [];
// Initialize drawing series
for (let k = 0; k < maxZones; k++) {
preTopSeries[k] = series_of(null);
preBotSeries[k] = series_of(null);
postTopSeries[k] = series_of(null);
postBotSeries[k] = series_of(null);
midSeries[k] = series_of(null);
}
// Debug counters
let debugCounters = {
bullFVGsDetected: 0,
bearFVGsDetected: 0,
bullInversions: 0, // Originally bullish FVGs that inverted (moved to bullInv)
bearInversions: 0, // Originally bearish FVGs that inverted (moved to bearInv)
bullSignals: 0,
bearSignals: 0,
zonesInvalidated: 0,
sessionGapsFiltered: 0
};
// Validation function
function validateFVG(fvg) {
return fvg &&
typeof fvg.left === 'number' && fvg.left >= 0 &&
typeof fvg.top0 === 'number' && typeof fvg.bot0 === 'number' &&
fvg.top0 > fvg.bot0 &&
Math.abs(fvg.mid0 - (fvg.top0 + fvg.bot0) / 2) < 0.0001;
}
// Helper function to detect session gaps (overnight gaps in stocks)
function isSessionGap(currentBar, prevBar) {
// If filtering is disabled, never consider it a session gap
if (!filterSessionGaps) return false;
// If open is significantly different from previous close, it's likely a session gap
const gapThreshold = sessionGapThreshold / 100; // Convert percentage to decimal
const prevClose = close[prevBar];
const currentOpen = open[currentBar];
if (prevClose == null || currentOpen == null) return false;
const gapSize = Math.abs(currentOpen - prevClose) / prevClose;
return gapSize > gapThreshold;
}
// Main processing loop
const dataLength = close.length;
for (let i = 2; i < dataLength; i++) {
// Data validation
if (high[i] == null || low[i] == null || close[i] == null || open[i] == null ||
high[i - 1] == null || low[i - 1] == null || close[i - 1] == null ||
high[i - 2] == null || low[i - 2] == null || close[i - 2] == null ||
high[i] < low[i] || high[i - 1] < low[i - 1] || high[i - 2] < low[i - 2]) {
continue;
}
try {
// Skip FVG detection if there are session gaps in the 3-bar pattern
// This prevents detecting overnight gaps as FVGs
if (isSessionGap(i, i - 1) || isSessionGap(i - 1, i - 2)) {
debugCounters.sessionGapsFiltered++;
continue;
}
// FVG Detection
// Only detect FVGs during continuous trading (no session gaps)
const bullFVG = (low[i] > high[i - 2]) && (close[i - 1] > high[i - 2]);
const bearFVG = (high[i] < low[i - 2]) && (close[i - 1] < low[i - 2]);
const bullGapWidth = bullFVG ? Math.abs(low[i] - high[i - 2]) : 0;
const bearGapWidth = bearFVG ? Math.abs(low[i - 2] - high[i]) : 0;
const currentMinWidth = minWidth[i] || 0;
const validBullFVG = bullFVG && bullGapWidth > currentMinWidth;
const validBearFVG = bearFVG && bearGapWidth > currentMinWidth;
// Optional: Mark seed FVGs
if (showSeeds) {
if (validBullFVG) { seedTop[i] = low[i]; seedBot[i] = high[i - 2]; }
if (validBearFVG) { seedTop[i] = low[i - 2]; seedBot[i] = high[i]; }
}
// Create FVG objects
if (validBullFVG) {
const newFVG = {
left: i - 1,
top0: low[i],
bot0: high[i - 2],
mid0: (low[i] + high[i - 2]) / 2,
dir: 1,
state: 0,
signaled: false,
alive: false,
endIndex: null,
xIndex: null
};
if (validateFVG(newFVG)) {
bulls.push(newFVG);
debugCounters.bullFVGsDetected++;
}
}
if (validBearFVG) {
const newFVG = {
left: i - 1,
top0: low[i - 2],
bot0: high[i],
mid0: (low[i - 2] + high[i]) / 2,
dir: -1,
state: 0,
signaled: false,
alive: false,
endIndex: null,
xIndex: null
};
if (validateFVG(newFVG)) {
bears.push(newFVG);
debugCounters.bearFVGsDetected++;
}
}
// Inversion Detection
const currentCBot = c_bot[i];
const currentCTop = c_top[i];
// FVG Management - Move to inversion arrays
// Bull FVG inversions - move to bullInv (originally bullish FVGs that will invert)
for (let j = bulls.length - 1; j >= 0; j--) {
const fvg = bulls[j];
if (fvg.dir === 1 && currentCBot < fvg.bot0) {
fvg.xIndex = i; // Mark inversion bar
bullInv.push(bulls.splice(j, 1)[0]); // Move to inversion array
debugCounters.bullInversions++;
inversionDots[i] = fvg.mid0;
}
}
// Bear FVG inversions - move to bearInv (originally bearish FVGs that will invert)
for (let j = bears.length - 1; j >= 0; j--) {
const fvg = bears[j];
if (fvg.dir === -1 && currentCTop > fvg.top0) {
fvg.xIndex = i; // Mark inversion bar
bearInv.push(bears.splice(j, 1)[0]); // Move to inversion array
debugCounters.bearInversions++;
inversionDots[i] = fvg.mid0;
}
}
// Inversion Management
// Process bullInv array (originally bullish FVGs that inverted)
for (let fvg of bullInv) {
if (fvg.state === 0) {
fvg.state = 1;
fvg.dir = -1; // Flip to bearish after inversion
fvg.alive = true;
fvg.endIndex = null;
}
if (fvg.state >= 1) {
// Extend the zone to current bar
}
}
// Process bearInv array (originally bearish FVGs that inverted)
for (let fvg of bearInv) {
if (fvg.state === 0) {
fvg.state = 1;
fvg.dir = 1; // Flip to bullish after inversion
fvg.alive = true;
fvg.endIndex = null;
}
if (fvg.state >= 1) {
// Extend the zone to current bar
}
}
// Signal Generation
if (i > 0) {
// Bearish signals from bullInv (originally bullish FVGs, now bearish)
for (let fvg of bullInv) {
if (fvg.dir === -1 && fvg.state === 1 && !fvg.signaled) {
const currentClose = close[i];
const prevPrice = useWickRetest ? high[i - 1] : close[i - 1];
if (currentClose < fvg.bot0 &&
prevPrice >= fvg.bot0 &&
prevPrice < fvg.top0) {
bearRet[i] = true;
fvg.signaled = true;
debugCounters.bearSignals++;
}
}
}
// Bullish signals from bearInv (originally bearish FVGs, now bullish)
for (let fvg of bearInv) {
if (fvg.dir === 1 && fvg.state === 1 && !fvg.signaled) {
const currentClose = close[i];
const prevPrice = useWickRetest ? low[i - 1] : close[i - 1];
if (currentClose > fvg.top0 &&
prevPrice <= fvg.top0 &&
prevPrice > fvg.bot0) {
bullRet[i] = true;
fvg.signaled = true;
debugCounters.bullSignals++;
}
}
}
}
// Post-inversion invalidation
for (let j = bullInv.length - 1; j >= 0; j--) {
const fvg = bullInv[j];
if (fvg.state >= 1 && fvg.alive !== false) {
const shouldInvalidate = (fvg.dir === -1 && currentCTop > fvg.top0) ||
(fvg.dir === 1 && currentCBot < fvg.bot0);
if (shouldInvalidate) {
fvg.alive = false;
fvg.endIndex = i;
debugCounters.zonesInvalidated++;
}
}
}
for (let j = bearInv.length - 1; j >= 0; j--) {
const fvg = bearInv[j];
if (fvg.state >= 1 && fvg.alive !== false) {
const shouldInvalidate = (fvg.dir === -1 && currentCTop > fvg.top0) ||
(fvg.dir === 1 && currentCBot < fvg.bot0);
if (shouldInvalidate) {
fvg.alive = false;
fvg.endIndex = i;
debugCounters.zonesInvalidated++;
}
}
}
// Memory management
if (bulls.length >= BUFFER) bulls.splice(0, bulls.length - BUFFER + 10);
if (bears.length >= BUFFER) bears.splice(0, bears.length - BUFFER + 10);
if (bullInv.length >= BUFFER) bullInv.splice(0, bullInv.length - BUFFER + 10);
if (bearInv.length >= BUFFER) bearInv.splice(0, bearInv.length - BUFFER + 10);
} catch (error) {
console.warn('IFVG processing error at bar ' + i + ':', error);
continue;
}
}
// Drawing logic - fill series for recent zones
// Display zones from all arrays (original + inverted)
// We need to include both original arrays (bulls, bears) and inversion arrays (bullInv, bearInv)
const recentBulls = bulls
.filter(z => z.xIndex !== undefined && z.xIndex !== null)
.slice(-disp_num);
const recentBears = bears
.filter(z => z.xIndex !== undefined && z.xIndex !== null)
.slice(-disp_num);
const recentBullInv = bullInv
.filter(z => z.xIndex !== undefined && z.xIndex !== null)
.slice(-disp_num);
const recentBearInv = bearInv
.filter(z => z.xIndex !== undefined && z.xIndex !== null)
.slice(-disp_num);
const allZones = recentBulls.concat(recentBears, recentBullInv, recentBearInv).sort((a, b) => a.xIndex - b.xIndex);
// Fill drawing series
for (let i = 0; i < close.length; i++) {
for (let k = 0; k < Math.min(allZones.length, maxZones); k++) {
const zone = allZones[k];
// Pre-inversion segment
if (i >= zone.left && i <= zone.xIndex) {
preTopSeries[k][i] = zone.top0;
preBotSeries[k][i] = zone.bot0;
}
// Post-inversion segment
let maxRight;
if (zone.endIndex !== null && zone.endIndex !== undefined) {
maxRight = zone.endIndex;
} else if (extendZones && zone.alive !== false) {
maxRight = Math.min(i + proj_len, close.length - 1);
} else {
maxRight = zone.xIndex;
}
if (i >= zone.xIndex && i <= maxRight) {
postTopSeries[k][i] = zone.top0;
postBotSeries[k][i] = zone.bot0;
}
// Midline
if (showMidlines) {
const totalRight = Math.max(zone.xIndex, maxRight);
if (i >= zone.left && i <= totalRight) {
midSeries[k][i] = zone.mid0;
}
}
}
}
// Visual rendering
if (showSeeds) {
const seedTopRef = paint(seedTop, { hidden: true, color: '#88888822' });
const seedBotRef = paint(seedBot, { hidden: true, color: '#88888822' });
fill(seedTopRef, seedBotRef, '#88888811');
}
// Paint zones
for (let k = 0; k < Math.min(allZones.length, maxZones); k++) {
const zone = allZones[k];
if (!zone) continue;
// Color assignment based on zone.dir
let preColor, postColor;
if (colorByLocation) {
// Override: color by location relative to current price
const currentPrice = close[close.length - 1];
const zoneAbovePrice = zone.bot0 > currentPrice;
preColor = zoneAbovePrice ? red : green;
postColor = zoneAbovePrice ? red : green;
} else {
// Standard logic: zone.dir determines colors
// For non-inverted zones, use original direction
// For inverted zones, use flipped direction (already stored in zone.dir)
if (zone.dir === -1) {
preColor = green; // dir=-1 zones are green pre-inversion
postColor = red; // dir=-1 zones are red post-inversion
} else {
preColor = red; // dir=1 zones are red pre-inversion
postColor = green; // dir=1 zones are green post-inversion
}
}
// Paint midline first
if (showMidlines) {
paint(midSeries[k], { color: midCol, style: 'dotted', thickness: 1 });
}
// Paint zone segments
const preTopRef = paint(preTopSeries[k], { hidden: true, color: preColor });
const preBotRef = paint(preBotSeries[k], { hidden: true, color: preColor });
const postTopRef = paint(postTopSeries[k], { hidden: true, color: postColor });
const postBotRef = paint(postBotSeries[k], { hidden: true, color: postColor });
// Fill zones
fill(preTopRef, preBotRef, preColor);
fill(postTopRef, postBotRef, postColor);
}
// Optional: Show inversion dots
if (showInversionDots) {
paint(inversionDots, { name: 'Inversion Points', color: midCol, style: 'dotted', thickness: 1 });
}
// Signal registration
register_signal(bearRet, 'Bearish IFVG Signal');
register_signal(bullRet, 'Bullish IFVG Signal');