Pine Script v5 to v6: what changed and how to migrate

The breaking changes between Pine Script v5 and v6, with a before and after for each, a migration checklist, and the traps the editor's converter does not fix.

Updated 30 Aug 2026

Pine Script v6 is mostly the same language as v5 with a stricter type system, requests that can run anywhere in a script, and a handful of removals that had been deprecated for a while. Most scripts convert in a minute. The trouble is the small set of changes that alter behaviour without producing a compile error, because those show up as a different trade list, not as a red line in the editor.

This page follows the official migration guide and the release notes, and adds the items that matter most for strategies that feed a webhook.

The changes that break compilation

These are the ones the editor will point at. Each has a mechanical fix.

Numbers are no longer booleans

v5 silently treated 0, 0.0 and na as false and anything else as true. v6 refuses to cast. Wrap the number with bool() or write the comparison you meant.

// v5color c = bar_index ? color.green : color.redif ta.change(time("D"))    entriesToday := 0
// v6color c = bool(bar_index) ? color.green : color.redif ta.change(time("D")) > 0    entriesToday := 0

Booleans cannot be na

A v5 bool had three states: true, false and na. v6 has two. na(), nz() and fixnan() no longer accept a bool argument, a bool variable cannot start as na, and an if with no else that returns a bool now returns false where v5 returned na. If your logic depended on the third state, replace the boolean with an int (say -1, 0, 1) or a string.

when is gone from order functions

strategy.entry(), strategy.order(), strategy.exit(), strategy.close(), strategy.close_all(), strategy.cancel() and strategy.cancel_all() no longer take when. Wrap the call in an if.

// v5strategy.entry("L", strategy.long, when = fired)
// v6if fired    strategy.entry("L", strategy.long)

transp is gone

Every function that took a transp argument now expects the transparency inside the colour: color.new(color.red, 80).

One argument per parameter, no history on literals

A call cannot pass the same parameter twice. The [] operator cannot be applied to a literal or directly to a field of a user defined type; assign to a variable first.

plot() offset must not be a series

The offset parameter of plot() and related functions accepts input or simple values only. If you were shifting a plot by a runtime value, that plot has to be rebuilt with lines or labels.

Unique type parameters cannot be na

Parameters such as style in plot() expect one of a fixed set of constants. A switch feeding one needs a default branch and an if needs an else, so the expression can never evaluate to na.

The changes that alter behaviour silently

These compile fine and run differently. Check each one against your backtest.

Changev5v6What to look for
Default marginmargin_long = 0, margin_short = 0Both default to 100New margin calls, fewer or smaller entries
const int / const intInteger division, 5 / 2 is 2Fractional, 5 / 2 is 2.5Bar counts, lot maths, array indices
and / orEvaluate both sidesLazy: right side skipped when the left decidesSide effects in the right operand stop running
timeframe.period"D", "W"Always has a multiplier: "1D", "1W"String comparisons against timeframe names
Trade limitError above 9000 ordersOldest orders trimmedOld strategy.closedtrades entries become na; use strategy.closedtrades.first_index
strategy.exit() with both relative and absolute levelsRelative ignoredWhichever level triggers first winsExits that fire earlier than before
for loop end boundaryEvaluated onceEvaluated before each iterationLoops whose bound changes inside the body
Negative array indexErrorCounts from the end in some array.*() functionsCode that relied on the error as a guard
Colour constantscolor.teal is #00897Bcolor.teal is #089981Purely visual

The division change is the one that catches strategies. barsInSession = 375 / 5 was 75 in v5 and is still 75 in v6, but 375 / 15 / 2 was 12 and is now 12.5, and anything used as a bar offset or an array index needs wrapping in int() or math.floor().

Dynamic requests

In v5, request.security() needed simple arguments for its symbol and timeframe and had to sit in the global scope unless you set dynamic_requests = true. In v6 dynamic requests are on by default: the symbol and timeframe can be series strings, the call can live inside a loop or an if, and one call instance can serve many symbols (migration guide, Dynamic requests).

The compiler turns the feature off again when a script does not need it, so there is no cost for simple scripts. Two cautions from the guide:

  • Chained requests, where the result of one request.security() call feeds the expression of another, can return slightly different values in dynamic mode. If a converted script's trades shift and you cannot see why, add dynamic_requests = false to the declaration and compare.
  • A v6 script that explicitly sets dynamic_requests = false cannot call a wrapped request.*() from a local scope at all, even inside a user function. Remove the explicit argument.

Nothing about lookahead changed. barmerge.lookahead_off is still the default and barmerge.lookahead_on still leaks the future unless you offset the expression. The repainting page covers that in detail.

Text formatting

str.format() behaves as it did in v5: it still groups thousands by default, so a webhook body that carries a price as {0} sends 57,400, which is not JSON. Use {0,number,#.##} and keep TradingView placeholders such as {{close}} outside the format string; the webhook alerts page has the full explanation.

Strategy arguments worth resetting

When you convert, reread the strategy() declaration rather than trusting the converter. The arguments that matter for a strategy that fires a webhook:

  • margin_long and margin_short: 100 in v6 by default. Decide deliberately.
  • calc_on_every_tick: keep it false so orders and alert() calls happen on confirmed bars.
  • process_orders_on_close: true fills at the close of the bar that produced the signal, which matches an alert that fires on bar close.
  • commission_type, commission_value, slippage: v6 did not change them, but a migration is a good moment to make sure the backtest pays realistic per order costs and a couple of ticks of slippage.
  • dynamic_requests: leave it unset unless you are diagnosing a difference.

Migration checklist

  1. Duplicate the script and convert the copy, from Manage script, Convert code to v6. The original v5 keeps compiling.
  2. Fix every highlighted error using the sections above. Most are when, transp, boolean na and numeric conditions.
  3. Search the source for / between integer literals and wrap the ones used as counts or indices.
  4. Search for timeframe.period == and add the multiplier to the compared string.
  5. Set margin_long and margin_short on purpose.
  6. Run the Strategy Tester on the same symbol, timeframe and date range for both versions and compare trade counts and the trade list. Any difference should trace back to one of the rows in the table above.
  7. Recreate every TradingView alert that pointed at the old script. Alerts are snapshots and do not follow edits.
  8. Fire a test alert at a request bin and confirm the body is still valid JSON with unformatted numbers. The webhook alerts page shows what to look for.

A before and after

The two snippets below are the same opening range breakout entry, first in v5 and then in v6. The differences are the when removal, an explicit else on the boolean helper, the margin arguments, and the number formatting in the webhook body.

//@version=5strategy("ORB 15m", overlay = true, process_orders_on_close = true,     commission_value = 350, slippage = 2)
orHigh = ta.highest(high, orBars)[1]fired  = ta.crossover(close, orHigh)body   = '{"sym":"{{ticker}}","side":"BUY","type":"ORB",' +         str.format('"sl":{0}', orLow) + '}'strategy.entry("L", strategy.long, when = fired and barstate.isconfirmed,     alert_message = body)
//@version=6strategy("ORB 15m", overlay = true, process_orders_on_close = true,     commission_value = 350, slippage = 2,     margin_long = 0, margin_short = 0)
orHigh = ta.highest(high, orBars)[1]fired  = ta.crossover(close, orHigh)body   = '{"sym":"{{ticker}}","side":"BUY","type":"ORB",' +         str.format('"sl":{0,number,#.##}', orLow) + '}'if fired and barstate.isconfirmed    strategy.entry("L", strategy.long, alert_message = body)    alert(body, alert.freq_once_per_bar_close)

Try it in Sarathi

The Studio writes Pine v6 from the start, so a strategy built there never needs this migration; it comes out with two state booleans, explicit if blocks around orders, calc_on_every_tick = false, and a webhook body formatted with {0,number,#.##}. Live Each alert from that script lands on your per-user webhook and becomes an order card in Telegram and on the web, waiting for your Confirm tap at ½, 1× or 2× size, or Skip. The paper broker fills confirmed orders today; Dhan and Zerodha adapters are planned. Planned Open the Studio.

Questions

Sarathi writes and checks Pine v6 strategies; paper trade before you connect a broker.

Open the Studio
Sarathiसारथी

Sarathi means charioteer. The charioteer drives; the warrior decides when to shoot. Here the software drives, and you confirm every order.

Status

Pre-launch · pre-revenue
Bridge verified live 24 Aug 2026

Get started →

Sarathi is an execution bridge, not an investment adviser, and is not registered with SEBI as a Research Analyst. Nothing on this page is advice or a recommendation to trade. Sarathi never holds your funds or securities; every order is placed by you, on your own broker account. Derivatives trading carries substantial risk of loss.

Chart illustrations use TradingView Lightweight Charts™. Every price on this page is made up.

TradingView and Telegram are trademarks of their respective owners. Sarathi is not affiliated with or endorsed by them.