TradingCharts

Repainting

A script repaints when values displayed on a confirmed historical bar change *after* that bar has closed. Repainting is the main source of "my strategy was profitable in backtest but…

The three kinds of repainting

1. Last-value transfer (always safe)

When a script runs on the forming bar, every bar-dependent calculation uses the current bar values. The moment the bar closes, the final values become the historical values. This is normal and not what we call repainting.

2. Series-mode functions in conditionals (bug)

A series-mode function (ta.sma, ta.rsi, …) must be called every bar for its state to stay in sync. Call it only inside an if branch and its state will diverge from the historical pass:

// ⚠ ta.rsi is skipped on bars where close == open — on those bars rsi reads stale
if close != open
    rsi = ta.rsi(close, 14)
    plot(rsi, "RSI")

The compiler warns in this case. The fix is to compute unconditionally and gate the display:

rsi = ta.rsi(close, 14)                          // computed every bar
plot(close != open ? rsi : na, "RSI")            // displayed selectively

3. request.security(..., lookahead=lookahead_on) (by design)

lookahead_on reads the future close of a higher-timeframe bar before the chart's current bar has reached that time. The historical pass has access to that future value because history is complete; a live script never sees it.

// Repaints: the daily close that today's intraday bars read does not yet exist
dClose = request.security(syminfo.tickerid, "D", close,
                          lookahead = barmerge.lookahead_on)

Use lookahead_off (the default) when you need forward-safe behaviour. See Other timeframes and data.

Rules of thumb

  • Prefer request.security(..., lookahead=barmerge.lookahead_off).
  • Keep series-mode ta.* calls at script scope, not inside conditionals.
  • Confirm strategy entries with barstate.isconfirmed to avoid flapping on the live bar.
  • A plot that uses offset to display a value "in the future" visually (lead indicators) is not repainting — it is relabelling what already happened. Document that in the plot title.

Debugging repaint claims

  • Add a log.info of the computed value at bar close:
if barstate.isconfirmed
    log.info("sig=" + str.tostring(signal) + " @ " + str.format_time(time))

Compare the log sequence between a historical replay and a live chart — any divergence surfaces repainting.

  • Use request.security(..., gaps=barmerge.gaps_on) to see explicitly where the HTF value hasn't formed yet.

Strategy fill-timing

With process_orders_on_close = false (the default), a strategy's market order queues on the signal bar and fills at the next bar's open. The backtest fill is not repainting — the fill is a bar after the signal, matching live behaviour. Setting process_orders_on_close = true fills at the signal bar's close; that is still forward-safe for backtest reporting because the engine processes orders after the bar's close is known.

See also