Guide

Pine Script v5 to v6: What Changed and How to Migrate

Migrating is mostly a handful of predictable changes. The part worth your attention is the smaller set that leaves your script compiling but quietly behaving differently.

11 min read

If you have a working v5 indicator or strategy and you are staring at a yellow-highlighted version line telling you to upgrade, the good news is that migrating from Pine Script v5 to v6 is mostly a handful of predictable changes. The better news is that TradingView ships an automatic converter that handles most of them for you. The part worth your attention is the smaller set of changes the converter cannot fully fix, especially the ones that leave your script compiling but quietly behaving differently.

This guide walks through what actually changed, grouped by the kind of code it affects, with the exact fix for each. The goal is to get you to a clean v6 script without rewriting your logic from scratch, and without a silent change to your backtest that you only notice weeks later.

Why the v6 changes matter for existing scripts

Most of the Pine Script version 6 changes are not new features you have to learn. They are corrections to behavior that was inconsistent or error-prone in v5, plus a few tightened type rules. A boolean that could secretly be na, a division that rounded differently depending on where its numbers came from, an offset that accepted a value it then ignored: v6 cleans these up. That is good for new code, but it means an old script can be technically valid v5 and still need attention before it runs the same way under v6.

The reason this matters for you is that a Pine Script v6 migration is not only about getting the script to compile again. The changes fall into two buckets. The first is syntax the compiler will reject outright, which is annoying but safe because the editor tells you exactly where to look. The second is behavior that still compiles but produces different output, which is the dangerous kind, because nothing flags it. This guide spends most of its time on that second bucket, since that is where a quietly wrong backtest comes from.

Start with the built-in converter

Before changing anything by hand, use the Pine Editor's converter. Open your v5 script, click the Manage script dropdown, and select Convert code to v6. The script has to compile as valid v5 first, and in most cases the converter does the mechanical work: removing deprecated parameters, rewriting transparency calls, and flagging what it cannot resolve on its own.

What it cannot do is understand your intent. A handful of v6 changes alter behavior rather than syntax, so the converted script compiles but runs differently. Those are the ones to read carefully below. Think of the converter as the first pass and this guide as the checklist for the rest.

Boolean logic: the changes most likely to bite

The largest category of breaking changes is how v6 treats booleans, and it is where a v5 script is most likely to either stop compiling or silently change.

First, integers and floats are no longer implicitly cast to a boolean. In v5 you could write a numeric value where a condition was expected, and zero or na counted as false while anything else counted as true. In v6 that is an error. The fix is to say what you mean by wrapping the number in bool().

pine
// v5: worked because bar_index was implicitly cast to bool
color c = bar_index ? color.green : color.red

// v6: cast the number explicitly
color c = bool(bar_index) ? color.green : color.red

Second, a boolean can no longer be na. In v5, a bool had three possible states: true, false, or na, which was a common source of confusion. In v6 a boolean is strictly true or false, and the na(), nz(), and fixnan() functions no longer accept boolean arguments. This matters most in an if or switch that assigns a bool: any branch you did not explicitly cover now returns false instead of na. If your logic actually relied on that third state, for example to distinguish "no position yet" from "short position," you need to rewrite it using a different type, such as an int carrying -1, 0, and 1.

Third, and easiest to miss, and and or are now evaluated lazily. In v5 both sides of an and were always evaluated even when the first was already false. In v6, once the result is decided, the second side is skipped. That is normally a good thing, but it breaks a specific pattern: if you buried a function that must run on every bar, like ta.rsi(), on the right side of an and, it will no longer run on bars where the left side is false, and its internal history will be wrong.

pine
// v6 risk: ta.rsi() is skipped on bars where close <= open,
// which corrupts its running calculation
bool signal = close > open and ta.rsi(close, 14) > 50

// fix: evaluate the series calculation every bar, in the global scope
float rsiValue = ta.rsi(close, 14)
bool signal = close > open and rsiValue > 50

That last one is a true behavior change, not a compile error, so the converter will not catch it. If you use any ta.* function inside a compound condition, pull it out to its own line first. It is also one of the most common sources of the compile errors we cover in why Pine Script won't compile, since version drift and boolean handling overlap.

Numbers and types

Integer division changed in a way that can shift your numbers. In v5, dividing two constant integers threw away the remainder, so 5 / 2 was 2. In v6, that same division returns 2.5. If a calculation depended on the old rounding-down behavior, wrap the division in int() to discard the fraction, or use math.floor() to be explicit about direction.

Two smaller type changes are worth knowing. Mutable variables that change bar to bar are now correctly treated as series, so if you were passing one as the length of a function like ta.ema(), which needs a simple value, v6 will reject it rather than silently using only its first value. And the offset parameter of plot() and similar functions no longer accepts a series value; it must be simple or weaker.

Strategy changes that alter backtests silently

If you are migrating a strategy rather than an indicator, read this section twice, because these changes can move your entry and exit prices without touching a line of your logic.

The when parameter is gone from the order functions like strategy.entry() and strategy.exit(). It was deprecated in v5 and is removed in v6. Wrap the call in an if instead.

pine
// v5
strategy.entry("Long", strategy.long, when = longCondition)

// v6
if longCondition
    strategy.entry("Long", strategy.long)

The default margin changed from 0 to 100 percent for both long and short. In v5 a strategy with the default never checked whether it had the funds for an order; in v6 it does, which means you can suddenly see margin calls and a different equity curve on the exact same script. To reproduce the old behavior, set margin_long and margin_short to 0 explicitly.

The strategy.exit() function also changed how it handles paired levels. In v5, if you passed both a relative parameter like profit and an absolute one like limit for the same exit, the absolute value always won. In v6 the function evaluates both and uses whichever the price would hit first. A call that mixed the two can now exit at a completely different price. And if a strategy exceeds the 9000-order limit, v6 trims the oldest orders instead of raising an error, so long-running strategies keep going but their earliest trades drop out of the results.

Describe the change you want and let the AI convert against the real v6 function signatures

Loops and requests that now behave differently

Two more changes belong in the silent category, because they compile fine and only show up when you look at the output.

The first is for loop boundaries. In v5, a for loop worked out its end value once, before the first iteration, and held it fixed for the whole loop. In v6, the loop re-evaluates its end boundary before every iteration. If your end value is a plain number this changes nothing, but if you used an expression that mutates inside the loop, such as one that pushes to an array and returns the new size, the loop can now grow its own boundary and run far longer than you intended, sometimes until it hits a runtime error. The fix is to compute the end value once into a variable outside the loop, then use that variable as the loop bound.

The second is dynamic requests. In v6, request.*() functions can run dynamically by default, which lets a single call pull different symbols or timeframes across bars and work inside loops and conditions. That is a genuine upgrade, but the guide notes that in some obscure cases, such as feeding the result of one request.security() call into the expression of another, a valid v5 script can behave differently after conversion. If you notice a data request producing different values in v6, you can set dynamic_requests = false in your indicator() or strategy() declaration to force the old behavior back.

Syntax and everything else

A cluster of smaller changes rounds out the list. The history-referencing operator [] no longer works directly on literals, built-in constants, or the fields of user-defined types. For a UDT field you now reference the object's history first, then read the field, using (myObject[10]).field with the parentheses. Conditional expressions that feed a parameter expecting a unique type, like the style of a plot(), must not be able to return na, so a switch needs a default block and an if needs an else.

The rest are quick: timeframe.period now always includes a multiplier, so a daily chart reads "1D" rather than "D", which breaks any string comparison like timeframe.period == "D". You can no longer pass the same parameter twice in a function call. The transp parameter is removed everywhere, so set transparency with color.new() instead. The minimum linewidth is now 1. And several color.* constants shifted their exact values slightly to match the current TradingView palette, so a plot's color may look marginally different even when nothing in your code changed.

One change works in your favor rather than against you. Functions like array.get(), array.set(), array.insert(), and array.remove() now accept negative indices, so -1 reads the last element and -2 the second to last. Nothing breaks here, but it is worth knowing when you convert Pine Script v5 to v6, because a pattern that raised a runtime error in v5 may now run silently, and you want that to be intentional rather than a surprise.

A v5-to-v6 quick reference

Areav5 behaviorv6 behaviorWhat to do
Numeric to boolImplicitly cast (0/na false)Compile errorWrap in bool()
Boolean natrue, false, or naOnly true or falseRewrite three-state logic with an int
and / orBoth sides evaluatedLazy, short-circuitsPull ta.* calls to the global scope
Const int division5 / 2 = 25 / 2 = 2.5Wrap in int() if you need rounding
strategy.* whenSupportedRemovedUse an if around the call
Default margin0100Set margin_long/margin_short to 0 to match v5
strategy.exit() pairsAbsolute value winsWhichever triggers firstRe-check mixed profit/limit calls
[] on UDT fieldsAllowedNot allowedUse (obj[n]).field
for loop end valueEvaluated onceRe-evaluated each passCompute the bound into a variable first
timeframe.period"D""1D"Update string comparisons
offset parameterAccepts seriesSimple or weaker onlyPass a non-series value
transp parameterHidden but presentRemovedUse color.new()

Migrating a real script without losing your logic

The reliable way to convert Pine Script v5 to v6 is a sequence, not a single button press. Run the converter first, then read the diff it produced with the behavior changes above in mind. Compile the result. If it compiles, do not assume you are done, because the dangerous changes are the ones that compile fine. For a strategy, run the backtest and compare the trade count and equity curve against the v5 version; a difference points you straight at the margin, strategy.exit(), or lazy-evaluation changes. For an indicator, check that anything using a ta.* function inside an and or or still plots the same values, and confirm any for loop with a computed boundary still runs the same number of times.

This is also where an AI that actually knows both versions earns its place in a Pine Script v6 migration. Because PineScripter has the Pine Script v5 and v6 manuals built in through retrieval, it writes and converts against the real v6 function signatures instead of guessing at syntax the way a general model does, and it can explain what a converted line now does differently. If you would rather describe the change you want than hunt through a migration checklist, you can try it free at the pricing page.

The takeaway

The decision to upgrade Pine Script from v5 to v6 is mostly low-risk once you know the pattern. Run the built-in converter, then walk the short list of behavior changes: explicit boolean casting, no boolean na, lazy and and or, fractional integer division, the removed when parameter, the new default margin, the strategy.exit() pairing, and the dynamic for loop boundary. Handle those and your script is v6-ready without a rewrite. The changes that compile but behave differently are the ones worth your attention, so always re-check a migrated strategy's backtest before you trust it.


Disclaimer: PineScripter is a coding tool for Pine Script development. It does not provide financial advice and does not guarantee trading profits. Always backtest strategies thoroughly and understand the risks before live trading.