A practical, code-first walkthrough of moving a script from Pine Script v5 to v6: the version declaration, what genuinely breaks, the namespace and behavioral changes, and a full before/after rewrite. This is educational material about the language itself, not trading or financial advice — and remember that all trading involves the risk of loss, regardless of how clean your code is.
Migrating from Pine Script v5 to v6 is usually less dramatic than people fear. Many well-written v5 scripts that already use namespaced built-ins will compile on v6 with little more than a version bump — but v6 does introduce a few real behavioral changes (most notably around the bool type) that can require code edits, so it is not always a single-character switch. The work is mostly about updating the declaration, then auditing a short list of behavioral and default changes that can silently alter what your plots and conditions do, or in a few cases stop the script from compiling.
The first and most important change is the version comment itself. Every v6 script must begin with the annotation telling the compiler which language version to use:
//@version=6
indicator("My Indicator", overlay = true)If you leave //@version=5 in place, you simply keep running on v5 — nothing breaks, but you also get none of v6's improvements. Changing the number to 6 is what opts you in. From there, the practical question is not just "will it compile?" but "does it still mean the same thing?"
The changes worth auditing fall into three buckets: stricter type handling (most notably that bool can no longer be na, and that int/float are no longer implicitly cast to bool), dynamic-request capabilities (v6 enables dynamic request.* calls by default), and a few changed defaults. None of these are profitability features — they are correctness and clarity features. Knowing the language better helps you avoid bugs; it does not help you predict markets, and nothing in this guide implies otherwise.
Before talking about what's new in v6, it's worth clearing up a common confusion: many things people call "v6 changes" were actually mandatory back in v5. If you are migrating from older code, you may be carrying v4-or-earlier habits that need fixing regardless of version. Getting these right is the foundation of a clean Pine Script v5 to v6 migration.
The big one is namespacing. Bare built-in functions were removed years ago. You cannot call ema(), sma(), rsi(), crossover(), security(), or study() directly. They must be written through their namespaces:
ema(close, 20) becomes ta.ema(close, 20)sma(close, 50) becomes ta.sma(close, 50)rsi(close, 14) becomes ta.rsi(close, 14)crossover(a, b) becomes ta.crossover(a, b)security(...) becomes request.security(...)study(...) becomes indicator(...)abs(), max(), round() become math.abs(), math.max(), math.round()tostring(), tonumber() become str.tostring(), str.tonumber()The other already-required change is the keyword argument syntax. In ancient Pine you might have seen plot(close, transp=70) or strategy.entry("L", strategy.long, when=cond). The transp= parameter is gone — use the alpha channel in color.new(color, transp) instead — and when= is no longer a valid argument on strategy.* calls. You gate those with an if block instead. If your script still contains any of these, fix them first; they will block compilation on both v5 and v6, so they are not really a v6 problem at all.
Once the legacy syntax is clean, focus on the handful of genuine v6 behavioral shifts. Some throw compile errors; others change results quietly. Both are worth knowing before you flip the version number.
Boolean and `na` handling — this is the headline change, and it is the opposite of what many migration write-ups claim. In v5, a bool had three possible states: true, false, or na. In v6, a bool can only be true or false. There is no longer an na boolean. As a direct consequence, na(), nz(), and fixnan() no longer accept `bool` arguments — passing a bool to nz() is now a compile error, not a defensive guard. Expressions that returned a boolean na in v5 now return false in v6. For example, referencing a bool's history on the very first bar (myBool[1] with no prior bar) returned na in v5 but returns false in v6. The practical upshot: conditions built from comparisons and ta.crossover (which already return a plain bool) need no na guard during warm-up — they are simply false until enough history exists.
No implicit numeric-to-bool casting. In v5, an int or float used where a bool was expected was implicitly cast — na, 0, or 0.0 meant false, anything else true. In v6 that implicit cast is gone. If you wrote if myFloat you must now be explicit: either compare (if myFloat != 0) or cast (if bool(myFloat)). This is the most common silent break when migrating older logic.
Dynamic requests. v6 enables dynamic request.*() calls by default — the old dynamic_requests = true flag is no longer needed, and a single request.security instance can pull from different datasets or run inside local scopes. This is a capability gain, so it won't break old code. The compiler turns the feature off automatically if a converted script doesn't need it; if behaviour differs, you can add dynamic_requests = false to the declaration to replicate v5. Importantly, more flexibility does not relax anti-repaint discipline: a higher-timeframe series should still be offset and have look-ahead explicitly turned off.
The safe migration move throughout is to be explicit: pass arguments by name, cast deliberately rather than leaning on implicit coercion, and set lookahead explicitly on every request.security. Explicit code survives version changes far better than code that leans on whatever the default happened to be. (ForexCodes' Audit tool exists precisely to flag these stricter-type and repaint traps automatically, but you can catch most of them by reading carefully.)
Here is a realistic example. Imagine a v5 indicator that plots a fast/slow EMA crossover and pulls the daily close from a higher timeframe. It works, but it has the classic problems: an unguarded higher-timeframe request, and an alert that could fire on an unconfirmed (still-forming) bar — a repainting hazard.
The v5 "before" might look like this:
//@version=5
indicator("EMA Cross + HTF", overlay = true)
fastLen = input.int(12, "Fast EMA")
slowLen = input.int(26, "Slow EMA")
fast = ta.ema(close, fastLen)
slow = ta.ema(close, slowLen)
// HTF daily close — no offset, no explicit lookahead (repaint risk)
dailyClose = request.security(syminfo.tickerid, "D", close)
bull = ta.crossover(fast, slow)
plot(fast, "Fast", color = color.aqua)
plot(slow, "Slow", color = color.orange)
plot(dailyClose, "Daily", color = color.gray)
// fires intrabar — can repaint
alertcondition(bull, "Bull Cross", "EMA crossed up")The rewrite below is the v6 "after". It bumps the version, offsets the higher-timeframe series by [1] with look-ahead explicitly disabled so it cannot peek into the future, and gates the alert on barstate.isconfirmed so it only evaluates on closed bars. Note what it does not need: a nz() guard on the crossover. ta.crossover returns a bool, and in v6 a bool is never na — it is simply false during warm-up — so the old defensive nz() wrapper would now be a compile error, not a safety net.
//@version=6
indicator("EMA Cross + HTF (v6)", overlay = true)
fastLen = input.int(12, "Fast EMA")
slowLen = input.int(26, "Slow EMA")
fast = ta.ema(close, fastLen)
slow = ta.ema(close, slowLen)
// Non-repainting HTF request: confirmed prior bar, no look-ahead
dailyClose = request.security(syminfo.tickerid, "D", close[1], lookahead = barmerge.lookahead_off)
// crossover is already a plain bool (false, never na, during warm-up)
rawBull = ta.crossover(fast, slow)
bull = rawBull and barstate.isconfirmed
plot(fast, "Fast", color = color.aqua)
plot(slow, "Slow", color = color.orange)
plot(dailyClose, "Daily", color = color.gray)
alertcondition(bull, "Bull Cross", "EMA crossed up (confirmed)")The logic is the same; the difference is honesty. The v6 version will not redraw a crossover marker that disappears on the next tick, and the daily value reflects a fully formed bar rather than a still-moving one. That is the whole point of a careful migration — the chart tells you the truth about what already happened, not a flattering version that vanishes on reload.
Treat the v5 to v6 move as a short, repeatable checklist rather than a vibe. Run it on each script in order, and most migrations take only a few minutes.
ema(, sma(, rsi(, security(, study(, abs(, tostring(. If any exist, prefix them with ta., request., math., or str., or replace study with indicator.transp= arguments, no when= on strategy.* calls. Move conditional entries into if blocks; move transparency into color.new().bool is expected (e.g. if myFloat). Make it explicit with a comparison (myFloat != 0) or bool(myFloat), since v6 dropped the implicit cast.bool is never na, and those functions no longer accept bool — such calls now fail to compile. Conditions from comparisons and ta.crossover are already false during warm-up, so the old guards can simply be deleted.lookahead = barmerge.lookahead_off explicitly, and offset the requested series by [1] unless you have a deliberate reason not to.Work through that list and the script will not only compile on v6 — it will behave more predictably than it did before. If you maintain many scripts, automated checkers (ForexCodes' Audit and Fixer among them) can flag the repaint, namespace, and stricter-type issues in bulk, but the checklist above is the same logic you'd apply by hand.
A closing reminder, because it matters more than any syntax detail: this article is educational material about the Pine Script language only. None of these changes — non-repainting requests, confirmed-bar gating, cleaner namespaces — make a strategy profitable or predict price. They make your code honest about the past. Trading carries a real risk of loss, a correct script can still describe a losing idea, and nothing here is financial advice. Migrate for correctness; backtest, paper-trade, and size risk with that clear-eyed understanding.
Educational only — not financial advice. Trading involves substantial risk of loss.