🎓 Tutorials

Pine Script v5 to v6 Migration: What Breaks and How to Fix It (2026 Checklist)

⚠️ Disclosure: Some links on this page are affiliate links. If you sign up through them, I may earn a commission — at no extra cost to you. I only review tools I actually use.
Every Pine Script developer knew this day was coming. TradingView officially graduated Pine Script to v6 in December 2024, and as of 2026, all future updates apply exclusively to v6. Your v5 scripts still work — for now — but they're frozen in time. No new features, no bug fixes, no performance improvements.

I migrated a production USDJPY momentum indicator and a moving average crossover strategy from v5 to v6 last month. The auto-converter handled about 70% of the work. The other 30% broke my scripts in ways that weren't immediately obvious — silent behavior changes that only showed up during backtesting.

This is the checklist I wish I'd had. Every breaking change, what it actually means for your code, and how to fix it.

Before You Start: The Auto-Converter

TradingView's Pine Editor has a built-in converter. Open your v5 script, click the "Manage script" dropdown, and select "Convert code to v6."

Important: Your v5 script must compile successfully before conversion. If it has errors in v5, fix those first.

The converter handles most syntactic changes automatically — version annotation, removed parameters, basic type adjustments. But it can't catch behavioral changes. That's where this checklist comes in.

Breaking Change #1: Booleans Can No Longer Be na

Impact level: 🔴 High — this breaks the most scripts

In v5, a bool variable could be true, false, or na. This "trilean" (three-state boolean) was a constant source of confusion, but plenty of scripts relied on it.

In v6, booleans are strictly binary: true or false. Period.

What breaks

// v5 — this works
var bool triggered = na
if someCondition
    triggered := true

// Later...
if na(triggered)
    // First-time logic

In v6, var bool triggered = na won't compile. The na() function no longer accepts bool arguments.

How to fix it

// v6 — use false as the initial state
var bool triggered = false
if someCondition
    triggered := true

// Later...
if not triggered
    // First-time logic

The subtle trap

In v5, if and switch blocks that returned bool would return na for unspecified conditions. In v6, they return false. This changes behavior silently:

// v5: crossSignal is na when neither condition is true
// v6: crossSignal is false when neither condition is true
bool crossSignal = if ta.crossover(fast, slow)
    true
else if ta.crossunder(fast, slow)
    false
// What about when neither? v5 = na, v6 = false

If your downstream logic distinguished between false and na, it will behave differently in v6. Review every conditional block that returns a boolean.

Breaking Change #2: No More Implicit int/float to bool Casting

Impact level: 🟡 Medium — easy to find, easy to fix

In v5, you could use a number where a boolean was expected. Zero and na were false, everything else was true.

// v5 — bar_index is an int, used as a bool
color expr = bar_index ? color.green : color.red

In v6, this won't compile. You need an explicit cast.

How to fix it

// v6 — wrap with bool()
color expr = bool(bar_index) ? color.green : color.red

// Or be explicit about what you actually mean
color expr = bar_index != 0 ? color.green : color.red

I prefer the second form — it's clearer about intent. The bool() function follows the same rule: 0, 0.0, and nafalse, everything else → true.

Common patterns to search for

Look for these in your code:

Breaking Change #3: Dynamic Requests Are Now Default

Impact level: 🟠 Medium-High — can silently change behavior

This is the trickiest one. In v5, request.security() and other request.*() calls were non-dynamic by default — they required "simple" (known at compile time) arguments for ticker and timeframe, and couldn't run inside loops or conditionals.

In v6, dynamic requests are on by default. The compiler analyzes your script and turns them off automatically if unnecessary, but there are edge cases where this changes behavior.

What breaks

If your v5 script nests request.security() calls — using the result of one as input to another — the behavior might differ in v6:

// This might behave differently in v6
float dailyClose = request.security(syminfo.tickerid, "1D", close)
float weeklyOfDaily = request.security(syminfo.tickerid, "1W", dailyClose)

How to fix it

If you see different results after conversion, add dynamic_requests = false to your declaration statement:

//@version=6
indicator("My Indicator", dynamic_requests = false)

This forces the v5 behavior. Test your output, and if the results match, you can try removing it later.

The v6 advantage

Dynamic requests are actually a huge upgrade. In v5, you needed to write separate request.security() calls for each symbol. In v6, you can loop through an array of symbols:

//@version=6
indicator("Multi-Symbol Scanner")

var array<string> symbols = array.from("NASDAQ:AAPL", "NASDAQ:MSFT", "NASDAQ:GOOGL")
array<float> closes = array.new<float>()

for [i, sym] in symbols
    float c = request.security(sym, "1D", close)
    closes.push(c)

plot(closes.avg(), "Average Close")

This was impossible in v5 without dynamic_requests = true. In v6, it just works.

Breaking Change #4: Lazy Boolean Evaluation

Impact level: 🟢 Low — mostly a positive change

In v5, both sides of and / or expressions were always evaluated, even when the result was already determined. In v6, evaluation is lazy (short-circuit):

// v6: if conditionA is true, conditionB is never evaluated
if conditionA or conditionB
    doSomething()

When this matters

If conditionB has side effects (like calling a function that modifies a variable), it won't execute when conditionA short-circuits the evaluation:

// v5: both f() and g() always execute
// v6: g() only executes if f() returns false
if f() or g()
    //...

How to fix it

If you need both functions to execute regardless:

bool resultF = f()
bool resultG = g()
if resultF or resultG
    //...

For most scripts, lazy evaluation is a pure performance win — Pine skips unnecessary computations. Only scripts with side-effect-heavy boolean expressions need adjustment.

💡 TradingView

Like what you're reading? Try it yourself — this link supports ChartedTrader at no cost to you.

Try TradingView →

Breaking Change #5: Strategy Parameter Changes

Impact level: 🟠 Medium — affects all strategy scripts

Three changes hit strategy scripts:

1. The when parameter is removed

Every strategy.*() function that accepted a when parameter (like strategy.entry(), strategy.exit(), strategy.close()) no longer supports it.

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

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

The auto-converter handles this one, wrapping the call in an if block.

2. Default margin is now 100%

In v5, the default margin_long and margin_short were both 0 (no margin requirement). In v6, they're 100% — meaning full cash backing required by default.

If your strategy relied on leverage without explicitly setting margin percentages, your backtest results will change dramatically.

// v6: explicitly set margin if you need leverage
strategy("My Strategy", margin_long = 10, margin_short = 10)  // 10x leverage

3. Trade limit trimming

In v5, exceeding 9,000 trades in a backtest raised an error. In v6, it silently trims the oldest orders. This is generally better, but if your strategy generates thousands of trades, be aware that early trade history might disappear from results.

Breaking Change #6: Integer Division Returns Fractional Values

Impact level: 🟡 Medium — sneaky when it hits

In v5, dividing two const int values gave an integer result (truncated). In v6, it returns a float:

// v5: result is 1 (integer truncation)
// v6: result is 1.5 (float)
var x = 3 / 2

How to fix it

If you need integer division:

var x = int(3 / 2)  // Explicitly truncate to int
// or
var x = math.floor(3 / 2)  // Floor division

This one is easy to miss because the variable type changes silently. If you use the result as an array index or loop counter, you'll get a type error downstream.

Breaking Change #7: History Referencing Restrictions

Impact level: 🟢 Low — rare pattern

The [] operator can no longer reference the history of literal values or fields of user-defined types directly:

// v5 — works but meaningless
x = 1[3]  // history of a literal? always 1

// v6 — compilation error

For UDT fields, you now need to reference the object's history first, then access the field:

// v5
myObj.price[1]

// v6 — reference the object history, then get field
myObj[1].price

Breaking Change #8: timeframe.period Format Change

Impact level: 🟢 Low — affects string comparisons

In v5, timeframe.period returned "D" for daily, "W" for weekly, "M" for monthly. In v6, it always includes a multiplier: "1D", "1W", "1M".

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

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

If your script compares timeframe.period against hardcoded strings, update them.

Breaking Change #9: plot() Offset No Longer Accepts Series

Impact level: 🟢 Low — niche use case

The offset parameter of plot(), plotshape(), and related functions no longer accepts "series" values — it must be "simple" (known before execution):

// v5 — worked
plot(close, offset = someCalculatedOffset)

// v6 — must be simple
plot(close, offset = input.int(5, "Offset"))

Breaking Change #10: Removed transp Parameter

Impact level: 🟢 Low — auto-converter handles this

The transp (transparency) parameter is removed from all functions. Use color.new() instead:

// v5
plot(close, color = color.blue, transp = 30)

// v6
plot(close, color = color.new(color.blue, 30))

The auto-converter does this automatically.

The Complete Migration Checklist

Here's the copy-paste checklist for migrating any v5 script:

Pre-migration: Auto-conversion: Manual review (the converter won't catch these): Post-migration:

Real-World Migration Example: USDJPY Momentum Indicator

Here's how I migrated a real indicator I use daily for USDJPY trading on TradingView.

The original v5 indicator tracked 60-day momentum with a signal line and color-coded background. During migration, I hit three issues:

1. Boolean na initialization — I had var bool trendUp = na as a "not yet determined" state. Changed to var bool trendUp = false and added a var bool initialized = false flag for first-bar logic.

2. Implicit numeric bool cast — Used if momentum instead of if momentum > 0 in two places. Quick fix with explicit comparison.

3. Timeframe string comparison — Compared timeframe.period == "D" in a multi-timeframe section. Changed to "1D".

Total migration time: about 15 minutes. The auto-converter caught the transp removal and version annotation. Everything else was manual.

Should You Migrate Now?

Yes. Here's why:

Start with your simplest indicator, use this checklist, compare the output carefully, then work your way up to complex strategies. The migration is straightforward once you know what to look for.

Related Resources

If you're working with Pine Script on TradingView, these guides might help:

---

*This article contains affiliate links. If you sign up for TradingView through our links, we earn a commission at no extra cost to you. We only recommend tools we actually use for trading.*

TradingView

Ready to get started? Use the link below — it helps support ChartedTrader at no cost to you.

Try TradingView →
📈

About the author

I'm a systematic trader running live strategies on IB (USDJPY momentum) and Hyperliquid (crypto perps). Every tool reviewed here is something I've used with real capital. Questions? Reach out.

📚 Related Articles

🎓 Tutorials

TradingView Free Plan Indicator Limit: How to Combine RSI, EMA, and MACD Into One Pine Script (2026)

Hit TradingView's 2-indicator limit on the free plan? Learn how to combine RSI, EMA, and MACD into a single all-in-one Pine Script v6 indicator — with full copy-paste code, visual customization tips, and a clear upgrade path when you outgrow the workaround.

March 23, 2026 ⏱ 10 min read
🎓 Tutorials

TradingView Webhook to Telegram Bot: Get Real-Time Alerts on Your Phone (2026 Setup Guide)

Learn how to send TradingView alerts directly to Telegram using webhooks. Step-by-step guide covering BotFather setup, free relay options, JSON payload formatting, and real trading alert examples — no coding experience required.

March 22, 2026 ⏱ 14 min read
🎓 Tutorials

TradingView Pine Script SuperTrend Strategy: Build a Custom Indicator Step by Step (2026)

Learn how to build a custom SuperTrend strategy indicator in Pine Script v6 with complete code. Includes RSI filter, multi-timeframe confirmation, stop loss/take profit, and backtesting setup for TradingView.

March 21, 2026 ⏱ 17 min read

📬 Get weekly trading insights

Real trades, honest reviews, no fluff. One email per week.