Pine Script v5 to v6 Migration

Fact checked by
Mike Christensen, CFOA
March 11, 2026
Step-by-step guide to migrating your Pine Script v5 scripts to v6, with before-and-after code examples for every breaking change.

Bottom Line

  • The Pine Editor auto-converter handles many v5 to v6 changes but boolean logic and strategy parameters often need manual fixes
  • Replace implicit bool checks like if myInt with explicit comparisons like if myInt != 0
  • Replace strategy when parameters with if blocks wrapping the strategy call
  • Test integer division results carefully as 1/2 now returns 0.5 instead of 0

If you have been writing indicators or strategies on TradingView, you will eventually need a Pine Script v6 migration guide. Pine Script v6 introduced several breaking changes that affect boolean logic, strategy functions, integer division, and history referencing1. This guide walks you through every step of migrating a v5 script to v6, with before-and-after code examples so you can update your scripts with confidence.

While the Pine Editor's built-in converter handles many changes automatically, several critical updates require manual intervention. Understanding each breaking change will save you hours of debugging and ensure your automated trading strategies continue to work correctly after the upgrade.

Before You Start

Before converting any script, take two precautions. First, make a backup of your v5 script. Copy the entire source code into a text file or duplicate the script within TradingView. Second, make sure your v5 script compiles without errors. The Pine Editor converter only works on scripts that compile successfully in v5.

Here is the full list of breaking changes you need to be aware of when migrating from v5 to v6:

  • Values of int and float types are no longer implicitly cast to bool
  • Boolean values can no longer be na, and na(), nz(), and fixnan() no longer accept bool arguments
  • The and/or operators now evaluate conditions lazily (short-circuit evaluation)
  • Division of two const int values can now return a fractional value
  • The when parameter is removed from all strategy functions
  • The default margin percentage for strategies is now 100
  • The history-referencing operator [] can no longer reference literal values or UDT fields directly
  • The offset parameter of plot() no longer accepts series values
  • The value of timeframe.period now always includes a multiplier
  • na values are no longer allowed in place of built-in constants of unique types1

Run the Auto-Converter

The easiest first step is to let TradingView handle what it can. Open your v5 script in the Pine Editor. You will see the //@version=5 annotation highlighted in yellow. Click the editor's "Manage script" dropdown menu and select "Convert code to v6." The converter will update the version annotation, remove deprecated parameters like transp, and adjust some syntax automatically.

In rare cases, the auto-converter produces a v6 script with compilation errors. When that happens, the errors are highlighted in the editor and you need to fix them manually using the steps below.

Fix Boolean Logic

Boolean changes are the most common source of migration errors. Pine Script v6 made three significant changes to how booleans work.

Implicit Casting Removed

In v5, int and float values were implicitly cast to bool when a boolean was expected. Zero and na evaluated as false, and any non-zero value evaluated as true. This no longer works in v6.

v5 code (works):

//@version=5
indicator("Implicit bool demo v5")
// bar_index is an int, implicitly cast to bool
color expr = bar_index ? color.green : color.red
bgcolor(expr)

v6 code (fixed):

//@version=6
indicator("Implicit bool demo v6")
// Must explicitly cast to bool or use a comparison
color expr = bool(bar_index) ? color.green : color.red
bgcolor(expr)

Here are common patterns that need updating:

  • if volume becomes if volume != 0 or if bool(volume)
  • if close - open becomes if close - open != 0 or if close > open
  • if ta.change(month) becomes if bool(ta.change(month))
  • if bar_index becomes if bar_index != 0 or if bool(bar_index)

The bool() function converts numeric values to boolean automatically: na, 0, and 0.0 become false, and any other value becomes true2. Use explicit comparisons when the intent is clearer, such as if volume > 0 instead of if bool(volume).

Booleans Cannot Be na

In v5, a bool variable could hold three states: true, false, or na. In v6, booleans are strictly true or false1. This means:

  • A bool variable can no longer be assigned na as its default value
  • The na(), nz(), and fixnan() functions no longer accept bool arguments
  • Conditional expressions (if/switch) that return bool now return false for unspecified conditions instead of na
  • History-referencing a bool on the first bar returns false instead of na1

v5 code (three-state bool):

//@version=5
strategy("Bool na demo v5", overlay=true, margin_long=100, margin_short=100)

bool isLong = if strategy.position_size > 0
    true
else if strategy.position_size < 0
    false
// When position_size == 0, isLong is na in v5

color stateColor = switch
    isLong == true  => color.new(color.blue, 90)
    isLong == false => color.new(color.orange, 90)
    na(isLong)      => color.new(color.red, 40)
bgcolor(stateColor)

v6 code (use int for three states):

//@version=6
strategy("Bool na demo v6", overlay=true, margin_long=100, margin_short=100)

int tradeDirection = if strategy.position_size < 0
    -1
else if strategy.position_size > 0
    1
else
    0

color directionColor = switch
    tradeDirection == 1  => color.new(color.blue, 90)
    tradeDirection == -1 => color.new(color.orange, 90)
    tradeDirection == 0  => na
bgcolor(directionColor)

The fix is to replace any three-state boolean with an int variable. Use -1, 0, and 1 (or similar) to represent the states you previously handled with true, false, and na.

Fix Strategy Code

Pine Script v6 removed the when parameter and changed default margin behavior for strategies.

Replace the when Parameter

The when parameter was deprecated in v5 and is fully removed in v61. It affected strategy.entry(), strategy.order(), strategy.exit(), strategy.close(), strategy.close_all(), strategy.cancel(), and strategy.cancel_all()2.

v5 code (uses when):

//@version=5
strategy("Conditional strategy v5", overlay=true)

longCondition = ta.crossover(ta.sma(close, 14), ta.sma(close, 28))
strategy.entry("Long", strategy.long, when = longCondition)

shortCondition = ta.crossunder(ta.sma(close, 14), ta.sma(close, 28))
strategy.entry("Short", strategy.short, when = shortCondition)

v6 code (uses if blocks):

//@version=6
strategy("Conditional strategy v6", overlay=true)

longCondition = ta.crossover(ta.sma(close, 14), ta.sma(close, 28))
if longCondition
    strategy.entry("Long", strategy.long)

shortCondition = ta.crossunder(ta.sma(close, 14), ta.sma(close, 28))
if shortCondition
    strategy.entry("Short", strategy.short)

Wrap each strategy call in an if block using the condition that was previously passed to the when parameter.

Update Default Margin

In v5, the default values of margin_long and margin_short were 0, meaning the strategy never checked available funds before placing orders. In v6, the default is 100, which means the strategy will not open positions requiring more capital than available and will margin-call short orders that lose too much1.

If your strategy results look different after migration, check whether the margin change is the cause. To replicate v5 behavior, explicitly set both margin values to 0:

//@version=6
strategy("My strategy", overlay=true, margin_long=0, margin_short=0)

strategy.exit() Parameter Pairs

In v5, when strategy.exit() received both a relative parameter (profit, loss, trail_points) and its corresponding absolute parameter (limit, stop, trail_price), it always used the absolute value and ignored the relative one. In v6, it evaluates both and uses whichever level the market price would trigger first.

Review any strategy.exit() calls that specify both relative and absolute parameters. If you set profit = 0 alongside a limit value, the zero-tick profit distance will trigger first in v6, causing immediate exits. Remove the zero-value relative parameters to keep your intended behavior.

Fix Integer Division

In v5, dividing two const int values performed integer division and discarded the fractional remainder. So 5 / 2 returned 2. In v6, the same division returns 2.51.

v5 behavior:

//@version=5
indicator("Int division v5")
plot(5 / 2)          // Returns 2 (integer division)
plot(5.0 / 2.0)      // Returns 2.5 (float division)

v6 behavior:

//@version=6
indicator("Int division v6")
plot(5 / 2)          // Returns 2.5 (fractional result)
plot(int(5 / 2))     // Returns 2 (explicitly truncated)

If your script relies on integer division to truncate results, wrap the division in int() to discard the fractional remainder. You can also use math.floor(), math.ceil(), or math.round() for specific rounding behavior.

Fix History Referencing

Pine Script v6 restricts the history-referencing operator [] in two important ways.

No History for Literals

In v5, you could apply [] to literal values and built-in constants, like 6[1] or true[10]. This was redundant because literals are fixed values. In v6, this triggers a compilation error1.

v5 code:

//@version=5
indicator("Literal history v5")
plot(6[1], "6[1]", linewidth = 3)
bgcolor(true[10] ? color.orange[3] : na)

v6 code:

//@version=6
indicator("Literal history v6")
plot(6, "6", linewidth = 3)
bgcolor(true ? color.orange : na)

Simply remove the [] operator from any literal values or built-in constants.

UDT Fields Need Parentheses

In v5, you could reference the history of a user-defined type field directly with myObject.field[10]. In v6, you must reference the object's history first, then access the field1. Use parentheses: (myObject[10]).field.

v5 code:

// v5: history-reference on UDT field directly
string txt = infoObject.lblStyle[10]
string diff = infoObject.diff[10]

v6 code:

// v6: history-reference on object, then access field
string txt = (infoObject[10]).lblStyle
string diff = (infoObject[10]).diff

Alternatively, assign the field to a variable first, then reference that variable's history:

string lblStyle = infoObject.lblStyle
string historicStyle = lblStyle[10]

Fix Lazy Evaluation Side Effects

In v5, both sides of and and or expressions were always evaluated (strict evaluation). In v6, these operators use lazy evaluation (also called short-circuit evaluation)1. An and expression stops evaluating if the first argument is false. An or expression stops if the first argument is true.

This matters when the second argument contains a function call that needs to execute on every bar to maintain its internal history, such as ta.rsi(), ta.ema(), or ta.macd()3.

v5 code (works because RSI runs every bar):

//@version=5
indicator("Lazy eval v5")
bool signal = false
if close > open and ta.rsi(close, 14) > 50
    signal := true
bgcolor(signal ? color.new(color.green, 90) : na)

v6 code (broken because RSI skips bars):

//@version=6
indicator("Lazy eval v6 - broken")
bool signal = false
// When close <= open, ta.rsi() is NOT called, corrupting its internal history
if close > open and ta.rsi(close, 14) > 50
    signal := true
bgcolor(signal ? color.new(color.green, 90) : na)

v6 code (fixed by extracting function call):

//@version=6
indicator("Lazy eval v6 - fixed")
// Calculate RSI on every bar in the global scope
float rsi = ta.rsi(close, 14)
bool signal = false
if close > open and rsi > 50
    signal := true
bgcolor(signal ? color.new(color.green, 90) : na)

The fix is simple: extract any function call that relies on historical context to the global scope. Assign the result to a variable, then use that variable inside your boolean expressions.

Note that lazy evaluation also has benefits. In v5, calling array.first() on an empty array inside a compound condition caused a runtime error even if you checked the size first, because both sides were always evaluated. In v6, you can safely write if myArray.size() != 0 and myArray.first() because the second part only executes when the array is not empty.

Handle Other Changes

Plot Offset

The offset parameter of plot() and similar functions no longer accepts series values1. In v5, passing a series int as offset only used the last calculated value anyway, which was almost never the intended behavior. In v6, offset must be a simple value or weaker (simple, input, or const).

// v5: allowed but buggy - only used last bar's value
plot(high, offset = bar_index / 2)

// v6: use a fixed simple value
plot(high, offset = -3)

na Constants Restriction

Parameters expecting unique types (like plot.style_line) no longer accept na1. In v5, passing na to the style parameter of plot() would silently use the default. In v6, this causes a compilation error. Ensure all switch and if expressions returning unique types include a default or else block.

// v5: switch without default could return na
selectedStyle = switch inputStyle
    "Area"    => plot.style_area
    "Columns" => plot.style_columns

// v6: must include default block
selectedStyle = switch inputStyle
    "Area"    => plot.style_area
    "Columns" => plot.style_columns
    => plot.style_line

timeframe.period Format

In v5, timeframe.period returned "D", "W", or "M" for daily, weekly, and monthly charts. In v6, the multiplier is always included: "1D", "1W", "1M"1. Update any string comparisons accordingly.

// v5
if timeframe.period == "D"

// v6
if timeframe.period == "1D"

Other Minor Changes

  • linewidth must be at least 1 (values less than 1 now cause errors)
  • Duplicate parameters in function calls now cause compilation errors instead of warnings
  • The transp parameter is fully removed; use color.new(myColor, 80) instead
  • Array functions like get(), set(), insert(), and remove() now accept negative indices (-1 for last element)
  • Some color constants have changed: color.red is now #F23645, color.teal is #089981, color.yellow is #FDD835
  • Default label text color changed from color.black to color.white
  • For loops now evaluate their end boundary dynamically before each iteration4

Testing Your Migrated Script

After applying all fixes, follow this testing checklist to verify your migrated script works correctly:

  • Compile the script and resolve any remaining errors highlighted in the Pine Editor
  • Compare the visual output of your v6 script against the v5 version on the same chart and timeframe
  • For strategies, compare the Strategy Tester results (total trades, net profit, max drawdown) between versions
  • Check edge cases: first bar behavior, empty arrays, and symbols with gaps in data
  • Test on multiple timeframes to ensure timeframe.period comparisons work correctly
  • Verify that any ta.* function calls execute on every bar and are not skipped by lazy evaluation

Pay special attention to strategy scripts. The combination of margin changes, strategy.exit() parameter pair evaluation, and the when parameter removal can significantly alter backtest results. If results differ, check each change individually to isolate the cause.

Automate Your v6 Strategies

Once your Pine Script v6 migration is complete and your strategy compiles cleanly, you can connect it to live markets through TradersPost. TradersPost receives TradingView alerts from your v6 strategies and automatically routes orders to supported brokers, including futures, stocks, options, and crypto5. No additional code changes are needed beyond what your strategy already generates through its alert messages.

The migration from v5 to v6 is a one-time effort that gives you access to cleaner boolean logic, safer lazy evaluation, dynamic request capabilities, and all future Pine Script features4. Take the time to understand each change, test thoroughly, and your scripts will be more reliable than ever.

References

1 Pine Script v5 to v6 Migration Guide
2 Pine Script v6 Language Reference
3 Pine Script User Manual
4 Pine Script Release Notes
5 TradersPost – Automated Trading Platform

Ready to automate your trading? Try a free 7-day account:
Try it for free ->