
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
Pine Script v6 introduces two restrictions on the history-referencing operator [].
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)
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.
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.
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().
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.
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.
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.
when parameter and wrapping calls in if blockstransp parameter and replacing it with color.new()bool()[] from literal valuesta.* functions are inside and/or expressionsna logic that requires restructuring to use int variablesstrategy.exit()timeframe.period string comparisonsThe 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.
Use this checklist when migrating any Pine Script v5 strategy or indicator to v6:
int/float values in conditionals and add explicit comparisonsna(), nz(), fixnan() calls on bool valuesta.* function calls out of and/or expressions to global scope variableswhen = parameter with an if blockmargin_long and margin_short are explicitly set if you need non-default valuesstrategy.exit() calls that mix relative and absolute parameters(object[n]).field syntaxtimeframe.period comparisons to include multipliersswitch/if expressions returning unique type constantsseries values from plot() offset parametersOnce 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.
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.
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