◆ New — Strategy Validator, Code Fixer & Prop Compliance Checker are live. Try one free →
Home / Learn / var vs varip in Pine Script: Persistent Variables Explained
Pine Script

var vs varip in Pine Script: Persistent Variables Explained

What var and varip actually do in Pine Script v6: var persists across bars but rolls back on every intrabar update, while varip survives those rollbacks and keeps state between ticks. That difference makes varip powerful for live monitoring and dangerous inside strategy logic, because varip-driven behaviour cannot be reproduced by a backtest. Educational material only, not financial advice; no persistence keyword makes a strategy profitable, and trading carries a real risk of loss.

What is the difference between var and varip in Pine Script?

Both keywords make a variable persistent across bars, but they differ on the realtime bar: a var variable is rolled back to its last bar-close value at the start of every intrabar update, while a varip variable keeps whatever value it reached on the previous tick. In other words, var persists per bar; varip ("var intrabar persist") persists per update. On historical bars — where the script runs exactly once per bar — the two behave identically.

That last sentence is the one this article exists for, so it is worth restating plainly: the entire difference between var and varip only exists on live, streaming bars. History has no ticks to persist across. A varip variable examined over historical bars behaves exactly like var, which means any logic whose behaviour depends on intrabar persistence has no historical counterpart at all — a fact with sharp consequences for backtesting that we treat as a correctness risk, not a neat trick.

For completeness, the third option is no keyword at all: a plain declaration re-initialises on every bar. The ladder looks like this:

  • int x = 0 — reset every bar.
  • var int x = 0 — initialised once on the first bar, persists bar to bar, rolls back between intrabar updates, commits at bar close.
  • varip int x = 0 — initialised once, persists bar to bar and tick to tick; never rolled back.

varip works with fundamental types — int, float, bool, color, string — and collections of them. Most scripts never need it, and that is the healthy default: reach for varip only when you can say precisely why bar-close semantics are not enough, and only in code that does not feed a backtest.

Rollback and commit: the execution model underneath

The var/varip split only makes sense once you see Pine's realtime execution model, which is built around rollback and commit.

On historical bars, life is simple: the script executes once per bar, on that bar's final OHLCV values. On the realtime bar, the script re-executes on every price update — possibly hundreds of times before the bar closes. If each of those executions permanently mutated your variables, a bar that updated 300 times would increment your counters 300 times more than a historical bar ever could, and your script's state would depend on tick arrival — something history does not record.

Pine's solution: before each intrabar execution, it rolls back all variables — plain and var alike — to the values they held after the last confirmed bar. Each tick therefore computes as if the realtime bar were being seen fresh, just with updated OHLCV. When the bar finally closes, the results of that last execution are committed and become the permanent history the next bar builds on.

Rollback is why a well-written script produces the same output whether it computed a bar live or loaded it from history — the property usually discussed under "repainting." The committed state is identical either way.

varip is the documented escape hatch. A varip variable is exempt from rollback: whatever it held after the previous tick, it still holds on the next one. That lets a script see and accumulate the tick-by-tick texture of the live bar — and it surgically removes the guarantee rollback exists to provide. State now depends on the update sequence, and the update sequence is precisely what historical data does not contain. Nothing about varip is a bug; it is a deliberate trade of reproducibility for intrabar visibility. The question is always whether the code downstream can afford that trade.

A minimal demonstration: counting bars versus counting updates

The cleanest way to internalise the difference is a script where var and varip receive identical treatment and diverge anyway:

//@version=6
// Educational only — validate before trading; not financial advice.
indicator("var vs varip: bars vs updates")

var int barCount     = 0  // rolls back each tick, commits at bar close
varip int updateCount = 0  // survives every rollback

barCount     += 1
updateCount  += 1

plot(barCount, "var: committed bars", color = color.new(color.teal, 0))
plot(updateCount, "varip: every update", color = color.new(color.orange, 0))

Both counters are incremented by the same line of code on every execution. On historical bars the two plots are identical — one execution per bar, one increment each, and the lines climb in lockstep.

Then the realtime bar arrives and they split. Each tick, barCount is rolled back to its committed value and incremented once, so it reads committed bars + 1 all the way through the live bar and gains exactly one at the close — tick count irrelevant. updateCount keeps every increment: on an active symbol it sprints ahead of barCount, and the gap between the two lines is a live census of how many updates the current session's bars have received.

Now the punchline. Refresh the page, or reopen the chart tomorrow. The realtime bars you watched are now historical bars — executed once each — and updateCount collapses back into agreement with barCount. All the intrabar structure it accumulated is gone, unrecoverable, because it was never part of committed history. A varip variable is a note scribbled during the live session, not an entry in the ledger. That evaporation-on-reload is harmless in a counter demo; the next section shows what it does inside a strategy.

Why varip logic is unreproducible in a backtest

Put varip in a strategy's entry logic and you have built a strategy whose backtest is structurally incapable of describing its live behaviour. Here is the ❌ wrong version — a "tick burst" entry that counts consecutive upticks inside the live bar:

// ❌ WRONG — varip drives entries; history cannot reproduce this.
//@version=6
strategy("Tick burst — unreproducible", overlay = true)

varip float lastPrice = na
varip int   upTicks   = 0

if barstate.isnew
    upTicks := 0
if close > lastPrice
    upTicks += 1
lastPrice := close

if upTicks > 5 and strategy.position_size == 0
    strategy.entry("Long", strategy.long)

Live, this fires whenever six upticks stack up inside a bar. But on historical bars the script runs once per bar, so upTicks can only ever reach 0 or 1 — the condition upTicks > 5 is unsatisfiable, and the Strategy Tester reports zero trades. The backtest is not merely optimistic or pessimistic; it is describing a different strategy. Worse, the live trade list itself is unrepeatable: reload the chart and the recomputed history shows none of the entries you watched fire. There is no dataset anywhere on which this logic can be audited.

The fix is to restate the idea in bar-close terms, where history and realtime agree — for example, consecutive up closes instead of upticks:

//@version=6
// Educational only — validate before trading; not financial advice.
strategy("Confirmed momentum — reproducible", overlay = true)

runLen  = input.int(5, "Consecutive up closes", minval = 1)
stopPts = input.int(300, "Stop (points)", minval = 1)

upRun = 0
upRun := close > close[1] ? nz(upRun[1]) + 1 : 0

if upRun >= runLen and barstate.isconfirmed and strategy.position_size == 0
    strategy.entry("Long", strategy.long)

if strategy.position_size > 0
    strategy.exit("Exit", "Long",
         stop = strategy.position_avg_price - stopPts * syminfo.mintick)

It is not the same signal — that is the honest cost. But it is a signal that fires identically on history and live, which is the minimum bar for a backtest to mean anything at all.

When varip is legitimate — and how to keep it honest

None of this makes varip a keyword to ban. It has real, defensible uses — the test is whether the varip-derived value stays on the observation side of your script or crosses into the decision side.

Legitimate uses are monitoring and diagnostics: counting updates per bar as a crude activity gauge, timestamping when inside a bar a level first broke, measuring tick-to-tick velocity for a live dashboard table, or accumulating an intrabar delta readout to watch during a session. In all of these, the value is displayed, logged, or eyeballed — and everyone understands it evaporates on reload. Pine's own documentation is candid that calculations relying on varip cannot be replicated on historical bars; treat that as a labelling requirement for anything you build with it.

Guardrails that keep it honest:

  • Never let varip state reach strategy.entry(), strategy.exit(), or an alert condition. The moment it does, your tester results and your live behaviour describe different systems, and no amount of walk-forward or Monte Carlo analysis downstream can repair inputs that were never comparable.
  • Prefix and comment. A naming convention like ipTickCount plus a one-line comment ("realtime-only; resets on reload") makes the unreproducible region of the script visible during review instead of discoverable during an incident.
  • Expect resets. A varip variable is reinitialised whenever the script recompiles, settings change, or the chart reloads — mid-session. Any consumer of the value must tolerate it snapping back to its initial state at an arbitrary moment.
  • Audit for leakage. The failure mode is rarely a varip sitting directly in an entry condition; it is a varip feeding a variable that feeds a condition three assignments later. Trace the data flow, or use tooling — ForexCodes' audit checks flag varip state that reaches order or alert logic for exactly this reason.

Educational material only, not financial advice: var and varip are state-management tools, a passing backtest is not a live edge, and trading always carries a real risk of loss.

Key takeaways

Educational only — not financial advice. Trading involves substantial risk of loss.

Catch the bug that compiles.Run auditGet Pro