A complete map of Pine Script v6's barstate.* flags — isconfirmed, isrealtime, ishistory, islast, isnew, isfirst, islastconfirmedhistory — and what each one implies for repainting. The barstate execution model (historical bars calculate once at close; the realtime bar recalculates on every tick with rollback) is the root cause of most 'works in backtest, fails live' complaints, and barstate.isconfirmed is the standard gate that closes the gap. Educational material only, not financial advice; correct bar-state handling removes an illusion, it does not create an edge.
barstate.isconfirmed is true only on the final calculation of a bar — the update on which the bar closes and its OHLC values become permanent. Use it to gate any action that should happen once per bar with settled data: alerts, plotshape signals, and strategy entries that must not react to values that are still changing. The canonical pattern is signal and barstate.isconfirmed.
Why this single flag matters so much comes down to Pine's two-regime execution model. On historical bars, your script runs exactly once per bar, using that bar's final OHLC — every historical calculation is, by definition, a confirmed-bar calculation. On the realtime bar, the script runs again on every incoming tick, and the bar's high, low, and close are provisional until the bar closes. A condition like ta.crossover(close, ta.sma(close, 20)) can be true at 14:32, false at 14:41, and true again at 14:59 — all within one bar. Whatever it shows intrabar, only the state at the final update matches what the historical record will say tomorrow.
barstate.isconfirmed is true on every historical bar and on exactly one realtime update per bar: the closing one. Gating on it therefore forces the realtime bar to behave like a historical bar — the script acts only on data that will never change. That is the precise sense in which it prevents a whole class of repainting.
The honest cost: you give up intrabar reaction speed. A confirmed-bar signal on a 4-hour chart arrives up to four hours after the move began. That trade-off is real and unavoidable; what isconfirmed buys is that your backtest and your live signals are finally describing the same events.
Pine v6 exposes seven flags. Each answers a different question about which calculation this is.
`barstate.ishistory` — this bar was already closed when the script loaded. Everything here is final; nothing repaints. `barstate.isrealtime` — this bar is forming now (or was, since the script loaded). All values are provisional until confirmation. These two partition every bar, and the boundary between them is where backtest/live discrepancies are born.
`barstate.isconfirmed` — this is the bar's final update. True on all historical bars and once per realtime bar, at close. The repaint-safety gate.
`barstate.isnew` — this is the first calculation on this bar. On historical bars it is always true (each bar gets one calculation, which is both first and last); on the realtime bar it is true only on the opening tick. Useful for once-per-bar initialisation like capturing the open of a session.
`barstate.islast` — this bar is the rightmost bar on the chart, whether the market is open or closed. Note islast does not mean realtime: on a closed market, the last bar is both islast and ishistory. Useful for drawing tables and labels only once, at the chart's edge.
`barstate.isfirst` — the very first bar of the dataset. Initialisation counterpart to islast.
`barstate.islastconfirmedhistory` — the final closed bar: the last historical bar when a realtime bar exists, or the last bar of a closed market. This is the flag for anchoring logic to "the most recent bar whose values are final."
The repaint reading in one line: values computed under ishistory or under isconfirmed are permanent; anything computed on an unconfirmed realtime update can — and routinely does — change before the bar closes.
The complaint is so common it has become a genre: an indicator or strategy looks excellent across years of history, then behaves erratically the moment it runs on live data. Before suspecting overfitting (a separate, equally real problem), understand the mechanical cause: the historical and realtime portions of the chart are computed under different rules.
On each realtime tick, Pine performs a rollback: the effects of the previous calculation on the same bar are undone — plots reverted, variables restored to their values as of the previous bar's close — and the script re-executes from that clean state with the newest provisional OHLC. The realtime bar is a whiteboard, erased and redrawn on every tick. Only when the bar closes is the final drawing committed, becoming the single historical calculation that future reloads will see.
The consequences follow directly. A signal that flickered true intrabar and fired an ungated alert simply never existed as far as the reloaded chart is concerned — the committed close shows no signal, and the chart looks like it "repainted" or "deleted" the arrow. Users watching live saw an entry; the backtest, recomputed purely from committed bars, never took it. Conversely, a backtest scored on final closes assumes reactions at prices the live script only knew after acting.
One subtlety: var variables and objects like label/line participate in rollback for value changes, but side effects that escape the chart — an alert already sent, a webhook already delivered — cannot be rolled back. That asymmetry is exactly why unconfirmed alerts are dangerous: the chart can take the signal back; your broker cannot.
So the fix is not mystical. Either gate actions on barstate.isconfirmed so live behaviour matches the committed record, or consciously accept intrabar logic and acknowledge that no historical simulation on committed bars can fully represent it.
Here is a compact, complete v6 indicator showing the pattern applied to shapes and alerts together:
//@version=6
// Educational only — validate before trading; not financial advice.
indicator("Confirmed-bar EMA cross", overlay = true)
fast = ta.ema(close, 9)
slow = ta.ema(close, 21)
plot(fast, "Fast EMA", color = color.teal)
plot(slow, "Slow EMA", color = color.orange)
crossUp = ta.crossover(fast, slow) and barstate.isconfirmed
crossDn = ta.crossunder(fast, slow) and barstate.isconfirmed
plotshape(crossUp, "Cross up", shape.triangleup, location.belowbar, color.green)
plotshape(crossDn, "Cross down", shape.triangledown, location.abovebar, color.red)
if crossUp
alert("EMA cross up on " + syminfo.ticker, alert.freq_once_per_bar_close)
if crossDn
alert("EMA cross down on " + syminfo.ticker, alert.freq_once_per_bar_close)Details worth noticing. The barstate.isconfirmed term lives in the condition, so shapes, alerts, and any future strategy port all inherit the same gate — one definition of "a signal happened" rather than three drifting copies. The alert() call uses alert.freq_once_per_bar_close, which belt-and-braces the same guarantee at the alert layer; with the condition already gated this is technically redundant, and that redundancy is deliberate — either line can be edited later without silently reopening the intrabar leak.
For strategies the identical gate applies at the entry decision:
if crossUp and strategy.position_size == 0
strategy.entry("Long", strategy.long)(with crossUp already containing barstate.isconfirmed, as above). One thing not to gate: strategy.exit() calls that maintain protective stops should generally remain registered whenever a position is open, so the protection itself never blinks out intrabar — gate the signal, not the safety net.
What this pattern costs is stated plainly in the first section: signals arrive at bar close, never earlier. What it buys is that the arrow you see live is the arrow the chart will still show next week.
barstate.isconfirmed closes the intrabar repaint hole on the chart's own timeframe, but a second, sneakier hole opens whenever a script pulls higher-timeframe data with request.security() — and no barstate flag alone fixes it.
The problem has two parts. Live: while a higher-timeframe bar is still forming, request.security(syminfo.tickerid, "60", close) returns its provisional close, which changes tick by tick — an unconfirmed value by construction, whatever your chart timeframe's barstate says. Historical: the same call returns the HTF bar's final close, and it appears on chart bars that occurred before that HTF bar had actually closed. The backtest sees tomorrow's settled value where the live script saw a moving one — a structural lookahead, even with lookahead left at its default of barmerge.lookahead_off. (Setting barmerge.lookahead_on on a non-offset series is strictly worse: it openly serves future data to historical bars and belongs only in intentionally-wrong demonstrations.)
The standard repaint-safe pattern requests the previous, completed HTF bar and states the merge mode explicitly:
//@version=6
// Educational only — validate before trading; not financial advice.
indicator("HTF close, repaint-safe", overlay = true)
htfClose = request.security(syminfo.tickerid, "60", close[1],
lookahead = barmerge.lookahead_off)
plot(htfClose, "Prev H1 close", color = color.blue)The [1] offset means every value the script ever sees — live or historical — is a bar that had already closed, so the two regimes finally agree. The cost is the by-now-familiar one: your HTF information is one completed HTF bar old.
A closing calibration. Bar-state discipline eliminates illusions — signals that exist live but not in the record, or in the record but not live. It does nothing to establish that the underlying idea has merit; a perfectly repaint-free strategy can still be overfit noise. Validate the mechanics with tooling (ForexCodes' validator checks confirmed-bar gating and request.security usage statically), then interrogate the idea itself with out-of-sample and walk-forward testing. Educational only, not financial advice — trading carries a real risk of loss.
Educational only — not financial advice. Trading involves substantial risk of loss.