We define repainting as: script behavior causing historical vs realtime calculations or plots to behave differently.
Repainting behavior is widespread and can be caused by many factors. Following our definition, our estimate is that more than 95% of indicators in existence repaint. Widely used indicators like MACD and RSI, for example, exhibit one form of repainting because they show one value on historical bars, yet when running in realtime they will produce results that constantly fluctuate until the realtime bar closes. They thus behave differently on historical and realtime bars. This does not necessarily make them less useful in all contexts, nor prevent knowledgeable traders from using them. Who would think of discrediting a volume profile indicator, for example because it updates in real time, and so repaints?
The different types of repainting we discuss in this page can be divided this way:
calc_on_evey_tick = true
,
scripts using repainting request.security() calls when their values are plotted on historical bars,
as their behavior will not be the same in realtime,
most scripts using varip,
most scripts using timenow,
some scripts using barstate.*
variables).The first two types of repainting can be perfectly acceptable if:
It should now be clear to you that not all repainting behavior is inherently wrong and should be avoided at all cost. In many situations, repainting indicators are exactly what’s needed. What’s important is to know when repainting behavior is not acceptable to you. To avoid such situations, you must understand exactly how the tools you use work, or how you should design the ones you build. If you publish scripts, any potentially misleading repainting behavior should be mentioned along with the other limitations of your script.
Note
We will not discuss the perils of using strategies on non-standard charts, as this problem is not related to repainting. See the Backtesting on Non-Standard Charts: Caution! script for a discussion of the subject.
You can very well decide to use repainting indicators if you understand how they behave, and that behavior meets your trading methodology’s requirements. Don’t be one of those newcomers to trading who slap “repaint” sentences on published scripts as if it discredits them. Doing so only reveals your incomprehension of the subject.
The question “Does it repaint?” means nothing. Consequently, it cannot be answered in a meaningful way. Why? Because it needs to be qualified. Instead, one could ask:
calc_on_every_tick = true
?What’s important is that you understand how the tools you use work, and if their behavior is compatible with your objectives, repainting or not. As you will learn if you read this page, repainting is a complex matter. It has many faces and many causes. Even if you don’t program in Pine Script™, this page will help you understand the array of causes that can lead to repainting, and hopefully enable more meaningful discussions with script authors.
As we discussed in the previous section, not all types of repainting behavior need to be avoided at all costs, and as we will see in the following text, some can’t. We hope this page helps you better understand the dynamics at play, so that you can make better design decisions concerning your trading tools. This page’s content should help you avoid making the most common coding mistakes that lead to repainting or misleading plots.
Whatever your design decisions are, if you publish your script, you should explain them to traders so they can understand how your script behaves.
We will survey three broad categories of repainting causes:
Historical data does not include records of intermediary price movements on bars; only open, high, low and close values (OHLC).
On realtime bars (bars running when the instrument’s market is open), however, the high, low and close values are not fixed; they can change values many times before the realtime bar closes and its HLC values are fixed. They are fluid. This leads to a script sometimes working differently on historical data and in real time, where only the open price will not change during the bar.
Any script using values like high, low and close in realtime is subject to producing calculations that may not be repeatable on historical bars — thus repaint.
Let’s look at this simple script. It detects crosses of the close value (in the realtime bar, this corresponds to the current price of the instrument) over and under an EMA
//@version=5
indicator("Repainting", "", true)
ma = ta.ema(close, 5)
xUp = ta.crossover(close, ma)
xDn = ta.crossunder(close, ma)
plot(ma, "MA", color.black, 2)
bgcolor(xUp ? color.new(color.lime, 80) : xDn ? color.new(color.fuchsia, 80) : na)
Note that:
To prevent this repainting, we must rewrite our script so that it does not use values that fluctuate during the realtime bar. This will require using values from a bar that has elapsed (typically the preceding bar), or the open price, which does not vary in realtime.
We can achieve this in many ways. This method adds a and barstate.isconfirmed
condition to our cross detections, which requires the script to be executing on the bar’s last iteration,
when it closes and prices are confirmed. It is a simple way to avoid repainting
//@version=5
indicator("Repainting", "", true)
ma = ta.ema(close, 5)
xUp = ta.crossover(close, ma) and barstate.isconfirmed
xDn = ta.crossunder(close, ma) and barstate.isconfirmed
plot(ma, "MA", color.black, 2)
bgcolor(xUp ? color.new(color.lime, 80) : xDn ? color.new(color.fuchsia, 80) : na)
This uses the crosses detected on the previous bar
//@version=5
indicator("Repainting", "", true)
ma = ta.ema(close, 5)
xUp = ta.crossover(close, ma)[1]
xDn = ta.crossunder(close, ma)[1]
plot(ma, "MA", color.black, 2)
bgcolor(xUp ? color.new(color.lime, 80) : xDn ? color.new(color.fuchsia, 80) : na)
This uses only confirmed close and EMA values for its calculations
//@version=5
indicator("Repainting", "", true)
ma = ta.ema(close[1], 5)
xUp = ta.crossover(close[1], ma)
xDn = ta.crossunder(close[1], ma)
plot(ma, "MA", color.black, 2)
bgcolor(xUp ? color.new(color.lime, 80) : xDn ? color.new(color.fuchsia, 80) : na)
This detects crosses between the realtime bar’s open
and the value of the EMA from the previous bars. Notice that the EMA is calculated using
close,
so it repaints. We must ensure we use a confirmed value to detect crosses, thus ma[1]
in the cross detection logic
//@version=5
indicator("Repainting", "", true)
ma = ta.ema(close, 5)
xUp = ta.crossover(open, ma[1])
xDn = ta.crossunder(open, ma[1])
plot(ma, "MA", color.black, 2)
bgcolor(xUp ? color.new(color.lime, 80) : xDn ? color.new(color.fuchsia, 80) : na)
Note that all these methods have one thing in common: while they prevent repainting, they will also trigger signals later than repainting scripts. This is an inevitable compromise if one wants to avoid repainting. You just can’t have your cake and eat it too.
The data fetched with request.security() will differ on historical and realtime bars if the function is not used in the correct manner. Repainting request.security() calls will produce historical data and plots that cannot be replicated in realtime. Let’s look at a script showing the difference between repainting and non-repainting request.security() calls
//@version=5
indicator("Repainting vs non-repainting `request.security()`", "", true)
var BLACK_MEDIUM = color.new(color.black, 50)
var ORANGE_LIGHT = color.new(color.orange, 80)
tfInput = input.timeframe("1")
repaintingClose = request.security(syminfo.tickerid, tfInput, close)
plot(repaintingClose, "Repainting close", BLACK_MEDIUM, 8)
indexHighTF = barstate.isrealtime ? 1 : 0
indexCurrTF = barstate.isrealtime ? 0 : 1
nonRepaintingClose = request.security(syminfo.tickerid, tfInput, close[indexHighTF])[indexCurrTF]
plot(nonRepaintingClose, "Non-repainting close", color.fuchsia, 3)
if ta.change(time(tfInput))
label.new(bar_index, na, "↻", yloc = yloc.abovebar, style = label.style_none, textcolor = color.black, size = size.large)
bgcolor(barstate.isrealtime ? ORANGE_LIGHT : na)
This is what its output looks like on a 5sec chart that has been running the script for a few minutes:
Note that:
repaintingClose
.nonRepaintingClose
.This script shows a nonRepaintingSecurity()
function that can be used to do the same as our non-repainting code in the previous example
//@version=5
indicator("Non-repainting `nonRepaintingSecurity()`", "", true)
tfInput = input.timeframe("1")
nonRepaintingSecurity(sym, tf, src) =>
request.security(sym, tf, src[barstate.isrealtime ? 1 : 0])[barstate.isrealtime ? 0 : 1]
nonRepaintingClose = nonRepaintingSecurity(syminfo.tickerid, "1", close)
plot(nonRepaintingClose, "Non-repainting close", color.fuchsia, 3)
Another way to produce non-repainting higher timeframe data is this,
which uses an offset of [1]
on the series, and lookahead
:
nonRepaintingSecurityAlternate(sym, tf, src) =>
request.security(sym, tf, src[1], lookahead = barmerge.lookahead_on)
It will produce the same non-repainting behavior as nonRepaintingSecurity()
.
Note that the [1]
offset to the series and the use of lookahead = barmerge.lookahead_on
are interdependent.
One cannot be removed without compromising the functionality of the function.
Also note that occasional one-bar variations between when the nonRepaintingSecurity()
and nonRepaintingSecurityAlternate()
values come in on historical bars are to be expected.
Some scripts use request.security() to request data from a timeframe lower than the chart’s timeframe. This can be useful when functions specifically designed to handle intrabars at lower timeframes are sent down the timeframe. When this type of user-defined function requires the detection of the intrabars’ first bar, as most do, the technique will only work on historical bars. This is due to the fact that realtime intrabars are not yet sorted. The impact of this is that such scripts cannot reproduce in real time their behavior on historical bars. Any logic generating alerts, for example, will be flawed, and constant refreshing will be required to recalculate elapsed realtime bars as historical bars.
When used at lower timeframes than the chart’s without specialized functions able to distinguish between intrabars, request.security() will only return the value of the last intrabar in the dilation of the chart’s bar, which is usually not useful, and will also not reproduce in real time, so lead to repainting.
For all these reasons, unless you understand the subtleties of using request.security() at lower timeframes than the chart’s, it is best to avoid using the function at those timeframes. Higher-quality scripts will have logic to detect such anomalies and prevent the display of results which would be invalid when a lower timeframe is used.
When request.security()
is used with lookahead = barmerge.lookahead_on
to fetch prices without offsetting the series by [1]
,
it will return data from the future on historical bars, which is dangerously misleading.
While historical bars will magically display future prices before they should be known, no lookahead is possible in realtime because the future there is unknown, as it should, so no future bars exist.
This is an example
// FUTURE LEAK! DO NOT USE!
//@version=5
indicator("Future leak", "", true)
futureHigh = request.security(syminfo.tickerid, "D", high, lookahead = barmerge.lookahead_on)
plot(futureHigh)
Note how the higher timeframe line is showing the timeframe’s high
value before it occurs. The solution is to use the function like we do in our nonRepaintingSecurity()
shown earlier.
Public scripts using this misleading technique will be moderated.
Scripts using the varip declaration mode for variables (see our section on varip for more information) save information across realtime updates, which cannot be reproduced on historical bars where only OHLC information is available. Such scripts may be useful in realtime, including to generate alerts, but their logic cannot be backtested, nor can their plots on historical bars reflect calculations that will be done in realtime.
Scripts using bar states may or may not repaint.
As we have seen in the previous section, using barstate.isconfirmed
is actually one way to avoid repainting that will reproduce on historical bars, which are always “confirmed”.
Uses of other bar states such as barstate.isnew,
however, will lead to repainting. The reason is that on historical bars,
barstate.isnew is true
on the bar’s
close, yet in realtime, it is true
on the bar’s
open.
Using the other bar state variables will usually cause some type of behavioral discrepancy between historical and realtime bars.
The timenow built-in returns the current time. Scripts using this variable cannot show consistent historical and realtime behavior, so they necessarily repaint.
Strategies using calc_on_every_tick = true
execute on each realtime update,
while strategies run on the close
of historical bars. They will most probably not generate the same order executions, and so repaint.
Note that when this happens, it also invalidates backtesting results,
as they are not representative of the strategy’s behavior in realtime.
Scripts detecting pivots after 5 bars have elapsed will often go back in the past to plot pivot levels or values on the actual pivot, 5 bars in the past. This will often cause unsuspecting traders looking at plots on historical bars to infer that when the pivot happens in realtime, the same plots will apppear on the pivot when it occurs, as opposed to when it is detected.
Let’s look at a script showing the price of high pivots by placing the price in the past, 5 bars after the pivot was detected
//@version=5
indicator("Plotting in the past", "", true)
pHi = ta.pivothigh(5, 5)
if not na(pHi)
label.new(bar_index[5], na, str.tostring(pHi, format.mintick) + "\n🠇", yloc = yloc.abovebar, style = label.style_none, textcolor = color.black, size = size.normal)
Note that:
The best solution to this problem when developing script for others is to plot without an offset by default, but give the option for script users to turn on plotting in the past through inputs, so they are necessarily aware of what the script is doing, e.g.
//@version=5
indicator("Plotting in the past", "", true)
plotInThePast = input(false, "Plot in the past")
pHi = ta.pivothigh(5, 5)
if not na(pHi)
label.new(bar_index[plotInThePast ? 5 : 0], na, str.tostring(pHi, format.mintick) + "\n🠇", yloc = yloc.abovebar, style = label.style_none, textcolor = color.black, size = size.normal)
Scripts begin executing on the chart’s first historical bar, and then execute on each bar sequentially, as is explained in this manual’s page on Pine Script™’s execution model. If the first bar changes, then the script will often not calculate the same way it did when the dataset began at a different point in time.
The following factors have an impact on the quantity of bars you see on your charts, and their starting point:
These are the account-specific bar limits:
Starting points are determined using the following rules, which depend on the chart’s timeframe:
As time goes by, these factors cause your chart’s history to start at different points in time. This often has an impact on your scripts calculations, because changes in calculation results in early bars can ripple through all the other bars in the dataset. Using functions like ta.valuewhen(), ta.barssince() or ta.ema(), for example, will yield results that vary with early history.
Historical and realtime bars are built using two different data feeds supplied by exchanges/brokers: historical data, and realtime data. When realtime bars elapse, exchanges/brokers sometimes make what are usually small adjustments to bar prices, which are then written to their historical data. When the chart is refreshed or the script is re-executed on those elapsed realtime bars, they will then be built and calculated using the historical data, which will contain those usually small price revisions, if any have been made.
Historical data may also be revised for other reasons, e.g., for stock splits.