Automation Gotchas

Pine Script Repainting: Signal Vanished, Order Sent

2026-09-09·10 min read

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.

The short version
Set your alert Frequency to Once per bar close, and wrap any reads of closehighlow 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.
3
common sources of repainting
60 sec
TVSBot dedup window (does not block repainting)
0
seconds you have to unsend an order

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

SourceWhat it doesFix
Fluid values on realtime barsThe condition reads closehighlowopen before the bar closes; those values move on every tickWrap the reads with barstate.isconfirmed, or change the alert Frequency to Once per bar close
request.security() pulling a higher timeframeOn 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 valueUse 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_onDeliberately tells Pine to use future data when computing historical values, making the history look cleanA 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 barOnce per bar close
When it firesThe tick when the condition first becomes true on that barWhen the bar closes with the condition true
Can it repaint?YesNo
Worst-case latencyThe next tickOne full bar
Suited forManual confirmation referenceAutomated 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.

pine
//@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:

text
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.

1
What's your alert Frequency set to?
Once per bar (or any other non-close option)It will repaint. Switching to Once per bar close is the simplest step — no code changes required.
Once per bar closeThis layer is blocked; keep going.
2
Does the condition read closehighlowopen?
Yes, but not wrapped in barstate.isconfirmedEven with Frequency set to Once per bar close, scripts with multiple alert conditions can still evaluate early. Wrap those reads with barstate.isconfirmed.
Yes, and wrapped in barstate.isconfirmedFluid values are handled.
3
Do you use request.security() or barmerge.lookahead_on?
Yes, but without both [1] and lookahead_onThese are a common source of backtest-vs-realtime drift. To get non-repainting HTF values, 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.
Not used, or already using both [1] and lookahead_onAll three layers are cleared; this script's repainting risk is as low as it gets.

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 closehighlowopen in a condition is wrapped in barstate.isconfirmed.
  • Every use of request.security() for higher-timeframe data has both [1] on the expression and lookahead = barmerge.lookahead_on — they depend on each other; either one alone breaks it.
  • No other barmerge.lookahead_on in 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?
Only if the condition itself isn't a repainting source. If the script uses 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?
Usually. Backtests use each bar's finalized OHLC; live is computed tick-by-tick, so there's an inherent gap. Switch to Once per bar close, watch a few sessions, and compare — if the gap shrinks noticeably it was repainting; if not, look at the other causes in the webhook debug flow.
Is there a difference between alertcondition() and alert()?
Logically, no — both are affected by 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?
TradingView doesn't leave a marker for "alert fired but the condition later became false" — the alert itself is a separate server-side process from the indicator you see on the chart. To view the actual trigger history, check the Alert Manager or your own execution log.
Can TVSBot's 60-second dedup be lengthened to catch some repainting?
Not recommended, and this layer alone isn't the right tool. The 60-second window is sized for TradingView 5xx retries — stretching it will start swallowing "you legitimately made two actions in the same minute" cases. Repainting has to be solved on the Pine side; the execution layer's job is an auditable trail, not second-guessing the trigger.

Get started

Ready to ship what you just learned?

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