Guide

barstate.isconfirmed in Pine Script: Signals That Stop Repainting

Signals that appear on unclosed bars and then vanish are one of the most common Pine Script frustrations. barstate.isconfirmed is the fix — here is exactly how it works.

9 min read

You add an indicator to your TradingView chart. A signal appears. You wait for it. By the time the bar closes, the signal has moved or disappeared entirely. You open a position based on a signal that no longer exists on the chart and that will never appear in your backtest.

This is the most common form of live-bar repainting in Pine Script, and it has a direct fix: barstate.isconfirmed. One built-in, one concept, and the problem is gone. This post explains exactly what it does and how to use it correctly across indicators, alerts, and strategies.

Why signals repaint on the current bar

Pine Script re-executes your entire script on every tick of the current live bar. Every condition you evaluate — RSI below 30, a crossover, price above a moving average — is checked continuously as price updates. If the condition is true at 2pm but false at 4pm when the bar closes, any signal or shape plotted at 2pm will be redrawn or removed at 4pm.

This is not a bug. It is how Pine Script is designed to work. The bar is not finished, so the indicator's output is not final either. The problem arises when traders act on the mid-bar signal as if it were final, or when they backtest with rules that only fire on confirmed closes but the live indicator fires throughout the bar.

Here is the problematic pattern and what it produces:

pine
//@version=6
indicator("Signal on every tick — repaints", overlay=true)

// BROKEN: this condition is evaluated on every tick of the current bar.
// The signal will appear mid-bar if the condition is true, then disappear
// when the bar closes and the condition is no longer true.
rsiValue  = ta.rsi(close, 14)
longSignal = rsiValue < 30

plotshape(longSignal, "Buy", shape.triangleup, location.belowbar, color.green)

What barstate.isconfirmed does

barstate.isconfirmed is a built-in boolean that istrue on exactly one tick per bar: the final tick, which is the bar's close. On every other tick of that bar — every intrabar price update — it is false.

Combining any condition with barstate.isconfirmed usingand means the combined expression can only be true on the final tick of a bar. Once that tick passes and the bar closes, the bar is historical. Historical bars execute only once, always at the close, so a signal placed on a historical bar never moves. The live and historical behavior now match.

pine
//@version=6
indicator("Signal on confirmed bars only", overlay=true)

// FIXED: barstate.isconfirmed is true only on the final tick of each bar —
// the close. Combining the condition with isconfirmed means the signal
// only fires once the bar has finished and will not change afterward.
rsiValue   = ta.rsi(close, 14)
longSignal = rsiValue < 30 and barstate.isconfirmed

plotshape(longSignal, "Buy", shape.triangleup, location.belowbar, color.green)

The only change is adding and barstate.isconfirmed to the condition. The signal now fires once, at the bar's close, exactly where it will appear permanently in the chart's history. The repainting is gone.

Using it correctly in alerts

Alerts have their own version of this problem. An alertconditionthat does not include barstate.isconfirmed can trigger multiple times per bar — once on every tick where the condition is true. Adding it ensures each alert fires exactly once per bar, at the close, matching the logic you actually want.

pine
//@version=6
indicator("Alert on confirmed bar", overlay=true)

rsiValue   = ta.rsi(close, 14)
longSignal = rsiValue < 30

// barstate.isconfirmed in alertcondition ensures the alert fires exactly
// once per bar, at the close, not on every tick that satisfies the condition.
alertcondition(longSignal and barstate.isconfirmed, "RSI oversold", "RSI crossed below 30 on confirmed close")

This is especially important for webhook-based automations where a duplicate alert could trigger an unintended second order. The full mechanics of alert timing are covered in the Pine Script alerts guide.

Using it in strategies

In a strategy, the default behavior is for orders to fill at the open of the bar after the signal bar. This already implies confirmation, but combining your entry condition with barstate.isconfirmed makes the intent explicit and prevents the strategy from placing orders mid-bar when calc_on_every_tick is enabled.

pine
//@version=6
strategy("Confirmed-bar entries only", overlay=true)

fastLen = input.int(9,  "Fast EMA")
slowLen = input.int(21, "Slow EMA")

fastEma = ta.ema(close, fastLen)
slowEma = ta.ema(close, slowLen)

// Hoist the crossover call before the 'and' to guarantee it runs every bar.
// v6 lazy evaluation can skip the right side of 'and', corrupting ta.crossover's
// internal state if it is placed there directly.
crossUp = ta.crossover(fastEma, slowEma)

// Only enter on a confirmed bar. This matches the bar that will appear in
// the backtest and ensures live signals agree with historical ones.
if crossUp and barstate.isconfirmed
    strategy.entry("Long", strategy.long)

if ta.crossunder(fastEma, slowEma) and barstate.isconfirmed
    strategy.close("Long")

Note the comment about hoisting ta.crossover() to its own variable. In Pine Script v6, the and operator evaluates lazily: if the left side is false, the right side is skipped. Functions liketa.crossover() need to execute on every bar to maintain their internal history buffer. If placed directly inside an andexpression, they may be skipped and produce incorrect results on subsequent bars. Assigning the call to a variable first guarantees it runs every bar, then the boolean is what participates in the condition. This is the same pattern explained in the multi-timeframe indicator build guide.

barstate.isconfirmed vs barstate.islast and barstate.isrealtime

These three are easy to confuse. barstate.isconfirmed is true on the close of every bar, both historical and realtime. It is the right choice for controlling when signals fire.

barstate.islast is true only while the script is processing the last bar on the chart — the current live bar. It is commonly used to draw labels or tables that should only appear once rather than on every historical bar. It does not control whether a signal fires at the close or mid-bar.

barstate.isrealtime is true only when the script is running on a live, updating bar — never on historical bars. Using it in a condition creates a mismatch between live and historical behavior, which is exactly the kind of structural repainting the Pine Script repainting checker flags. For consistent behavior across all bars, barstate.isconfirmed is almost always what you want.

The broader repainting picture

Live-bar repainting from unconfirmed conditions is one of five distinct repainting types in Pine Script. The other four — higher-timeframe lookahead, security without confirmed bars, drawing objects, andvarip in conditions — have different causes and different fixes. The full repainting guide covers all five. For a quick automated check of your own code, paste it into the repainting checker.

If you are building a complex indicator with multiple conditions, timeframes, and alerts, getting all of these patterns right simultaneously is where most hand-coded scripts develop subtle bugs. PineScripter applies barstate.isconfirmed and the confirmed-barrequest.security pattern by default when generating indicators, so the output does not require a repainting audit before you use it.


Disclaimer: PineScripter is a coding tool for Pine Script development. It does not provide financial advice and does not guarantee trading profits. Always backtest strategies thoroughly and understand the risks before live trading.