
Boolean handling is at the heart of every Pine Script indicator and strategy. Every entry signal, exit condition, and filter depends on boolean logic evaluating correctly. Pine Script v6 boolean changes affect three fundamental areas: implicit type casting, the na state, and how and/or operators evaluate their arguments1. If you write or maintain TradingView scripts, understanding these changes is essential to avoiding broken signals and unexpected behavior after upgrading.
These are not cosmetic changes. They alter how your conditions resolve on every single bar, which directly impacts when your strategy enters and exits trades. This guide explains each change in detail with practical code examples showing exactly what breaks and how to fix it.
The most common migration error comes from Pine Script v6 removing implicit int-to-bool and float-to-bool casting2. In v5 and earlier, any numeric value could be used where a boolean was expected. Zero and na evaluated as false, and every other number evaluated as true. This was convenient shorthand, but it also made code ambiguous and prone to subtle bugs.
In v5, you could pass an integer or float directly into an if condition, a ternary expression, or any parameter expecting a bool2. The Pine runtime silently converted the number to true or false based on whether it was non-zero.
//@version=5
indicator("Implicit casting v5")
// bar_index is an int, used directly as a bool condition
color bgColor = bar_index ? color.green : color.red
bgcolor(bgColor)
// volume is a float, used as an if condition
if volume
label.new(bar_index, high, "Has volume")
// ta.change() returns an int, used as a bool
newMonth = ta.change(month)
if newMonth
label.new(bar_index, high, "New month")
All three patterns compiled and ran without errors in v5. The bar_index value of 0 on the first bar evaluated as false, and any positive bar_index evaluated as true. The volume value worked the same way: zero volume was false, any positive volume was true.
In v6, every one of those patterns produces a compilation error2. The error message states that a non-bool value is used where a bool is expected. The Pine compiler no longer performs the automatic conversion.
//@version=6
indicator("Implicit casting v6 - errors")
// ERROR: Cannot use "int" as "bool"
color bgColor = bar_index ? color.green : color.red
// ERROR: Cannot use "float" as "bool"
if volume
label.new(bar_index, high, "Has volume")
// ERROR: Cannot use "int" as "bool"
newMonth = ta.change(month)
if newMonth
label.new(bar_index, high, "New month")
There are two approaches to fix implicit casting. You can use the bool() function for a direct conversion (na, 0, and 0.0 become false; everything else becomes true)3, or you can write an explicit comparison that makes the intent of your condition clear.
Here is a reference table of common v5 patterns and their v6 equivalents:
if bar_index becomes if bar_index != 0if volume becomes if volume > 0if close - open becomes if close > open or if close - open != 0if ta.change(month) becomes if bool(ta.change(month))if ta.crossover(a, b) stays the same (already returns bool)if str.length(myStr) becomes if str.length(myStr) != 0color expr = bar_index ? color.green : color.red becomes color expr = bool(bar_index) ? color.green : color.redWhen choosing between bool() and an explicit comparison, prefer the explicit comparison when it makes the logic clearer. Writing if volume > 0 is more readable than if bool(volume) because it communicates what the code actually checks. Use bool() when you genuinely want the zero/non-zero interpretation and there is no more descriptive comparison available.
//@version=6
indicator("Implicit casting v6 - fixed")
// Explicit comparison for bar_index
color bgColor = bar_index != 0 ? color.green : color.red
bgcolor(bgColor)
// Explicit comparison for volume
if volume > 0
label.new(bar_index, high, "Has volume")
// bool() wrapper for ta.change() result
newMonth = ta.change(month)
if bool(newMonth)
label.new(bar_index, high, "New month")
The second major Pine Script v6 boolean change eliminates the third boolean state2. In v5, a bool variable could be true, false, or na. This three-state system caused real confusion because na behaved inconsistently: it evaluated as false in conditional expressions, but na == false returned false, and na(boolVar) returned true when the variable was na.
In v5, the na boolean state created a paradox. A boolean na value was not true, was not equal to false, and required special handling with na() to detect. This led to bugs where developers assumed a bool was either true or false but did not account for the na case.
//@version=5
indicator("Bool na in v5")
// This if-statement only assigns true or false when conditions are met.
// When neither condition is met, isUp is na (not true AND not false).
bool isUp = if close > open
true
else if close < open
false
// When close == open, isUp is na
// All three of these can be true at different times:
if isUp == true
label.new(bar_index, high, "Up")
if isUp == false
label.new(bar_index, low, "Down")
if na(isUp)
label.new(bar_index, close, "Neutral")
The na(), nz(), and fixnan() functions all had overloads accepting bool arguments to handle this three-state system3.
In v6, a bool variable is always either true or false2. There is no third state. This change has several consequences:
na(), nz(), and fixnan() functions no longer accept bool arguments2[] to reference a bool value from before the first bar returns false instead of na2//@version=6
indicator("Bool na in v6 - errors")
bool isUp = if close > open
true
else if close < open
false
// In v6, the unspecified else returns false (not na)
// ERROR: na() does not accept bool arguments
if na(isUp)
label.new(bar_index, close, "Neutral")
If your script logic requires distinguishing between more than two states, replace the bool with an int or string variable. Here is a complete before-and-after example using a strategy that tracks position direction.
v5 approach (three-state bool):
//@version=5
strategy("Position tracker v5", overlay=true, margin_long=100, margin_short=100)
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)
// isLong is true (long), false (short), or na (flat)
bool isLong = if strategy.position_size > 0
true
else if strategy.position_size < 0
false
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 approach (int for three states):
//@version=6
strategy("Position tracker v6", overlay=true, margin_long=100, margin_short=100)
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)
// Use int: 1 = long, -1 = short, 0 = flat
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 => color.new(color.red, 40)
bgcolor(stateColor)
Notice the v6 version requires an explicit else block. Without it, the unspecified case would silently return 0, which happens to be correct here but could mask bugs in other situations. Always include explicit else blocks when all cases matter.
A common v5 pattern used na booleans for optional user inputs. If a bool input defaulted to na, the script could detect whether the user had made a choice. In v6, you need a different approach.
// v5: optional bool input using na default
// bool userChoice = input.bool(na, "Enable feature")
// if na(userChoice)
// // User hasn't chosen yet
// v6: use a string input with three options instead
string userChoice = input.string("Auto", "Enable feature", options=["Auto", "Yes", "No"])
bool featureEnabled = switch userChoice
"Yes" => true
"No" => false
=> close > ta.sma(close, 50) // "Auto" mode uses a condition
The third Pine Script v6 boolean change is how and and or operators evaluate their arguments2. In v5, both sides of every boolean expression were always evaluated, regardless of the first side's result. In v6, evaluation stops as soon as the overall result is determined. This is called lazy evaluation or short-circuit evaluation.
In v5, the expression A and B always evaluated both A and B, even if A was false (making the entire expression false regardless of B). Similarly, A or B always evaluated both sides, even if A was true (making the entire expression true regardless of B).
This meant every function call inside a boolean expression executed on every bar, which was important for functions that rely on internal history like ta.rsi(), ta.ema(), and ta.macd().
In v6, the rules are:
A and B: if A is false, B is not evaluated (result is false)A or B: if A is true, B is not evaluated (result is true)2The right side of the expression only executes when the left side does not determine the final result on its own.
Lazy evaluation causes problems when the right side of a boolean expression contains a function call that must execute on every bar to maintain accurate internal calculations. Technical analysis functions like ta.rsi(), ta.ema(), ta.sma(), ta.macd(), and ta.atr() all maintain internal state that depends on being called sequentially on every bar4.
v5 code (RSI executes every bar):
//@version=5
indicator("Lazy eval problem v5")
bool signal = false
// In v5, ta.rsi() is called on EVERY bar, even when close <= open
if close > open and ta.rsi(close, 14) > 50
signal := true
bgcolor(signal ? color.new(color.green, 90) : na)
v6 code (RSI skips bars, producing wrong values):
//@version=6
indicator("Lazy eval problem v6")
bool signal = false
// In v6, when close <= open, ta.rsi() is NOT called
// This corrupts the RSI calculation because it misses bars
if close > open and ta.rsi(close, 14) > 50
signal := true
bgcolor(signal ? color.new(color.green, 90) : na)
The v5 and v6 scripts produce different signals on the same chart. The RSI values in the v6 version are incorrect because the function does not execute on bars where close <= open, creating gaps in its internal history.
The fix: extract history-dependent functions to the global scope.
//@version=6
indicator("Lazy eval fixed v6")
// Calculate RSI on every bar at the global scope
float rsiValue = ta.rsi(close, 14)
bool signal = false
// Now use the pre-calculated variable in the condition
if close > open and rsiValue > 50
signal := true
bgcolor(signal ? color.new(color.green, 90) : na)
By assigning ta.rsi(close, 14) to a variable at the global scope, the function executes on every bar regardless of what happens in the boolean expression4. The variable rsiValue is then safely used inside the and condition without affecting the RSI calculation.
Lazy evaluation is not just a breaking change. It provides real advantages that make v6 code cleaner and safer.
Safe array access:
//@version=6
indicator("Safe array access v6")
array<float> prices = array.new<float>()
if close > open
prices.push(close)
// In v5, this would crash with a runtime error on bars where the array is empty
// because array.first() was always evaluated.
// In v6, array.first() only runs when size != 0.
if prices.size() != 0 and prices.first() > ta.sma(close, 20)
label.new(bar_index, high, "Signal")
In v5, you needed nested if-blocks to avoid the runtime error:
//@version=5
indicator("Safe array access v5")
array<float> prices = array.new<float>()
if close > open
prices.push(close)
// Must use nested if-blocks to avoid calling array.first() on empty array
if prices.size() != 0
if prices.first() > ta.sma(close, 20)
label.new(bar_index, high, "Signal")
Avoiding na errors:
//@version=6
indicator("Na safety v6")
float myValue = request.security("AAPL", "1D", close)
// Safe: if myValue is na, the comparison is never evaluated
if not na(myValue) and myValue > 150.0
label.new(bar_index, high, "Above 150")
Efficiency:
//@version=6
indicator("Efficient conditions v6")
// Expensive calculation only runs when the cheap check passes first
if barstate.isconfirmed and ta.percentrank(close, 100) > 90
label.new(bar_index, high, "Top 10%")
Structure your and conditions with the cheapest or most-likely-to-fail check on the left side. This way, the expensive calculation on the right only runs when necessary.
Here are two complete real-world patterns that combine multiple boolean changes, showing the full v5-to-v6 transformation.
v5 version:
//@version=5
strategy("EMA Cross v5", overlay=true)
fast = ta.ema(close, 9)
slow = ta.ema(close, 21)
// Uses implicit bool cast (volume), na bool, and when parameter
longSignal = ta.crossover(fast, slow)
strategy.entry("Long", strategy.long, when = longSignal and volume)
shortSignal = ta.crossunder(fast, slow)
strategy.entry("Short", strategy.short, when = shortSignal)
bool trending = if fast > slow
true
// trending is na when fast <= slow
bgcolor(na(trending) ? color.new(color.gray, 90) : trending ? color.new(color.green, 90) : na)
v6 version:
//@version=6
strategy("EMA Cross v6", overlay=true)
fast = ta.ema(close, 9)
slow = ta.ema(close, 21)
// Explicit bool comparison, if-block instead of when
longSignal = ta.crossover(fast, slow)
if longSignal and volume > 0
strategy.entry("Long", strategy.long)
shortSignal = ta.crossunder(fast, slow)
if shortSignal
strategy.entry("Short", strategy.short)
// Use int instead of three-state bool
int trend = if fast > slow
1
else if fast < slow
-1
else
0
bgcolor(trend == 0 ? color.new(color.gray, 90) : trend == 1 ? color.new(color.green, 90) : na)
v5 version:
//@version=5
indicator("Multi-Condition v5", overlay=true)
rsiAbove = close > open and ta.rsi(close, 14) > 50
macdPositive = ta.change(close) and ta.macd(close, 12, 26, 9)
if rsiAbove and macdPositive
label.new(bar_index, high, "Buy signal")
v6 version:
//@version=6
indicator("Multi-Condition v6", overlay=true)
// Extract all history-dependent functions to global scope
float rsi = ta.rsi(close, 14)
[macdLine, signalLine, histLine] = ta.macd(close, 12, 26, 9)
// Use explicit comparisons instead of implicit casting
rsiAbove = close > open and rsi > 50
macdPositive = ta.change(close) != 0 and macdLine > 0
if rsiAbove and macdPositive
label.new(bar_index, high, "Buy signal")
Notice how the v6 version is actually more readable. The explicit comparisons (rsi > 50, macdLine > 0, ta.change(close) != 0) make the intent of each condition clear. The extracted function calls at the global scope ensure every technical indicator calculates correctly on every bar.
The Pine Script v6 boolean changes are not just restrictions. They are guardrails that push you toward writing clearer, more reliable scripts. Here is a summary of best practices:
These patterns produce scripts that are easier to debug, maintain, and share with other traders. When you connect your v6 strategies to live trading through platforms like TradersPost, the improved predictability of boolean logic means fewer unexpected signals and more reliable automated execution5.
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