Pine Script Repainting: Signal Vanished, Order Sent
Your strategy fires an alert mid-bar, the webhook (the channel that auto-POSTs signals to your execution server) hits your backend, and the order fills. When you come back to that bar later, the signal that triggered the alert isn't on the chart at all — that's what repainting looks like in the most common way it breaks live automation: the condition was true before the bar closed and false after, but the order already went out.
Nobody warns you up front: this isn't your script being broken. It's how Pine Script (the scripting language TradingView uses for chart strategies) defines realtime values like close and high — while a bar is still open, they update on every tick. This behavior has a name — repainting — and it's the place automated trading tends to break the quietest.
This piece doesn't rank which strategies are more prone to repaint — that depends on your own logic. It does three things: explain the mechanism, list what you can actually block on the Pine side, and explain why your webhook execution layer isn't the right place to solve this.
close/high/low in your conditions with barstate.isconfirmed. Doing both kills most of the false triggers caused by repainting. If you can't, treat any signal as "not really confirmed yet" — no amount of replay or dedup on the execution side can un-send an alert that shouldn't have gone out in the first place.Which Step Is Actually Lying to You?
TradingView's own wording on repainting is direct: script behavior causing historical vs realtime calculations or plots to behave differently. That covers two things — plots that redraw differently after the fact (visual repainting), and realtime calculations whose intermediate values differ from the closed-bar version (which affects trigger decisions). The one that breaks live automation is the second.
Pine's execution model is bar-by-bar: historical bars are computed once, on their final OHLC values; realtime bars are re-computed on every tick, on that tick's data. The same code sees different inputs in the two phases, so it can produce different outputs.
The close your condition reads on a realtime bar is "the current price on this tick", not "the close of this bar." Your if close > sma_line may become true on some tick, the alert fires, the webhook goes out; the next tick pulls price back and the condition is false again. By the time the bar actually closes, close is a different number, and the chart shows no trace that the condition was ever met.
Why It Feels Harmless Manually but Sends Real Orders When Automated
Sitting at the chart, this only gives you the occasional "that line looks weird" feeling — you aren't deciding with it because your action only happens when you click. The condition can become true and false any number of times in between and nothing actually happens.
Automated trading is the opposite. The alert firing is the action; the webhook has already hit your execution server, the order has already been placed. Even if the bar closes with the condition no longer true, that order can't be pulled back — all you see later is a fill on your account that you can't reverse-engineer.
What makes it dangerous is the combination of "looks silent" and "real orders went out." Nothing errors out, no dialog pops up, and if you check alert history the messages will be there. It breaks quietly.
Three Common Sources of Repainting, Each with Its Own Fix
| Source | What it does | Fix |
|---|---|---|
| Fluid values on realtime bars | The condition reads close/high/low/open before the bar closes; those values move on every tick | Wrap the reads with barstate.isconfirmed, or change the alert Frequency to Once per bar close |
request.security() pulling a higher timeframe | On a 5m chart, calling request.security() for the 1h close: in backtest it uses the finalized 1h close, but in realtime it may pick up an in-progress 1h value | Use the historical offset [1] together with lookahead = barmerge.lookahead_on — the docs are explicit that they depend on each other, and neither works alone — so you consistently get the previous, closed HTF bar |
barmerge.lookahead_on | Deliberately tells Pine to use future data when computing historical values, making the history look clean | A common source of backtest fakery — don't use it in a strategy that trades; if you truly need it for non-repainting HTF reads, always pair it with [1] to offset the future bias |
Three common sources of repainting and how to address each (Pine Script v6, verified August 2026). Details defer to the Repainting section of the Pine Script Language Reference Manual v6.
Any script can hit one, all, or none of these — there's no one-liner that says whether "your script repaints." The way to tell is to hand the script to an AI (or walk it yourself) and check every read of OHLCV or request.security() against the table above.
The Two Things You Can Actually Do on the Pine Side
Whichever source you have, only two moves at the script level actually prevent repainting. Neither is enforced by the compiler, and neither prints a "you did it right" message — TradingView's own docs say it's a tradeoff: waiting for confirmed close removes repainting but delays your signal by seconds up to a full bar, and you can't have both.
1: Set alert Frequency to Once per bar close
The alert creation dialog has a Frequency dropdown; the Help Center page "Differences between alert frequencies" lists four options: Once only, Once per bar, Once per bar close, and Once per minute or every time. The two most relevant to automated strategies are the middle two:
| Once per bar | Once per bar close | ||
|---|---|---|---|
| When it fires | The tick when the condition first becomes true on that bar | When the bar closes with the condition true | |
| Can it repaint? | Yes | No | |
| Worst-case latency | The next tick | One full bar | |
| Suited for | Manual confirmation reference | Automated trading strategies |
This dropdown is a click in your browser — no code change. If you don't pick Once per bar close, the second fix below is pointless: no matter how clean the condition is, Once per bar still fires mid-tick. (The other two options — Once only fires just once in a lifetime, Once per minute or every time fires every minute whenever the condition holds — don't suit a continuously running automated strategy, so they're out of scope here.)
2: Wrap the conditions with barstate.isconfirmed
Changing Frequency alone isn't enough. Your strategy may have multiple alert conditions, or use the alert() function instead of alertcondition() — in either case you also want to gate the check inside Pine on "did this bar actually close." The variable for that is barstate.isconfirmed: on historical bars it's always true; on realtime bars it's true only on the last (closing) tick.
//@version=6
strategy("safe-cross", overlay=true)
fast = ta.sma(close, 9)
slow = ta.sma(close, 21)
// ❌ Unsafe: enter as soon as any tick satisfies the condition
// if ta.crossover(fast, slow)
// strategy.entry("L", strategy.long)
// ✅ Safe: only decide after the bar closes and is confirmed
if ta.crossover(fast, slow) and barstate.isconfirmed
strategy.entry("L", strategy.long)Not sure which lines to wrap? Paste the script to an AI and ask it these four in order:
This is a Pine Script v6 strategy that I want to wire up to a webhook for
automated trading. Don't rewrite it yet — first answer:
1. In this script, which conditions could become "true mid-bar and false at
close"? Enumerate them line by line.
2. Does it use request.security() to pull higher-timeframe data?
If so, does it use [1] together with lookahead = barmerge.lookahead_on?
(They depend on each other; adding just one is not enough for
non-repainting HTF reads.)
3. Does it use barmerge.lookahead_on anywhere else?
If so, is it paired with [1] to offset the future bias?
4. If I set the alert Frequency to Once per bar close, which behaviors of
this script change and which don't?
Answer all four first, then give me a corrected version.
[paste your script here]Answering before editing checks whether the AI actually understood the script, or is just parroting your prompt back. Question 1 asks it to enumerate line by line — a vague answer like "looks fine to me" means it didn't read the script, and letting it rewrite in that state usually deletes the entire block that could have been the problem.
Once the Order Is Out, Can the Webhook Layer Save You?
Short answer: not really. Longer answer: there are two mitigations you can put on the webhook layer, but neither can compensate for an alert whose condition was never really true.
The first is the intuitive one: dedup on the webhook side — if you receive two messages on the same bar, treat them as one. That works for the "same alert retried because the receiver returned 5xx" case; but the two payloads produced by repainting may not be identical — values like strategy.equity and strategy.position_size also drift on every tick, so the computed qty can differ by a few digits. We've tripped over this ourselves.
As an example, the TVSBot execution side has a 60-second dedup window; the fingerprint is the first 32 chars of the SHA-256 of user_id | strategy | symbol | action | qty. A second hit within 60 seconds with the same fingerprint is skipped and not sent. That works for "TradingView retrying the same message"; it doesn't work for "same bar, but qty drifted because equity moved" — the two fingerprints differ, so both go through. This is an inference from the mechanism, not an official guarantee — an execution layer that doesn't include qty in the fingerprint would behave differently.
The second layer is post-hoc replay/audit: keep every incoming payload, the decision at the time, and the outgoing order, so you can reconstruct what happened. That doesn't prevent repainting either — it lets you rebuild the scene, but it doesn't pull the order back.
So the correct layer to solve repainting is always the Pine side: Frequency + barstate.isconfirmed. The best the execution side can do is make actually-sent-but-wrong signals traceable and replayable — a related, but different, problem.
close/high/low/open?barstate.isconfirmed.request.security() or barmerge.lookahead_on?request.security() needs both [1] on the expression and lookahead = barmerge.lookahead_on — the Repainting docs say they depend on each other and dropping either breaks the guarantee.Pre-Launch Self-Check (Five Items)
- Alert Frequency is set to Once per bar close (not Once per bar, not Once only, not Once per minute or every time). No code change required — easiest first move.
- Every read of
close/high/low/openin a condition is wrapped inbarstate.isconfirmed. - Every use of
request.security()for higher-timeframe data has both[1]on the expression andlookahead = barmerge.lookahead_on— they depend on each other; either one alone breaks it. - No other
barmerge.lookahead_onin the script — if there is one, verify it's paired with[1]to offset the future bias, not used to make backtests look pretty. - Your execution side keeps the raw payload and processing log for every incoming signal, so you can align against TradingView's trigger time later — not auto-dedup, just an auditable trail.
Even after all five, you may still see "two orders on one bar." That's usually not repainting — it's the bar producing multiple order events (entry plus stop and take-profit sent separately) or your receiver returning 5xx and TradingView auto-retrying. Different mechanism, different fix; see the debug flow in the piece on alerts having no memory.
The Honest Bit: 60-Second Dedup Isn't a Repainting Cure
No automated trading stack blocks 100% of repainting-triggered orders — including ours. TVSBot's 60-second dedup is designed for "TradingView 5xx retry", not for repainting; we can't decide at signal receipt whether the current close will hold to the bar's close — only the Pine runtime knows that, and the bar's final OHLC isn't settled when the webhook lands. The design lowers the odds of duplicate orders and shrinks their blast radius, but it can't block the one that should never have been sent.
Anyone telling you their webhook middleware can do that probably has nothing to back it up — it would require knowing the future close price, and nobody can.
FAQ
Once I set Frequency to Once per bar close, is repainting fully gone?
request.security() without both [1] and lookahead = barmerge.lookahead_on (the docs are explicit that both are required for non-repainting), or uses barmerge.lookahead_on elsewhere without [1], backtest and realtime will still diverge even with close-confirmed alerts. Frequency only blocks the fluid-value source.Clean backtest, messy live — is it always repainting?
Is there a difference between alertcondition() and alert()?
barstate.isconfirmed. The difference is where frequency is controlled: alertcondition() takes it from the Frequency dropdown in the UI, while alert() takes it from an argument like alert.freq_once_per_bar_close in code. Both can be non-repainting, but choosing the wrong one leaves the setting unchanged where it matters.Why doesn't the chart show any trace of an alert that fired but then failed?
Can TVSBot's 60-second dedup be lengthened to catch some repainting?
Get started
With Frequency and barstate handled on the Pine side, hand the webhook-to-exchange leg to TVSBot — with your own API key, dry-run first, every payload replayable after the fact.
Start free