Create your own
Lesson illustration

Pine Script Execution, Series Values, and Repainting Basics

Hello. This begins the Pine Script Foundations module: the goal is to turn the chart-analysis ideas from the course into transparent, testable TradingView tools rather than treating indicators as black boxes.

Today’s foundation is deceptively important. Pine looks superficially familiar if you have worked with JavaScript or Python, but it is not a long-running event handler or a program that processes one mutable chart object. It is a calculation over a time-indexed sequence of bars. That distinction explains both Pine’s strengths and many misleading signals.

By the end, you should be able to explain what Pine runs on each bar, classify values as const, input, simple, or series, and recognize the most common ways a script can repaint.


1. Pine executes across a chart’s bars

A TradingView chart is a dataset of bars ordered from oldest to newest. On a 1-hour chart, each bar summarizes one hour of price action; on a daily chart, each represents a day. Pine takes your script and evaluates it from top to bottom once for each bar, moving chronologically through that dataset.

So this short script does not produce one moving-average value. It produces a moving-average series, one value per bar:

//@version=6
indicator("20-bar SMA", overlay = true)

length = input.int(20, "Length", minval = 1)
smaValue = ta.sma(close, length)

plot(smaValue, title = "SMA")

When you add it to a chart, TradingView begins at the first available bar, runs these statements, saves the result for that bar, and repeats for the next bar. By the time it reaches the right edge, the connected plotted points form the SMA line.

This means Pine is best understood as a bar-by-bar dataflow language. Variables such as close are not one permanently changing scalar in the usual application-programming sense. They represent a sequence of values indexed by bars.

Execution model of Pinescript | Lesson 3 | Pine script Course

Watch “Execution model of Pinescript” by Pine Wizards for a visual distinction between closed historical candles and the currently forming candle.

Watch the introduction for the two execution contexts. Then watch bar behavior, focusing on the fact that historical bars run once after completion while the live bar can update repeatedly.

Historical bars: final data, one calculation

Every closed bar to the left of the current bar is a historical bar. Its open, high, low, close, and volume are final for the chart dataset. An indicator executes once on each such bar.

For example, on a completed 30-minute bar:

  • open is the first traded price in that 30-minute period.
  • high and low are the final extremes.
  • close is the final price.
  • volume is the final accumulated volume.

Once Pine has calculated an indicator value from those data, the normal expectation is that it remains stable.

The live bar: provisional data and repeated calculation

The rightmost bar is usually the realtime bar. It has opened, but it has not yet closed. As incoming trades update the current price or volume, Pine indicators can run again.

During an open bar:

  • open is fixed once the bar begins.
  • close changes with the latest trade.
  • high may rise if a new intrabar high occurs.
  • low may fall if a new intrabar low occurs.
  • volume accumulates.
  • A condition based on any of these can become true, then false, before the bar closes.
A Litecoin 30-second TradingView chart: shaded bands show successive realtime calculations, while the circled rightmost candle is still open. The moving average and price-dependent conditions can update until that candle closes.

TradingView handles this repeated execution through rollback. Before recalculating an indicator on a new tick of the open bar, Pine restores the script’s state to the last confirmed state at the start of that bar, then runs the calculation using the latest tick data. Only the final calculation at bar close becomes the confirmed historical result.

This is why an indicator may visibly change on the rightmost candle without necessarily being dishonest or defective. It is responding to data that are genuinely still changing. The problem begins when a trader mistakes a provisional intrabar condition for a confirmed signal.

Language / Execution model

Read TradingView’s official “Language / Execution model” documentation. It provides the precise runtime model behind the chart behavior you will observe while developing and testing indicators.

Read the full “Bar-by-bar execution” subsection, then “Storing and using data from previous bars” and “Realtime bars.” In the history subsection, locate history access and connect close[1] to the prior confirmed bar. In “Realtime bars,” continue through the rollback discussion and use closing behavior as a landmark for the final, confirmed calculation.


2. Time series, history, and persistent state

Pine saves confirmed results bar by bar into internal time series. The history-reference operator, [], lets you retrieve past values.

currentClose = close
previousClose = close[1]
closeTenBarsAgo = close[10]

Here, [1] means one completed bar back relative to the current bar. On the first chart bar, close[1] has no prior observation, so its value is na (“not available”).

This is not array indexing in the JavaScript sense. You do not build and manually populate a close array. Pine maintains the bar history for you and evaluates the reference in the context of each current bar.

Many technical-analysis functions use this history internally. For example:

sma20 = ta.sma(close, 20)
highestHigh = ta.highest(high, 20)

On each bar, ta.sma() uses the current close and the relevant preceding closes. Its output therefore changes from bar to bar and is a series value.

Ordinary variables are recalculated on every bar

A common initial misconception is to read this code as if x should grow indefinitely:

x = 0
x += 10

It does not. On each bar, Pine evaluates x = 0, then adds 10, so the final value is 10 on every bar.

To preserve a value across bars, use var:

var int barsSeen = 0
barsSeen += 1

barsSeen initializes once on the first bar and then retains its value as the script moves through subsequent bars. However, var does not make a value permanently fixed, and it does not convert it into a non-series value. It merely changes the initialization and persistence behavior.

There is also varip, which persists even between ticks within the same realtime bar. It is occasionally useful for explicitly intrabar logic, but it deserves caution: intrabar ticks are not retained in historical data after a reload. A script that relies on them can show different past results after refresh.


3. Qualifiers: when a value is known and whether it can vary

In Pine, a base type such as int, float, bool, or string is only half the story. The qualifier says when the value becomes available and whether it may change as the script runs.

The four qualifiers, ordered from most fixed to most dynamic, are:

QualifierWhen establishedCan change from bar to bar?Typical example
constCompile timeNoA literal title such as "Fast SMA"
inputUser settings timeNo during a runinput.int(20, "Length")
simpleFirst runtime barNo after thatsyminfo.ticker, timeframe.period
seriesRuntimeYesclose, volume, ta.rsi(close, 14)

The hierarchy matters because a function that needs a fixed setting cannot safely accept a value that may change bar to bar. A const, input, or simple value can generally be used where a more dynamic qualifier is accepted. But a series value cannot be passed to a parameter that requires simple, input, or const.

const: written into the script

A literal is normally a constant:

const string MA_TITLE = "20-bar SMA"
const int DEFAULT_LENGTH = 20

These exist before the script starts processing the chart. They are useful for items TradingView must know at compilation, such as many titles.

input: chosen in the settings panel

Inputs are user-configurable but remain fixed while the script is running on a dataset:

length = input.int(20, "Moving-average length", minval = 1)
showSignals = input.bool(true, "Show signals")

Changing an input does not merely alter the latest bar. TradingView reloads the script and runs it again across the chart’s history with the new setting.

One important exception: input.source() gives the user a source-selection control, but its result is still a series float, because the selected source is price or another plotted series that varies over bars.

simple: known once the script starts

A simple value is established on the first bar and remains unchanged during that run. Symbol and timeframe metadata are common examples:

tickerName = syminfo.ticker
chartTimeframe = timeframe.period

If you switch symbol or timeframe, TradingView reloads the script, so these can differ on the new run. But they do not vary from one bar to the next within a run.

series: values that participate in chart logic

Price and volume data are series:

isGreenBar = close > open
sma20 = ta.sma(close, 20)
isAboveSma = close > sma20

All three outputs are series. isGreenBar and isAboveSma are series bool; sma20 is a series float.

A useful rule is: if an expression depends on a series, its result is also a series, even if it happens to have the same numeric value on several bars.

Language / Type system

Read the relevant sections of TradingView’s official “Language / Type system” page to make the qualifier vocabulary precise. Focus on qualifiers rather than the later catalogue of all Pine types.

In “Qualifiers,” read the explanation of availability at compile, input, and runtime, plus the compatibility hierarchy. Then read “const,” “input,” “simple,” and “series” in order. In “const,” use literal values as a landmark, then continue through the examples. Pay particular attention to the input.source() exception in “input” and to the fact that an expression depending on a series remains series-qualified.


4. What repainting actually means

In trading discussions, repainting is used loosely. A practical definition is:

A script repaints when a value, signal, or drawing that was previously visible changes or disappears after later information arrives or after the script reloads.

Not every update on an open candle is a fault. Live values are allowed to move. The concern is whether the visual output gives the impression that a historical, actionable signal existed when it could not actually have been known at that time.

A non-repainting script is not necessarily profitable, predictive, or useful. It simply avoids presenting future or temporary information as if it had been confirmed historically.

Cause 1: using the open bar as though it were closed

Consider a moving-average crossover:

fast = ta.ema(close, 10)
slow = ta.ema(close, 30)
crossUp = ta.crossover(fast, slow)

On an open bar, close changes tick by tick, so the two EMAs and crossUp can change too. Price might briefly rise enough to produce a crossover, then fall before the bar closes. After the close, there is no confirmed crossover in history.

For a bar-close signal, explicitly require confirmation:

confirmedCrossUp = ta.crossover(fast, slow) and barstate.isconfirmed

plotshape(
     confirmedCrossUp,
     title = "Confirmed bullish crossover",
     style = shape.triangleup,
     location = location.belowbar,
     color = color.lime
)

On historical bars, barstate.isconfirmed is true. On the realtime bar, it becomes true only at the closing update. This does not prevent the EMA lines themselves from moving intrabar; it prevents the plotted signal from being treated as final before the bar closes.

Cause 2: retaining intrabar state with varip

varip can preserve information from every tick inside a live candle. After a refresh, however, TradingView has the bar’s final OHLCV values, not the original sequence of all ticks that happened during the bar.

For example, a script that records “price crossed above this level at any point during the bar” with varip may display an event in real time that it cannot reconstruct after reload. That is a legitimate source of repainting.

For a beginner rule-based indicator, prefer confirmed bar-close data unless intrabar behavior is essential and clearly labelled as such.

Cause 3: incomplete higher-timeframe data

A 5-minute chart can request a 1-hour value, but the current 1-hour candle remains open for twelve 5-minute bars. Its high, low, close, indicators, and signal conditions are provisional until the hour ends.

A Bitcoin one-minute chart comparing a repainting higher-timeframe `request.security()` result in purple with a confirmed, non-repainting version in green. The purple line moves while the requested higher-timeframe candle is still forming; the green line updates only from completed higher-timeframe information.

The stable principle is simple: use completed higher-timeframe values for historical signals. A common confirmed-data pattern is:

confirmedHourlyClose = request.security(
    syminfo.tickerid,
    "60",
    close[1],
    lookahead = barmerge.lookahead_on
)

Here, close[1] is evaluated in the requested 60-minute context, so it refers to the previous completed hourly candle. The lookahead setting aligns that already-known value across the lower-timeframe bars where it was available. Do not copy this mechanically yet; retain the underlying rule: never use a still-forming hourly close to claim a completed lower-timeframe signal.

Cause 4: using future bars, directly or indirectly

Some chart features require future confirmation. A pivot high, for example, may need several bars to its right before it can be confirmed. This is not inherently wrong, but it becomes misleading if the script draws that pivot back on its original bar without making clear that confirmation occurred later.

Similarly, plotting with a backward visual offset can make an indicator look earlier or more accurate than it was in real time. In future lessons, treat the time a condition becomes known separately from the bar where it is visually displayed.

Cause 5: inconsistent calculations in conditional scopes

This is usually a logic bug rather than classic repainting, but it can create unreliable indicator histories. Functions such as ta.sma(), ta.ema(), and other ta.* functions rely on bar history. Calling them only on selected bars can produce an inconsistent internal history.

Avoid this pattern:

if close > open
    conditionalSma = ta.sma(close, 20)

Prefer calculating the series on every bar, then using the result conditionally:

sma20 = ta.sma(close, 20)
conditionalSma = close > open ? sma20 : na

If the Pine Editor warns that a function “should be called on each calculation for consistency,” treat it seriously. Compute the historical series in global scope, once per bar.


5. A practical anti-repainting review

Before trusting a custom indicator or published script, inspect it with this checklist:

  1. Is the signal based on a completed bar?
    If the intended trading rule is “act at bar close,” the script should gate the signal with barstate.isconfirmed or otherwise make its provisional status explicit.

  2. Does it request another timeframe?
    Verify whether it uses the current, incomplete higher-timeframe candle. If it does, its values can change until that candle closes.

  3. Does it use varip or tick-level logic?
    Expect differences between live behavior and a reloaded chart unless the script has been designed and tested specifically for that limitation.

  4. Does it place marks in the past?
    Inspect pivot logic, negative plot offsets, and any “perfect” historical entry marks. Ask when the condition could first have been known.

  5. Do outputs remain stable after refresh?
    Reloading the chart is a useful basic test. A bar-close indicator should normally retain the same signals on closed bars after reload, apart from rare data-feed corrections.

  6. Are historical functions evaluated every bar?
    Calculate moving averages, RSI, and similar time-series functions globally, rather than only inside branches that sometimes do not run.

Short TradingView observation lab

Open a liquid instrument such as BTCUSD or a major US share on a 5-minute or 15-minute chart. Add any moving average and watch the final candle for a few minutes. Note that the line can move while the candle is open, then becomes fixed after its close.

Next, create a new Pine indicator using the confirmed crossover code above. Apply it to the same chart and observe the distinction: the moving averages can respond throughout the live candle, but the triangle is only committed when the candle closes. Finally, refresh the chart and verify that triangles on earlier closed bars remain in the same places.


Key takeaways

Pine runs chronologically across a chart, once per historical bar and repeatedly on an open realtime bar for indicators. Its internal time series allow references such as close[1], while var allows state to persist across bars.

The qualifier system identifies when values are known: const values are compiled into the script, input values come from settings, simple values are fixed after the first runtime bar, and series values can vary bar by bar. Market data and technical-indicator outputs are normally series.

Finally, repainting most often comes from treating provisional data as confirmed: open-bar conditions, intrabar varip state, incomplete higher-timeframe requests, future-dependent markings, or inconsistent history-dependent calculations. For a beginner’s rule-based signal, confirmation at bar close is the safest default.

Next, you will use these ideas to build a small Pine indicator with user inputs, calculations, and plotted output.

Can't find a good explanation? Sign up and we'll make it for you

Sign up