Skip to content
ChartDeck ChartScript docs Account Open the terminal

Repainting

What repainting is, which ChartScript features can repaint (the live bar, developing requests, live state, timenow, drawings), and how to avoid it.

An indicator repaints when what it shows for a bar in the past is different from what it showed while that bar was live. A signal that appeared, then vanished; an arrow that was never there when you were watching; a backtest by eye that looks far better than anything you could have traded. Repainting is not always a bug, but you should always know when your script does it, because history that repaints lies about how a signal would have behaved.

#Why it happens

Your script runs once on every closed bar of the history, and then again and again on the live bar as each update arrives (see bars and time). On history, each bar is seen once, already finished. Live, the same bar is seen many times while it is still moving. Anything that depends on the unfinished bar can look different in the two cases.

ChartScript is built so that, by default, a closed bar shows what the live chart showed at that bar's close. The sources of repainting below are the ways around that default, and each one is something you choose.

#Source 1: the live bar itself

On the live bar, close is the last price, and high, low and volume are still growing. A condition such as ta.crossover(close, avg) can be true at one update and false at the next. If it flickers on and finally ends false, the closed bar shows no signal, while you saw one live.

The fix is to act only on closed bars:

ChartScriptFragmentOpen in editor
const avg = ta.ema(close, 21)
const crossUp = ta.crossover(close, avg) && barstate.isconfirmed
plotshape(crossUp, "Cross up", shape.triangleup, location.belowbar, color.up)

barstate.isconfirmed is true on every history bar and on the final run of the live bar when it closes, so the marker appears at the close and never disappears. The cost is waiting for the close; that is the honest cost of the signal.

state variables are safe here: ChartScript rolls back every state variable on each live update, so a counter like state n = 0 / n += 1 counts bars, not updates, and history matches live.

#Source 2: developing requests

A request with at: request.developing shows a higher-timeframe bar that is still forming, so on the live chart bar its value moves. Closed chart bars keep the value they had at their close, which is exactly what they showed live, so the history does not lie, but a live signal built on it can still appear and vanish before the bar closes.

The default, request.close, never moves during a chart bar, and request.start gives values that were final at the open of the higher bar. Both are safe for signals. Other timeframes and markets has the full table.

#Source 3: live state

A live state variable is not rolled back between live updates; that is its purpose. It can count ticks, remember the highest price seen during the live bar, or measure how long a condition has held. History has no ticks, so on history bars a live state variable only sees one run per bar:

ChartScriptFragmentOpen in editor
live state updates: int = 0
if (barstate.isnew) {
    updates = 0
}
updates += 1
plot(updates, "Updates in this bar")

On history this plots 1 on every bar; live it climbs with every update. Use live state for live-only displays, never for signals you want to judge on history.

#Source 4: timenow and other run-dependent values

timenow is the time of the run, so it is different on every reload. Values computed from it (bars since "now", "the last 24 hours") change as time passes. They are fine for a label on the last bar and wrong for anything plotted across history.

#Source 5: drawings and tables on the last bar

Drawings created inside if (barstate.islast) (labels, lines, tables) are rebuilt on every live update, and a live update's drawing changes are rolled back with it, just like state variables. That is not repainting history: it is a display of now. Keep it that way: do not draw signals on the last bar that you never draw on earlier bars, or the chart will show a mark that has no history behind it.

#Not repainting, but easy to confuse: delayed confirmation

Some tools can only confirm a bar later. A swing high found by ta.pivothigh(high, 5, 5) is known 5 bars after the high itself. If you plot it with offset: -5, the mark sits on the high, which looks perfect on history, but live it appears 5 bars after the fact. The values do not change once shown, so this is not repainting; just be clear in your labels that the mark arrives late.

#A checklist

  1. Signals: require barstate.isconfirmed, or base them on values that cannot move (close[1], a request.close value).
  2. Higher timeframes: keep the default at: request.close; use request.start for values known at the open; use request.developing for display, and confirm signals on closed bars.
  3. live state and timenow: live displays only.
  4. Drawings on the last bar: show the present, not invented history.
  5. Test: add the script, watch a few live bars, then reload the chart. If anything on those bars changed, find out which of the sources above caused it.

#A whole example: a signal that never repaints

This script plots a fast and a slow average and marks crosses only on closed bars. It also shows how far apart the two averages are right now, in a label on the last bar.

ChartScriptOpen in editor
//@chartscript 1
indicator("Confirmed crosses", { overlay: true })

const fastLen = input.int(9, "Fast", { minval: 1 })
const slowLen = input.int(21, "Slow", { minval: 2 })

const fast = ta.ema(close, fastLen)
const slow = ta.ema(close, slowLen)
plot(fast, "Fast", color.accent)
plot(slow, "Slow", color.muted)

const bull = ta.crossover(fast, slow) && barstate.isconfirmed
const bear = ta.crossunder(fast, slow) && barstate.isconfirmed
plotshape(bull, "Bull cross", shape.triangleup, location.belowbar, color.up)
plotshape(bear, "Bear cross", shape.triangledown, location.abovebar, color.down)
alert.on(bull, "Bull cross", "{{ticker}} fast average crossed above slow")
alert.on(bear, "Bear cross", "{{ticker}} fast average crossed below slow")

state gap: label = na
if (barstate.islast) {
    label.delete(gap)
    const pct = (fast - slow) / slow * 100
    gap = label.new(
        bar_index,
        fast,
        "Gap " + str.tostring(pct, "0.00") + " % (live)",
        { style: label.style_label_left, textcolor: color.text, color: color.new(color.muted, 70) }
    )
}

The crosses depend only on closed bars, so the markers you see on history are exactly the ones you would have seen live. The label is a live display and makes no claim about the past.

The ChartScript documentation is in English for now; translations will follow.