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:

TierExamplesVerdict
Widespread, often acceptableUsing close on the open bar; higher timeframe request.security() tracking an unconfirmed periodFine if you know it and gate alerts on bar close
Potentially misleadingPlotting into the past, calc_on_every_tick = true, varip, timenow, pivots that confirm lateRemove from anything that fires orders
UnacceptableLeaking future data into history, strategies on non standard charts, alerts from intrabar ticksNever in a strategy that feeds a webhook
UnavoidableData revisions, the chart's starting bar shiftingAccept 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.

  1. 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.
  2. 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.
  3. 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 missing isconfirmed gate, or a higher timeframe request.
  4. Search the source. lookahead_on without a [1] on the expression, calc_on_every_tick = true, varip, timenow in a condition, negative offset values in plots, and request.security() on a lower timeframe are all reasons to stop and read.
  5. Shift the start. Change the chart's date range so the first bar moves. Functions with a memory, such as ta.ema(), ta.barssince() and ta.valuewhen(), change slightly with the starting point. Small drift is expected; changed trade direction is not.

Checklist

CheckPass condition
calc_on_every_tickAbsent or false
Entry and exit conditionsInclude barstate.isconfirmed
alert() frequencyalert.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 timeframeNot used for signals
varip, timenowNot in any condition that places an order
Plot offsetsNone negative on signal markers
Exit alertsDriven by position change, not by the exit call
Reload testMarkers unchanged after refresh
Bar replaySame 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

Sarathi writes and checks Pine v6 strategies; paper trade before you connect a broker.

Open the Studio
Sarathiसारथी

Sarathi means charioteer. The charioteer drives; the warrior decides when to shoot. Here the software drives, and you confirm every order.

Status

Pre-launch · pre-revenue
Bridge verified live 24 Aug 2026

Get started →

Sarathi is an execution bridge, not an investment adviser, and is not registered with SEBI as a Research Analyst. Nothing on this page is advice or a recommendation to trade. Sarathi never holds your funds or securities; every order is placed by you, on your own broker account. Derivatives trading carries substantial risk of loss.

Chart illustrations use TradingView Lightweight Charts™. Every price on this page is made up.

TradingView and Telegram are trademarks of their respective owners. Sarathi is not affiliated with or endorsed by them.