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 na → false, everything else → true.
Common patterns to search for
Look for these in your code:
- Ternary operators with numeric conditions:
someInt ? valueA : valueB ifstatements with numeric conditions:if volumeinstead ofif volume > 0whileloops with numeric conditions:while counterinstead ofwhile counter > 0
Breaking Change #3: Dynamic Requests Are Now Default
Impact level: 🟠 Medium-High — can silently change behaviorThis 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 changeIn 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.
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 scriptsThree 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 hitsIn 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 patternThe [] 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:- [ ] Script compiles in v5 without errors
- [ ] Take a screenshot of your current chart output (for comparison)
- [ ] Note your current backtest results (if strategy)
- [ ] Click "Manage script" → "Convert code to v6"
- [ ] Fix any compilation errors the converter missed
- [ ] Search for
var boolinitialized tona→ change tofalse - [ ] Search for
na()/nz()/fixnan()with bool arguments → refactor - [ ] Search for
if/switchblocks returning bool → check unspecified branches - [ ] Search for numeric values in boolean contexts → add
bool()or explicit comparison - [ ] Search for
request.security()nesting → test results, adddynamic_requests = falseif different - [ ] Search for
and/orwith side-effect functions → extract to separate variables - [ ] Search for
strategy.*(..., when = ...)→ wrap inifblock (auto-converter usually handles this) - [ ] Check
margin_long/margin_shortdefaults → set explicitly if using leverage - [ ] Search for
const intdivision → addint()ormath.floor()if integer result needed - [ ] Search for
timeframe.period ==string comparisons → update to include multiplier ("D" → "1D") - [ ] Search for
.field[n]on UDTs → change to[n].field - [ ] Search for
plot(..., offset = seriesVar)→ make offset a simple value
- [ ] Compare chart output to pre-migration screenshot
- [ ] Compare backtest results (if strategy) — check trade count, P&L, max drawdown
- [ ] Run on multiple timeframes to catch edge cases
- [ ] If anything looks different, add
dynamic_requests = falseand re-test
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:
- v6 gets all future features. Pine Script v6 already has
request.footprint()for order flow analysis, improved performance from lazy evaluation, and dynamic request support. Every new feature TradingView releases will be v6-only.
- Performance gains are real. Lazy boolean evaluation and compiler optimizations make v6 scripts measurably faster, especially complex ones with many conditions.
- The longer you wait, the harder it gets. As you add features to your v5 scripts, the migration surface area grows. Migrate now while your scripts are still familiar.
- Community scripts are moving to v6. If you publish scripts on TradingView or use libraries, the ecosystem is shifting. v5 libraries won't get updates.
Related Resources
If you're working with Pine Script on TradingView, these guides might help:
- Pine Script Beginner Guide: Build Your First Indicator
- Pine Script MA Crossover: Build a Real Strategy
- Pine Script RSI Divergence Indicator Tutorial
- TradingView Strategy Tester: Backtest Settings Explained
- Pineify vs ChatGPT: Which Pine Script AI Generator Works?
- TradingView Plans: Essential vs Plus vs Premium
*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.*