Fair Value Gap

Stock chart of Apple Inc. showing price movements over several days, with marked zones indicating sellers and buyers fair value gaps.

Definition

Fair Value Gap is a situation caused by a sudden increase or decrease of a security price due to a large organization making a transaction for example. When such a condition occurs, the security price is expected temporally to move quickly in the opposite direction to fill in the gap after which it will continue going in the original direction.

we identify two types of FVG:

  1. Bullish FVG: It will happen when a transaction push the price up after which the chart will fall temporialy to fill in the gap and then continues to increase bullish.

  2. Bearish FVG: this will happen the reverse direction. A price gap will happen due to a large transaction pushing the price down. The chart will then temporialy rise back to fill the gap and after which the price will continue falling in the original direction.

Identifying FVG

To identify a Bullish FVG, we will need a 3-candle shape. The candle in the middle bottom value will be above the candle to the left higher value meantime the candle in the middle higher value will be below the right candle lower value. Shape can be identified as per the image below:

A stock trading chart shows a bullish engulfing pattern with the text 'Bullish FVG' in green at the top, indicating a buy signal, and a label pointing to a fair value gap.

To identify a bearish FVG, the same conditions must be applied in a 3-candle shape in the opposite direction. In this case, the candle on the left bottom value will be below the middle candle top value in the meantime the candle to the right high value will be above the bottom value of the middle candle. Shape can be identified from shape below:

A financial chart illustrating a bearish false value gap (FVG) with red candlesticks, labeled 'Bearish FVG,' alongside a drawing of a bear.

Usage

Once an FVG is identified on a chart, the gap should be drawn on the chard either manually or using auto-trading and identification tools. When the chart prices bounces back to fill the gap, it’s an indication that the price will continue moving again in the original direction. Let’s examine this for bullish and bearish FVG.

  • Bullish FVG:

Financial trading chart showing price movements with candlestick patterns over several days. Green and red candles illustrate upward and downward trends, with a highlighted green zone labeled "Buyers Fair Value Gap #165."

As you see in the graph, when the conditions for a bullish FVG are met, we draw a box of the bullish Gap (between top of first candle and bottom of third candle). As we see the chart price fall again in this gap, we take a Long position (or Buy) Stock as we expect the price will continue in the original bullish direction.

A Stop loss can be set quite below the gap lowest level. For Take Profit, FVG doesn’t give an indication about the take profit limit and for that probably other technical analysis tools may need to be used.

  • Bearish FVG

Candlestick chart showing a downtrend in Rheinmetall AG stock price over several hours with red and green candles, indicating declining and some rising prices.

As you see in the graph, a bearish gap was formed as the conditions for a bearish gap were met. We should then manually or with an automated trading tool draw the gap between the bottom of the left candle and the top of the right candle. Once the chart price rises again to fill the gap, we should take a short position (or sell). A reasonalble SL should be slightly above the top value of the gap. For Take profit, some other technical tool such as crossing averages can be deployed.

Pine Script V6

Snippet
//@version=6
 
indicator('Fair Value Gap Finder - Close Condition Mode', overlay = true)
 
 
 
// Inputs
 
useVolume = input.bool(false, 'Use Volume to determine FVGs (Recommended)', group = 'Volume')
 
vr = input.int(2, 'Volume must be X times average', step = 1, group = 'Volume')
 
vl = input.int(20, 'Volume moving average length', step = 1, group = 'Volume')
 
emaLength = input.int(20, 'EMA Length', step = 1, group = 'Trend Identification')
 
 
 
ol = input.int(60, 'Gap length', step = 10, group = 'Mind The Gap')
 
useAutoMinGapSize = input.bool(true, 'Automatically adjust minimum gap size based on ATR', group = 'Mind The Gap')
 
minGapSize = input.float(0.25, 'Minimum Gap Size to show (ignored if Automatically adjust gap is selected)', step = 0.05, group = 'Mind The Gap')
 
 
 
onlyShowLast = input.bool(false, 'Only show latest gap', group = 'Mind The Gap')
 
deleteFilledGaps = input.bool(true, 'Delete Filled Gaps', group = 'Mind The Gap')
 
 
 
if useAutoMinGapSize
 
minGapSize := ta.atr(14)
 
minGapSize
 
 
 
ob = volume[1] > ta.sma(volume[1], vl) * vr
 
 
 
// Colors and Label Styles
 
sellFVGColor = input.color(color.new(color.red, 70), group = 'Styles')
 
buyFVGColor = input.color(color.new(color.green, 70), group = 'Styles')
 
buyFVGText = input.string('Buyers Fair Value Gap', group = 'Styles')
 
sellFVGText = input.string('Sellers Fair Value Gap', group = 'Styles')
 
textColor = input.color(color.new(color.gray, 10), group = 'Styles')
 
sizeOption = input.string(title = 'Label Size', options = ['Auto', 'Huge', 'Large', 'Normal', 'Small', 'Tiny'], defval = 'Small', group = 'Styles')
 
 
 
labelSize = sizeOption == 'Huge' ? size.huge : sizeOption == 'Large' ? size.large : sizeOption == 'Small' ? size.small : sizeOption == 'Tiny' ? size.tiny : sizeOption == 'Auto' ? size.auto : size.normal
 
 
 
// Initialize variables
 
ema = ta.ema(close, emaLength)
 
var float buyVolume = na
 
var float sellVolume = na
 
buyVolume := high[1] == low[1] ? 0 : volume[1] * (close[1] - low[1]) / (high[1] - low[1])
 
sellVolume := high[1] == low[1] ? 0 : volume[1] * (high[1] - close[1]) / (high[1] - low[1])
 
var box b = na
 
var boxarray = array.new<box>()
 
var boolarray = array.new<bool>()
 
var createdBarArray = array.new<int>()
 
var idArray = array.new<int>() // To track unique box identifiers
 
 
 
var int currentId = 0 // Unique ID counter
 
 
 
// Create Fair Value Gaps and push to array
 
if useVolume
 
if ob and buyVolume > sellVolume and high[2] < low and low - high[2] > minGapSize and close[1] > ema
 
if onlyShowLast
 
if not na(b)
 
box.delete(b)
 
b := box.new(bar_index, high[2], bar_index + ol, low, border_color = color.new(color.black, 100), bgcolor = buyFVGColor, text = buyFVGText + ' #' + str.tostring(currentId), text_color = textColor, text_size = labelSize, text_halign = text.align_right)
 
array.push(boxarray, b)
 
array.push(boolarray, true)
 
array.push(createdBarArray, bar_index)
 
array.push(idArray, currentId)
 
//label.new(bar_index, high, "Created Box ID: " + str.tostring(currentId), color=color.green) // Debug: Log box creation
 
currentId := currentId + 1
 
currentId
 
else if ob and buyVolume < sellVolume and low[2] > high and low[2] - high > minGapSize and close[1] < ema
 
if onlyShowLast
 
if not na(b)
 
box.delete(b)
 
b := box.new(bar_index, low[2], bar_index + ol, high, border_color = color.new(color.black, 100), bgcolor = sellFVGColor, text = sellFVGText + ' #' + str.tostring(currentId), text_color = textColor, text_size = labelSize, text_halign = text.align_right)
 
array.push(boxarray, b)
 
array.push(boolarray, false)
 
array.push(createdBarArray, bar_index)
 
array.push(idArray, currentId)
 
//label.new(bar_index, high, "Created Box ID: " + str.tostring(currentId), color=color.green) // Debug: Log box creation
 
currentId := currentId + 1
 
currentId
 
else
 
if high[2] < low and low - high[2] > minGapSize and close[1] > ema
 
if onlyShowLast
 
if not na(b)
 
box.delete(b)
 
b := box.new(bar_index, high[2], bar_index + ol, low, border_color = color.new(color.black, 100), bgcolor = buyFVGColor, text = buyFVGText + ' #' + str.tostring(currentId), text_color = textColor, text_size = labelSize, text_halign = text.align_right)
 
array.push(boxarray, b)
 
array.push(boolarray, true)
 
array.push(createdBarArray, bar_index)
 
array.push(idArray, currentId)
 
//label.new(bar_index, high, "Created Box ID: " + str.tostring(currentId), color=color.green) // Debug: Log box creation
 
currentId := currentId + 1
 
currentId
 
else if low[2] > high and low[2] - high > minGapSize and close[1] < ema
 
if onlyShowLast
 
if not na(b)
 
box.delete(b)
 
b := box.new(bar_index, low[2], bar_index + ol, high, border_color = color.new(color.black, 100), bgcolor = sellFVGColor, text = sellFVGText + ' #' + str.tostring(currentId), text_color = textColor, text_size = labelSize, text_halign = text.align_right)
 
array.push(boxarray, b)
 
array.push(boolarray, false)
 
array.push(createdBarArray, bar_index)
 
array.push(idArray, currentId)
 
//label.new(bar_index, high, "Created Box ID: " + str.tostring(currentId), color=color.green) // Debug: Log box creation
 
currentId := currentId + 1
 
currentId
 
 
 
// Iterate over boxarray to check if any boxes have been filled
 
if deleteFilledGaps and array.size(boxarray) > 0
 
i = array.size(boxarray) - 1
 
while i >= 0
 
box currentBox = array.get(boxarray, i)
 
bool isBuy = array.get(boolarray, i)
 
 
 
// Use left and right boundaries of the box to determine if within timeframe
 
int boxLeft = box.get_left(currentBox)
 
int boxRight = box.get_right(currentBox)
 
bool withinTimeframe = bar_index <= boxRight
 
 
 
if withinTimeframe
 
float bottom = box.get_top(currentBox)
 
float top = box.get_bottom(currentBox)
 
 
 
// Debugging: Log every deletion attempt
 
//label.new(bar_index, high, "Checking Deletion for Box ID: " + str.tostring(array.get(idArray, i)) + " Close: " + str.tostring(close) + " Low: " + str.tostring(low) + " Bottom: " + str.tostring(bottom) + " Top: " + str.tostring(top), color=color.yellow)
 
 
 
// Buyers gap is filled if the close price or low price falls below the bottom of the box
 
if isBuy
 
if (close < bottom or low < bottom) and bar_index > boxLeft + 1 and bar_index <= boxRight
 
box.delete(currentBox)
 
array.remove(boxarray, i)
 
array.remove(boolarray, i)
 
array.remove(createdBarArray, i)
 
array.remove(idArray, i)
 
// Sellers gap is filled if the close price or high price rises above the top of the box
 
else
 
if (close > top or high > top) and bar_index > boxLeft + 1 and bar_index <= boxRight
 
box.delete(currentBox)
 
array.remove(boxarray, i)
 
array.remove(boolarray, i)
 
array.remove(createdBarArray, i)
 
array.remove(idArray, i)
 
i := i - 1 // Decrement the loop counter
 
i