Pine Script v6 Breaking Changes

Fact checked by
Mike Christensen, CFOA
March 11, 2026
Complete list of Pine Script v6 breaking changes that will affect your v5 scripts, with fixes for each one.

Bottom Line

  • Pine Script v6 has 13 breaking changes from v5 including removal of implicit bool casting, no more na booleans, and lazy and/or evaluation
  • The Pine Editor converter handles many changes automatically but boolean logic, strategy parameters, and history referencing often need manual fixes
  • Integer division now returns fractional values (1/2 = 0.5 instead of 0) which can silently change strategy behavior
  • The when parameter is removed from all strategy functions and must be replaced with if blocks

Pine Script v6 brings significant improvements to TradingView's scripting language, but it also introduces breaking changes that will stop your v5 scripts from compiling or, worse, silently alter their behavior1. This guide covers every Pine Script v6 breaking change with before-and-after code so you can update your scripts quickly and confidently.

Whether you are migrating a personal indicator or maintaining a published library, understanding these changes is essential. The Pine Editor's built-in converter handles some of them automatically, but many require manual attention2. Below you will find each breaking change numbered, explained, and paired with the exact fix.

1. No Implicit Int/Float to Bool Casting

In Pine Script v5, integer and float values were implicitly cast to boolean wherever a bool was expected. Zero and na evaluated to false, and any nonzero value evaluated to true2. This made it easy to write shorthand conditionals like if bar_index or if myCount.

In v6, this implicit casting is removed entirely2. You must explicitly compare numeric values to produce a boolean result, or wrap them with the bool() function3.

v5 (implicit casting worked)

//@version=5
indicator("Implicit cast demo")
int count = 5
if count
    label.new(bar_index, high, "Has count")

color expr = bar_index ? color.green : color.red
bgcolor(expr)

v6 (explicit comparison required)

//@version=6
indicator("Explicit cast demo")
int count = 5
if count != 0
    label.new(bar_index, high, "Has count")

color expr = bool(bar_index) ? color.green : color.red
bgcolor(expr)

How to fix: Replace any bare numeric value used in a boolean context with an explicit comparison (!= 0, > 0, etc.) or wrap it with bool(). The converter handles many of these automatically, but review every conditional that tests an int or float directly.

2. Booleans Cannot Be na

Pine Script v5 allowed booleans to hold three states: true, false, or na. The functions na(), nz(), and fixnan() all accepted bool arguments2. This three-state behavior was a frequent source of subtle bugs, because na booleans behaved differently from false when compared with ==.

In v6, booleans are strictly true or false. Assigning na to a bool variable is a compilation error2. The na(), nz(), and fixnan() functions no longer accept bool arguments3. Any conditional expression (if, switch) that returns a bool will return false instead of na for unhandled branches.

v5 (bool could be na)

//@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 (use int or enum 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 stateColor = switch
    tradeDirection == 1  => color.new(color.blue, 90)
    tradeDirection == -1 => color.new(color.orange, 90)
    tradeDirection == 0  => na
bgcolor(stateColor)

How to fix: Remove all na(), nz(), and fixnan() calls on boolean values. If your logic genuinely requires a third state, replace the bool with an int variable using values like -1, 0, and 1 to represent each state.

3. Lazy Evaluation for and/or

In v5, both sides of an and or or expression were always evaluated, regardless of the first operand's result. This is called strict evaluation. In v6, Pine Script uses lazy (short-circuit) evaluation: if the left operand of and is false, the right operand is skipped entirely. If the left operand of or is true, the right operand is skipped2.

This matters most when the right operand contains a function that relies on being called on every bar to maintain its internal history, such as ta.rsi(), ta.ema(), or ta.macd().

v5 (both sides always evaluated)

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

v6 (right side may be skipped)

//@version=6
indicator("Lazy evaluation v6")
// Extract the function call to global scope so it runs on every bar
float rsiValue = ta.rsi(close, 14)

bool signal = false
if close > open and rsiValue > 50
    signal := true
bgcolor(signal ? color.new(color.green, 90) : na)

How to fix: Move any function call that depends on historical state (indicators, ta.* functions) out of the and/or expression and into a variable at the global scope. Then reference that variable in your conditional. The converter does not fix this automatically.

4. Dynamic request.*() Functions

In v5, all request.*() functions required simple string arguments for ticker and timeframe, meaning they had to be known at compile time and could not change between bars2. Functions containing request.*() calls could not be placed inside loops or conditionals unless dynamic_requests = true was explicitly set in the declaration.

In v6, dynamic requests are enabled by default1. The compiler accepts series string arguments, and request.*() calls can appear inside for loops, if blocks, and library exports without any special flag3.

v5 (static requests only by default)

//@version=5
indicator("Static requests v5", dynamic_requests = true)
var array<string> symbols = array.from("NASDAQ:AAPL", "NASDAQ:MSFT")
for [i, sym] in symbols
    float reqClose = request.security(sym, "1D", close)

v6 (dynamic by default)

//@version=6
indicator("Dynamic requests v6")
var array<string> symbols = array.from("NASDAQ:AAPL", "NASDAQ:MSFT")
for [i, sym] in symbols
    float reqClose = request.security(sym, "1D", close)

Potential issue: In rare cases, a v5 script that relied on non-dynamic request behavior can produce different results after conversion. If you notice differences, add dynamic_requests = false to your declaration statement to replicate the old behavior. Also note that in v6, if dynamic_requests is explicitly set to false, wrapped request.*() calls inside local scopes will now cause a compilation error, unlike v5.

5. Integer Division Returns Fractional Values

This is one of the most dangerous Pine Script v6 breaking changes because it can silently alter your strategy's calculations without producing any error.

In v5, dividing two const int values performed integer division, discarding the fractional remainder. So 5 / 2 returned 2. However, if either operand was qualified as input, simple, or series, the result preserved the fraction (2.5)2. This inconsistency was confusing.

In v6, dividing two integers always returns the fractional result regardless of qualifier2. 5 / 2 now returns 2.5, and 1 / 2 returns 0.5 instead of 0.

v5 (integer division for const int)

//@version=5
indicator("Int division v5")
plot(5 / 2)          // Plots 2
plot(1 / 2)          // Plots 0
plot(int(5) / int(2)) // Plots 2

v6 (fractional division always)

//@version=6
indicator("Int division v6")
plot(5 / 2)          // Plots 2.5
plot(1 / 2)          // Plots 0.5
plot(int(5 / 2))     // Plots 2 (explicitly cast back to int)

How to fix: Wrap any integer division where you need the old truncated result with int(), or use math.floor(), math.round(), or math.ceil() depending on the rounding behavior you want. Search your scripts for / operations between integer literals and verify each one.

6. when Parameter Removed from Strategy Functions

The when parameter was deprecated in Pine Script v5 and is fully removed in v62. It previously appeared in strategy.entry(), strategy.order(), strategy.exit(), strategy.close(), strategy.close_all(), strategy.cancel(), and strategy.cancel_all()3.

Any script using when will fail to compile in v6.

v5 (when parameter)

//@version=5
strategy("When demo 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 (if blocks required)

//@version=6
strategy("When demo 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)

How to fix: Replace every when = condition argument with a wrapping if condition block. The converter handles this change automatically in most cases.

7. Default Margin Changed to 100%

In v5, the default values for margin_long and margin_short in the strategy() declaration were 0, meaning the strategy never checked available funds before placing orders2. It could open positions larger than the account balance and would never trigger margin calls on short positions.

In v6, both default to 100, meaning the strategy enforces realistic margin requirements2. It will not open entries requiring more capital than available, and short positions will be margin-called if losses exceed available funds.

v5 (no margin enforcement by default)

//@version=5
strategy("Margin demo v5", overlay=true,
     default_qty_type=strategy.percent_of_equity,
     default_qty_value=100)
// margin_long=0, margin_short=0 by default

v6 (100% margin enforced by default)

//@version=6
strategy("Margin demo v6", overlay=true,
     default_qty_type=strategy.percent_of_equity,
     default_qty_value=100)
// margin_long=100, margin_short=100 by default
// To replicate v5 behavior:
// margin_long=0, margin_short=0

How to fix: If your strategy relied on the old behavior of unlimited margin, explicitly add margin_long = 0, margin_short = 0 to your strategy() declaration. Otherwise, expect to see margin calls in your backtests that did not appear in v5. This is actually a more realistic simulation for most traders.

8. Trade Limit Trimming (9000 Orders)

In v5, when a strategy generated more than 9,000 orders outside of Deep Backtesting mode, it raised a runtime error and halted all further calculations2. This was especially problematic for high-frequency strategies on long time periods.

In v6, the strategy no longer halts. Instead, it trims the oldest orders from the beginning until the total count is back within the 9,000 limit2. Trimmed orders disappear from the Strategy Tester, and referencing them through strategy.closedtrades.* functions returns na.

v5 (runtime error at 9000 orders)

//@version=5
strategy("Order limit v5", overlay=true, pyramiding=5)
if bar_index % 2 == 0
    for i = 1 to 5
        strategy.entry("Entry " + str.tostring(i), strategy.long, qty = 5)
else
    strategy.entry("Short", strategy.short, qty = 25)
// Throws runtime error once 9000 orders exceeded

v6 (oldest orders trimmed automatically)

//@version=6
strategy("Order limit v6", overlay=true, pyramiding=5)
if bar_index % 2 == 0
    for i = 1 to 5
        strategy.entry("Entry " + str.tostring(i), strategy.long, qty = 5)
else
    strategy.entry("Short", strategy.short, qty = 25)
// No error. Oldest orders trimmed. Use strategy.closedtrades.first_index
// to find the index of the first non-trimmed trade.

How to fix: No code changes needed for compilation. However, if your scripts loop through closed trades by index starting at 0, use strategy.closedtrades.first_index as your starting index to avoid referencing trimmed (now na) trades.

9. strategy.exit() Evaluates Both Relative and Absolute Parameters

The strategy.exit() function has three pairs of relative and absolute parameters: profit/limit (take-profit), loss/stop (stop-loss), and trail_points/trail_price (trailing stop activation)3. In v5, when both the relative and absolute parameter in a pair were specified, the absolute parameter always won and the relative one was silently ignored.

In v6, the function evaluates both parameters in each pair and uses whichever price level the market would trigger first2. This is a significant behavioral change that can drastically alter strategy results.

v5 (absolute always takes priority)

//@version=5
strategy("Exit pairs v5", overlay=true, margin_long=100, margin_short=100)
float atr = ta.atr(14)

if bar_index % 28 == 0
    strategy.entry("Buy", strategy.long)
    // profit=0 is ignored because limit is also specified
    strategy.exit("Exit", "Buy",
         profit = 0, limit = close + 2.0 * atr,
         loss = 0, stop = close - 2.0 * atr)

v6 (both are evaluated, closest triggers first)

//@version=6
strategy("Exit pairs v6", overlay=true, margin_long=100, margin_short=100)
float atr = ta.atr(14)

if bar_index % 28 == 0
    strategy.entry("Buy", strategy.long)
    // profit=0 means exit at entry price (0 ticks away), which triggers
    // before limit, so the trade exits immediately!
    strategy.exit("Exit", "Buy",
         profit = 0, limit = close + 2.0 * atr,
         loss = 0, stop = close - 2.0 * atr)
    // Fix: remove the relative params if you only want absolute levels
    // strategy.exit("Exit", "Buy",
    //      limit = close + 2.0 * atr,
    //      stop = close - 2.0 * atr)

How to fix: Audit every strategy.exit() call that specifies both a relative and absolute parameter for the same exit type. Remove whichever parameter you do not actually need. If you had profit = 0 or loss = 0 as throwaway values alongside limit and stop, remove the zero-valued relative parameters to preserve v5 behavior.

10. History Referencing [] Restrictions

Pine Script v6 introduces two restrictions on the history-referencing operator [].

No History for Literal Values

In v5, you could apply [] to literal values and built-in constants, such as 6[1] or true[10] or color.red[3]. While this rarely did anything useful (a literal always has the same value), it compiled without error. In v6, applying [] to a literal or constant is a compilation error2.

v5 (worked but was pointless)

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

v6 (remove [] from literals)

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

No History for UDT Fields Directly

In v5, you could use the [] operator directly on a field of a user-defined type object, like myObject.field[10]. While this compiled, the behavior was erroneous2. In v6, this syntax is a compilation error.

v5 (direct field history referencing)

//@version=5
indicator("UDT history v5", overlay=true)
type Settings
    bool isUp = false
    string diff

Settings info = Settings.new()
info.isUp := close > open
info.diff := str.tostring((close - open) / open * 100, "#.##") + "%"

if barstate.islast
    string historicDiff = info.diff[10]  // Direct field history

v6 (reference object history, then access field)

//@version=6
indicator("UDT history v6", overlay=true)
type Settings
    bool isUp = false
    string diff

Settings info = Settings.new()
info.isUp := close > open
info.diff := str.tostring((close - open) / open * 100, "#.##") + "%"

if barstate.islast
    // Option 1: History of object, then field
    string historicDiff = (info[10]).diff

    // Option 2: Assign field to variable, then history of variable
    string diffValue = info.diff
    string historicDiff2 = diffValue[10]

How to fix: For literals, simply remove the [] operator. For UDT fields, either reference the object's history first with parentheses (myObject[n]).field, or assign the field to an intermediate variable and apply [] to that variable.

11. No Duplicate Function Parameters

In v5, you could accidentally pass the same parameter twice in a function call. The compiler would issue a warning but still compile, using only the first value. In v6, specifying the same parameter more than once is a compilation error2.

v5 (compiled with warning)

//@version=5
indicator("Duplicate params v5")
// color specified twice - only first (blue) is used
plot(close, "Close", color = color.blue, linewidth = 2, color = color.red)

v6 (compilation error)

//@version=6
indicator("Duplicate params v6")
// Remove duplicate parameter
plot(close, "Close", color = color.blue, linewidth = 2)

How to fix: Remove the duplicate parameter assignment. Keep whichever value you actually want. The converter handles this automatically.

12. plot() Offset No Longer Accepts Series

In v5, the offset parameter of plot() and similar functions accepted series int arguments. However, the behavior was buggy: only the last calculated offset value was applied to the entire chart2. A compiler warning was issued, but the script still compiled.

In v6, offset requires a value qualified as simple int or weaker (input int or const int)3. Passing a series value triggers a compilation error.

v5 (series offset accepted but buggy)

//@version=5
indicator("Offset demo v5", overlay=true)
int seriesOffset = bar_index / 2
plot(math.max(close, open), "", color.orange, 4,
     plot.style_stepline, offset = seriesOffset)

v6 (must use simple or const int)

//@version=6
indicator("Offset demo v6", overlay=true)
int fixedOffset = input.int(-5, "Plot offset")
plot(math.max(close, open), "", color.orange, 4,
     plot.style_stepline, offset = fixedOffset)

How to fix: Replace any dynamic series value passed to offset with a constant or input value. If you need a dynamic visual shift, consider using line.new() or label.new() instead of plot().

13. na Values for Built-in Constants of Unique Types

Some Pine Script parameters expect values of unique types, such as plot.style_line, xloc.bar_index, or label.style_arrowup. In v5, you could pass na where these constants were expected, and the function would silently use its default value.

In v6, passing na to a parameter that expects a unique type constant is a compilation error2. Additionally, conditional expressions (if/switch) that return unique types must include a default branch to ensure they never return na.

v5 (na accepted for unique types)

//@version=5
indicator("Unique types v5")
string inputStyle = input.string("Area", "Plot style",
     options = ["Area", "Columns"])

selectedPlotStyle = switch inputStyle
    "Area"    => plot.style_area
    "Columns" => plot.style_columns
// No default block - returns na for any other value
// v5 silently uses plot.style_line as default

plot(close, "Source", style = selectedPlotStyle)

v6 (default branch required)

//@version=6
indicator("Unique types v6")
string inputStyle = input.string("Area", "Plot style",
     options = ["Area", "Columns"])

selectedPlotStyle = switch inputStyle
    "Area"    => plot.style_area
    "Columns" => plot.style_columns
    => plot.style_line  // Default branch required in v6

plot(close, "Source", style = selectedPlotStyle)

How to fix: Add a default branch (=> in switch, or else in if) to every conditional expression that returns a unique type constant. Ensure no na is ever passed to these parameters.

14. timeframe.period Always Includes a Multiplier

In v5, the timeframe.period variable returned strings without a multiplier when the chart was on a 1-unit timeframe. A daily chart returned "D", a weekly chart returned "W", and a monthly chart returned "M".

In v6, the multiplier is always included: "1D", "1W", "1M"2. This affects any script that compares timeframe.period to a hardcoded string.

v5 (no multiplier for 1-unit timeframes)

//@version=5
indicator("Timeframe v5")
bool isDaily = timeframe.period == "D"       // true on daily chart
bool isWeekly = timeframe.period == "W"      // true on weekly chart

v6 (multiplier always present)

//@version=6
indicator("Timeframe v6")
bool isDaily = timeframe.period == "1D"      // true on daily chart
bool isWeekly = timeframe.period == "1W"     // true on weekly chart

// Or use timeframe.isdaily, timeframe.isweekly for cleaner code
bool isDailyAlt = timeframe.isdaily

How to fix: Search your scripts for string comparisons against timeframe.period using values like "D", "W", or "M" and add the "1" prefix. Better yet, use the built-in boolean variables like timeframe.isdaily, timeframe.isweekly, and timeframe.ismonthly where possible.

Using the Pine Editor Auto-Converter

Before manually fixing any of the changes above, try the built-in v5-to-v6 converter in the Pine Editor4. Open your script, click the "Manage script" dropdown menu, and select "Convert code to v6." The script must compile successfully in v5 before conversion.

What the Converter Handles Automatically

  • Removing the when parameter and wrapping calls in if blocks
  • Removing duplicate function parameters
  • Removing the deprecated transp parameter and replacing it with color.new()
  • Basic implicit bool casting fixes using bool()
  • Removing [] from literal values

What You Must Fix Manually

  • Lazy evaluation issues where ta.* functions are inside and/or expressions
  • Boolean na logic that requires restructuring to use int variables
  • Integer division changes that silently alter calculations
  • Mixed relative/absolute parameters in strategy.exit()
  • UDT field history referencing syntax
  • timeframe.period string comparisons
  • Default margin percentage changes affecting strategy backtest results

The converter may occasionally produce code that does not compile. When this happens, the Pine Editor highlights the errors and you can use the information in this guide to resolve them one by one.

Migration Checklist

Use this checklist when migrating any Pine Script v5 strategy or indicator to v6:

  • Run the auto-converter first and compile to see remaining errors
  • Search for bare int/float values in conditionals and add explicit comparisons
  • Remove all na(), nz(), fixnan() calls on bool values
  • Move ta.* function calls out of and/or expressions to global scope variables
  • Check all integer division operations, especially in position sizing and lot calculations
  • Replace every when = parameter with an if block
  • Verify margin_long and margin_short are explicitly set if you need non-default values
  • Audit strategy.exit() calls that mix relative and absolute parameters
  • Fix UDT field history references to use (object[n]).field syntax
  • Update timeframe.period comparisons to include multipliers
  • Add default branches to all switch/if expressions returning unique type constants
  • Remove any series values from plot() offset parameters
  • Compare backtest results between v5 and v6 versions to catch silent behavioral changes

Automate Your Updated v6 Strategies

Once you have migrated your Pine Script strategies to v6, you can automate them with TradersPost. TradersPost connects your TradingView alerts to live broker accounts, executing trades automatically based on your strategy signals5. It works with both v5 and v6 scripts, so you can migrate at your own pace without disrupting your live automation.

TradersPost supports brokers including Alpaca, TradeStation, Tradier, Interactive Brokers, and leading futures prop firms5. Whether you are running a simple moving average crossover or a complex multi-timeframe strategy, TradersPost bridges the gap between your TradingView scripts and real-time order execution.

If you are interested in learning more about Pine Script versions, check out our guides on What Is Pine Script v5 and Pine Script v6: What's New and Why It Matters.

Conclusion

Pine Script v6 introduces 14 breaking changes that range from compilation errors (easy to find and fix) to silent behavioral shifts (dangerous if overlooked). The three most impactful changes for most traders are the removal of implicit bool casting, the switch to lazy evaluation for and/or operators, and the change to integer division returning fractional values.

Start by running the auto-converter, then work through remaining issues using the before-and-after code examples in this guide. Always compare your backtest results between v5 and v6 to catch any silent changes, particularly around integer division and strategy.exit() parameter evaluation. With methodical testing, the migration is straightforward, and v6's improvements in dynamic requests, stricter typing, and realistic margin handling make it well worth the effort.

References

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

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