Rajandran R Creator of OpenAlgo - OpenSource Algo Trading framework for Indian Traders. Building GenAI Applications. Telecom Engineer turned Full-time Derivative Trader. Mostly Trading Nifty, Banknifty, High Liquid Stock Derivatives. Trading the Markets Since 2006 onwards. Using Market Profile and Orderflow for more than a decade. Designed and published 100+ open source trading systems on various trading tools. Strongly believe that market understanding and robust trading frameworks are the key to the trading success. Building Algo Platforms, Writing about Markets, Trading System Design, Market Sentiment, Trading Softwares & Trading Nuances since 2007 onwards. Author of Marketcalls.in

How to Draw Gap Zones in TradingView Pine Script

7 min read

Gaps are one of the cleanest visual signals in price action. A gap forms when today’s price range does not overlap with yesterday’s range, leaving an empty pocket on the chart that often acts as a magnet for future price. Drawing these gap zones automatically and tracking whether they get filled is a common request from traders, and Pine Script v6 makes it straightforward once you understand how box objects and arrays work together.

In this post we will build a gap zone indicator from scratch. By the end you will have a script that detects gap ups and gap downs, draws a filled zone between the previous bar and the gap bar, extends the zone forward until price closes the gap, and colors the gap bar yellow when unfilled or gray once filled.

What is a Price Gap?

A price gap is a discontinuity between two adjacent bars where the current bar’s range sits entirely above or below the previous bar’s range. It is not the same as a higher open or a lower open, which simply opens away from the previous close but still overlaps the prior range.

A gap up forms when low > previous high, leaving untouched price territory between the two bars. A gap down forms when high < previous low, again leaving an untouched pocket. The empty pocket between the two ranges is what we call the gap zone, and it has two natural boundaries: the previous bar’s extreme on one side and the gap bar’s extreme on the other.

How is a Gap Considered Filled?

A gap is considered filled when price trades back through the empty pocket and touches the previous bar’s level that originally formed the gap. For a gap up, this means price coming back down so that the current low reaches or breaches the previous high. For a gap down, it means price rallying back up so that the current high reaches or breaches the previous low. Until that happens, the gap remains open and the zone stays active on the chart.

Why Track Gap Zones Visually?

Traders watch gap zones because unfilled gaps tend to attract price over time, and the zone itself often acts as support or resistance while it is open. Visually marking the zone helps you do three things at a glance: spot where price is likely to react, see how long a gap has been open, and confirm fills the moment they happen without having to eyeball previous bars. A static box on the chart is far more useful than scrolling back to check exact levels every time.

Setting Up the Indicator

We start with a v5 indicator declaration that draws on the main price chart. We need overlay=true so our boxes appear on the candles, and we raise max_boxes_count because every new gap creates a persistent box that we want to keep on the chart.

//@version=6
indicator("Gap Zones Explainer", overlay=true, max_boxes_count=500)

The max_boxes_count parameter caps how many active boxes Pine will retain. The default of 50 is usually too low for a long backtest on a daily chart, so 500 gives us plenty of headroom.

Detecting a Gap

Detection itself is just two conditions. We compare today’s high and low against the previous bar’s low and high using the history-referencing operator [1].

prevHigh = nz(high[1])
prevLow  = nz(low[1])

isGapUp   = low  > prevHigh and bar_index > 0
isGapDown = high < prevLow  and bar_index > 0

The nz() wrapper guards against na values on the very first bar, and the bar_index > 0 check ensures we never compare against a non-existent previous bar. These two booleans are all we need to trigger zone creation.

Drawing the Gap Zone with box.new()

The box.new() function is the workhorse here. It takes four coordinates (left bar, top price, right bar, bottom price) and draws a filled rectangle. For a gap down, the top of the zone is the previous low and the bottom is the current high. For a gap up, the top is the current low and the bottom is the previous high.

if isGapDown
    box.new(left=bar_index, top=prevLow, right=bar_index + 1, bottom=high, bgcolor=color.new(color.yellow, 80), border_color=color.orange, border_width=1)

That single call draws the zone the moment a gap down is detected. We set right=bar_index + 1 so the box has a visible width of one bar initially, and we use a transparent yellow fill so the candles underneath remain readable.

Extending the Zone Until It is Filled

A static box that only covers one bar is not enough. We want the zone to extend rightward each new bar until price fills it. To do that we need to remember every box we have drawn and update its right edge on every bar. Pine Script arrays are the right tool for this kind of bookkeeping.

var array<box>   activeBoxes = array.new<box>()
var array<float> fillLevels  = array.new<float>()
var array<bool>  filledFlags = array.new<bool>()

The var keyword is critical here. Without it the arrays would be reinitialized on every bar and we would lose all our state. With var, the arrays persist across bars and accumulate boxes as new gaps form.

When a new gap down is detected, we push the box reference, the fill level (previous low), and a false filled flag into our parallel arrays:

if isGapDown
    newBox = box.new(left=bar_index, top=prevLow, right=bar_index + 1, bottom=high, bgcolor=color.new(color.yellow, 80), border_color=color.orange, border_width=1)
    array.push(activeBoxes, newBox)
    array.push(fillLevels, prevLow)
    array.push(filledFlags, false)

Then on every bar, we loop through all stored boxes and either extend the right edge forward or mark them as filled if price has reached the fill level:

if array.size(activeBoxes) > 0
    for i = 0 to array.size(activeBoxes) - 1
        b       = array.get(activeBoxes, i)
        fillLvl = array.get(fillLevels, i)
        filled  = array.get(filledFlags, i)

        if not filled
            if high >= fillLvl
                box.set_right(b, bar_index)
                array.set(filledFlags, i, true)
            else
                box.set_right(b, bar_index + 1)

This loop runs once per bar, which is fast even on long histories. Notice how box.set_right() mutates the existing box rather than creating a new one. This is what gives us the smooth zone extension visual without flooding the chart with hundreds of boxes per gap.

Coloring the Gap Bar Yellow or Gray

The bar coloring is a separate concern from the zone. We want the gap bar itself to stand out in yellow when its zone is still open and turn gray once filled. The trick is that we have to track whether the bar at the current bar_index was originally a gap bar, and look up the corresponding filled flag.

A clean way to do this is to store the bar index of every gap when it is detected, then on each bar check whether the current bar_index matches any stored gap bar. If it does, we use the filled flag to pick the color.

var array<int> gapBars = array.new<int>()

if isGapDown
    array.push(gapBars, bar_index)

color barColorVal = na
if array.size(gapBars) > 0
    for j = 0 to array.size(gapBars) - 1
        if array.get(gapBars, j) == bar_index
            barColorVal := array.get(filledFlags, j) ? color.gray : color.yellow

barcolor(barColorVal, title="Gap Bar")

The explicit color barColorVal = na declaration matters. If you write just barColorVal = na, Pine cannot infer the type and throws the error CE10097: Value with NA type cannot be assigned to a variable that was defined without type keyword. Declaring the type up front fixes this and makes the intent clear.

Notice that barcolor() is called unconditionally at the end with the barColorVal variable. When the bar is not a gap bar, the variable stays na and Pine simply leaves the bar’s default color alone.

Putting It All Together

Here is the complete minimal indicator that covers gap downs only. Once you understand this version, extending it to handle gap ups is just a matter of mirroring the logic with low > prevHigh and using low <= fillLvl as the fill condition.

// This Pine Script® code is subject to the terms of the Mozilla Public License 2.0 at https://mozilla.org/MPL/2.0/
// © algostudio

//@version=6
indicator("Gap Scanner (Up & Down) with Zone Tracking", shorttitle="Gap Scanner", overlay=true, max_boxes_count=500, max_lines_count=500)

// ========== INPUTS ==========
showGapUp        = input.bool(true,  "Show Gap Ups",   group="Display")
showGapDown      = input.bool(true,  "Show Gap Downs", group="Display")

unfilledUpColor  = input.color(color.new(color.lime,   60), "Unfilled Gap Up Bar Color",   group="Gap Up")
filledUpColor    = input.color(color.new(color.gray,   60), "Filled Gap Up Bar Color",     group="Gap Up")
zoneUpColor      = input.color(color.new(color.green,  80), "Gap Up Zone Color",            group="Gap Up")
zoneUpBorder     = input.color(color.new(color.green,  40), "Gap Up Zone Border",           group="Gap Up")

unfilledDnColor  = input.color(color.new(color.yellow, 60), "Unfilled Gap Down Bar Color", group="Gap Down")
filledDnColor    = input.color(color.new(color.gray,   60), "Filled Gap Down Bar Color",   group="Gap Down")
zoneDnColor      = input.color(color.new(color.yellow, 80), "Gap Down Zone Color",          group="Gap Down")
zoneDnBorder     = input.color(color.new(color.orange, 40), "Gap Down Zone Border",         group="Gap Down")

showLabel        = input.bool(true,  "Show Gap % Label", group="Display")
extendZone       = input.bool(true,  "Extend Zone Until Filled", group="Display")

// ========== GAP DETECTION ==========
prevHigh  = nz(high[1])
prevLow   = nz(low[1])
prevClose = nz(close[1])

isGapUp   = low  > prevHigh and bar_index > 0
isGapDown = high < prevLow  and bar_index > 0

gapUpPct   = prevClose != 0 ? ((low  - prevClose) / prevClose) * 100 : 0.0
gapDownPct = prevClose != 0 ? ((prevClose - high) / prevClose) * 100 : 0.0

qualifiesUp   = isGapUp   and showGapUp
qualifiesDown = isGapDown and showGapDown

// ========== TRACK ACTIVE GAP ZONES ==========
// gapType: 1 = up, -1 = down
// For gap up   -> zone bottom = prevHigh, top = today's low,  filled when low  <= prevHigh
// For gap down -> zone top    = prevLow,  bottom = today's high, filled when high >= prevLow

var array<box>   activeBoxes = array.new<box>()
var array<float> fillLevels  = array.new<float>()   // price level that closes the gap
var array<int>   gapBars     = array.new<int>()
var array<int>   gapTypes    = array.new<int>()     // 1 up, -1 down
var array<bool>  filledFlags = array.new<bool>()

// Create new gap up zone
if qualifiesUp
    topLevel = low
    botLevel = prevHigh
    newBox = box.new(left=bar_index, top=topLevel, right=bar_index + 1, bottom=botLevel, bgcolor=zoneUpColor, border_color=zoneUpBorder, border_width=1, extend=extend.none)
    array.push(activeBoxes, newBox)
    array.push(fillLevels, prevHigh)
    array.push(gapBars,    bar_index)
    array.push(gapTypes,   1)
    array.push(filledFlags, false)

    if showLabel
        label.new(bar_index, low, text=str.tostring(gapUpPct, "#.##") + "% gap up", style=label.style_label_down, color=color.new(color.green, 20), textcolor=color.white, size=size.small)

// Create new gap down zone
if qualifiesDown
    topLevel = prevLow
    botLevel = high
    newBox = box.new(left=bar_index, top=topLevel, right=bar_index + 1, bottom=botLevel, bgcolor=zoneDnColor, border_color=zoneDnBorder, border_width=1, extend=extend.none)
    array.push(activeBoxes, newBox)
    array.push(fillLevels, prevLow)
    array.push(gapBars,    bar_index)
    array.push(gapTypes,   -1)
    array.push(filledFlags, false)

    if showLabel
        label.new(bar_index, high, text=str.tostring(gapDownPct, "#.##") + "% gap down", style=label.style_label_up, color=color.new(color.orange, 20), textcolor=color.white, size=size.small)

// Update existing zones
if array.size(activeBoxes) > 0
    i = array.size(activeBoxes) - 1
    while i >= 0
        b       = array.get(activeBoxes, i)
        fillLvl = array.get(fillLevels, i)
        gType   = array.get(gapTypes, i)
        filled  = array.get(filledFlags, i)

        if not filled
            // Gap up filled when low trades back down to prevHigh
            // Gap down filled when high trades back up to prevLow
            isFilledNow = gType == 1 ? (low <= fillLvl) : (high >= fillLvl)
            if isFilledNow
                box.set_right(b, bar_index)
                array.set(filledFlags, i, true)
            else
                if extendZone
                    box.set_right(b, bar_index + 1)
        i := i - 1

// ========== BAR COLORING ==========
color barColorVal = na
if array.size(gapBars) > 0
    for j = 0 to array.size(gapBars) - 1
        gBar = array.get(gapBars, j)
        if gBar == bar_index
            gType  = array.get(gapTypes, j)
            filled = array.get(filledFlags, j)
            if gType == 1
                barColorVal := filled ? filledUpColor : unfilledUpColor
            else
                barColorVal := filled ? filledDnColor : unfilledDnColor

barcolor(barColorVal, title="Gap Bar")

// ========== ALERTS ==========
alertcondition(qualifiesUp,   title="Gap Up Detected",   message="Gap up detected on {{ticker}} at {{time}}")
alertcondition(qualifiesDown, title="Gap Down Detected", message="Gap down detected on {{ticker}} at {{time}}")

Drop this on a daily chart of any liquid stock or index and you will immediately see the gap downs marked, with each zone extending forward until price climbs back to the previous low and closes the gap.

Common Pitfalls to Avoid

The first trap most people fall into is forgetting the var keyword on the arrays. Without it, every bar starts with empty arrays and your zones never persist. The second trap is iterating in a way that fails when the array is empty. Always wrap your loops with an array.size() > 0 check, otherwise you will hit a runtime error on the first few bars before any gap has formed. The third trap is creating a new box every bar instead of mutating the existing one with box.set_right(). That approach quickly hits the max_boxes_count ceiling and TradingView starts dropping older boxes.

Where to Take This Next

This indicator is a foundation, not a finished tool. You can extend it by adding a percentage filter to ignore tiny gaps, by drawing a midline through each zone for partial fill detection, by counting how many bars a gap has been open and surfacing that as a label, or by exporting fill events as alerts for use in a broader scanner. The same box and array machinery scales up cleanly to all of these use cases.

If you are running a multi-symbol scanner across the NIFTY 500 or your own watchlist, the per-symbol Pine Script approach has limits. At that point you will want to combine this visual indicator with a server-side scanner that pulls historical bars through OpenAlgo or a similar data API and surfaces unfilled gaps as a daily report. The detection logic stays identical, just translated to Python.

Rajandran R Creator of OpenAlgo - OpenSource Algo Trading framework for Indian Traders. Building GenAI Applications. Telecom Engineer turned Full-time Derivative Trader. Mostly Trading Nifty, Banknifty, High Liquid Stock Derivatives. Trading the Markets Since 2006 onwards. Using Market Profile and Orderflow for more than a decade. Designed and published 100+ open source trading systems on various trading tools. Strongly believe that market understanding and robust trading frameworks are the key to the trading success. Building Algo Platforms, Writing about Markets, Trading System Design, Market Sentiment, Trading Softwares & Trading Nuances since 2007 onwards. Author of Marketcalls.in

Leave a Reply

Get Notifications, Alerts on Market Updates, Trading Tools, Automation & More