Repainting, lookahead and calc_on_every_tick explained
Why a Pine strategy trades differently live than in the backtest: lookahead, request.security offsets, calc_on_every_tick, barstate.isconfirmed, how to test.
Updated 30 Aug 2026
A strategy that shows a clean backtest and then trades differently live is not necessarily wrong. It is usually repainting, which TradingView defines as "script behavior causing historical vs realtime calculations or plots to behave differently" (Pine Script docs: Repainting). The definition is broad on purpose. By the docs' own account most indicators repaint in some form, and most of that is harmless. The question for anyone sending alerts to a webhook is narrower: does the signal that fires live match the signal the backtest counted?
What repainting is
Pine runs a script once per historical bar, with the bar's final open, high, low and close already known. On the live bar it runs again on every price update, and only the closing run is committed to history. Historical data has no record of the path price took inside a bar, so anything that depended on that path cannot be reproduced when the chart reloads (TradingView support: different results after refreshing).
The docs group the causes into four tiers. Two are worth acting on:
| Tier | Examples | Verdict |
|---|---|---|
| Widespread, often acceptable | Using close on the open bar; higher timeframe request.security() tracking an unconfirmed period | Fine if you know it and gate alerts on bar close |
| Potentially misleading | Plotting into the past, calc_on_every_tick = true, varip, timenow, pivots that confirm late | Remove from anything that fires orders |
| Unacceptable | Leaking future data into history, strategies on non standard charts, alerts from intrabar ticks | Never in a strategy that feeds a webhook |
| Unavoidable | Data revisions, the chart's starting bar shifting | Accept and document |
barmerge.lookahead_on and lookahead_off
request.security() takes a lookahead argument. With barmerge.lookahead_off, the default, a historical chart bar only sees a higher timeframe value once that higher timeframe bar has closed. On the live bar, the same call returns the higher timeframe bar that is still forming. The two do not match, and the mismatch is the most common source of "it worked in the backtest" (Pine Script docs: Other timeframes and data).
With barmerge.lookahead_on and no offset, the reverse happens: every historical chart bar inside a daily period sees that day's final close before the day has finished. The backtest knows the future. That is the "unacceptable" tier, and it produces equity curves that cannot be reproduced live.
The documented fix is the two together: offset the expression by one bar and turn lookahead on.
// Same value on historical and live bars: yesterday's confirmed daily close.dailyClose = request.security(syminfo.tickerid, "1D", close[1], lookahead = barmerge.lookahead_on)close[1] inside the request refers to the previous daily bar, which is complete on every chart bar, and lookahead_on lets historical bars read it at the start of the day rather than at the end. Live bars see the same thing. The price is that the value is always one daily bar late, by design (Pine Script docs FAQ: avoiding repainting with request.security()).
Why x[1] alone lags on historical bars
People often add the [1] and keep lookahead_off, thinking the offset is the safe part. It is not. With lookahead off, the historical series only advances when the higher timeframe bar closes, so close[1] on a historical chart bar is the close of the day before the one that most recently finished. Live, the same expression returns yesterday's close. The result is a series that is one period late in real time and two periods late in history, and it still does not line up. The offset and the lookahead flag are one technique, not two options.
calc_on_every_tick
By default a strategy executes once per bar, on the closing tick. calc_on_every_tick = true makes it recalculate on every price update of the live bar, so orders and alert() calls can happen mid bar (Pine Script docs: Bar states). Historical bars have no ticks, so the backtest still evaluates once per bar with the final close. The strategy is now two different strategies, one for the past and one for the present, and TradingView lists it under potentially misleading repainting.
Leave it false for anything that fires a webhook. The signal arrives a few seconds after the bar closes instead of somewhere inside it, and the backtest counted exactly those signals.
barstate.isconfirmed
barstate.isconfirmed is true on every historical bar and on the closing update of the live bar. Put it in the entry condition and the condition can only become true at the moment the bar is final, which is the same moment the backtest evaluated it.
fired = ta.crossover(close, orHigh) and barstate.isconfirmedif fired strategy.entry("L", strategy.long, alert_message = body) alert(body, alert.freq_once_per_bar_close)Two limits. It does not work inside a request.security() expression, so it cannot fix a higher timeframe leak. And in an indicator it is the only thing standing between you and an alert() that fires on the first tick that satisfies the condition, because alert() defaults to once per bar, not once per bar close (Pine Script docs: Alerts). Use both the gate and alert.freq_once_per_bar_close.
process_orders_on_close
A strategy that decides on bar close normally fills on the next bar's open. With process_orders_on_close = true in the declaration, the broker emulator fills at the close of the bar that produced the signal. That is the fill the backtest should assume when the live path is "alert fires at bar close, human confirms, order placed at market", because the price the trader sees on the card is the close the strategy acted on. Pair it with a realistic per order commission and a couple of ticks of slippage so the assumption does not flatter the result.
The one place process_orders_on_close can mislead is stop and target exits. Those fill inside a bar in the emulator, before the script runs on that bar, so an exit alert driven by strategy.exit() alone may not carry the state you expect. Derive exit alerts from the confirmed position change instead:
exitedLong = strategy.position_size[1] > 0 and strategy.position_size == 0exitedShort = strategy.position_size[1] < 0 and strategy.position_size == 0if barstate.isconfirmed and (exitedLong or exitedShort) alert(makeMsg(exitedLong ? "SELL" : "BUY"), alert.freq_once_per_bar_close)How to test for it
None of this needs a special tool. It needs patience and a notebook.
- Reload test. Add the strategy to a chart during market hours, let it run for twenty or thirty bars, note the markers, then reload the page. Every marker that moves or vanishes is a repaint.
- Bar replay. Step through the same session with Bar Replay and compare the signals it produces against the ones the full backtest shows for that day. They should be identical.
- Historical versus live count. Keep a strategy alert running for a week and compare the alert log against the Strategy Tester's trade list for the same week. Mismatched counts point at
calc_on_every_tick, a missingisconfirmedgate, or a higher timeframe request. - Search the source.
lookahead_onwithout a[1]on the expression,calc_on_every_tick = true,varip,timenowin a condition, negativeoffsetvalues in plots, andrequest.security()on a lower timeframe are all reasons to stop and read. - Shift the start. Change the chart's date range so the first bar moves. Functions with a memory, such as
ta.ema(),ta.barssince()andta.valuewhen(), change slightly with the starting point. Small drift is expected; changed trade direction is not.
Checklist
| Check | Pass condition |
|---|---|
calc_on_every_tick | Absent or false |
| Entry and exit conditions | Include barstate.isconfirmed |
alert() frequency | alert.freq_once_per_bar_close |
request.security() higher timeframe | [1] offset with lookahead_on, or completed values with lookahead_off and the lag documented |
request.security() lower timeframe | Not used for signals |
varip, timenow | Not in any condition that places an order |
| Plot offsets | None negative on signal markers |
| Exit alerts | Driven by position change, not by the exit call |
| Reload test | Markers unchanged after refresh |
| Bar replay | Same signals as the backtest for the same session |
Try it in Sarathi
The 13 house strategies in the Studio act on confirmed closes only, keep calc_on_every_tick off, and were tested with ₹350 per order and two ticks of slippage charged. Strategies the Studio writes for you follow the same rules and carry the webhook payload in both alert_message and alert(). Live Each alert becomes an order card in Telegram and on the web dashboard; you tap Confirm at ½, 1× or 2× size, or Skip, and nothing is placed without the tap. The paper broker fills today; Dhan and Zerodha adapters are planned. Planned Open the Studio.
Questions
No. TradingView's own docs say most indicators repaint in some way, because live bars update until they close. The problem is a script whose alerts or orders behave differently live than in the backtest. That is the kind to remove.
It stops one cause: acting on an unconfirmed live bar. It does not fix lookahead leaks inside request.security(), and it does not work inside a request.security() call.
Only if you accept that the backtest cannot reproduce intrabar behaviour. For a webhook bridge that a human confirms, bar close signals with a one bar delay are the honest choice.
Offset the expression by one bar and use barmerge.lookahead_on, so both historical and live bars see the last completed higher timeframe bar. The value is one period late by design.
Related
- Pine Script v5 to v6: what changed and how to migrateThe breaking changes between Pine Script v5 and v6, with a before and after for each, a migration checklist, and the traps the editor's converter does not fix.
- alert() vs alertcondition(): which one fires your webhookThe four combinations of alert(), alertcondition(), strategy and indicator in Pine Script, what each can put in a webhook body, and which frequency to choose.
- The pre-live checklist for a Pine strategyTen checks before a TradingView strategy's alerts reach a broker: v6 compile, repainting, alert payloads, sessions, sizing, paper trades and the journal.