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](https://www.tradingview.com/pine-script-docs/migration-guides/to-pine-version-6/) and the [release notes](https://www.tradingview.com/pine-script-docs/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.

```pine
// v5
color c = bar_index ? color.green : color.red
if ta.change(time("D"))
    entriesToday := 0

// v6
color c = bool(bar_index) ? color.green : color.red
if 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`.

```pine
// v5
strategy.entry("L", strategy.long, when = fired)

// v6
if 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.

| Change | v5 | v6 | What to look for |
| --- | --- | --- | --- |
| Default margin | `margin_long = 0`, `margin_short = 0` | Both default to 100 | New margin calls, fewer or smaller entries |
| `const int / const int` | Integer division, `5 / 2` is `2` | Fractional, `5 / 2` is `2.5` | Bar counts, lot maths, array indices |
| `and` / `or` | Evaluate both sides | Lazy: right side skipped when the left decides | Side effects in the right operand stop running |
| `timeframe.period` | `"D"`, `"W"` | Always has a multiplier: `"1D"`, `"1W"` | String comparisons against timeframe names |
| Trade limit | Error above 9000 orders | Oldest orders trimmed | Old `strategy.closedtrades` entries become `na`; use `strategy.closedtrades.first_index` |
| `strategy.exit()` with both relative and absolute levels | Relative ignored | Whichever level triggers first wins | Exits that fire earlier than before |
| `for` loop end boundary | Evaluated once | Evaluated before each iteration | Loops whose bound changes inside the body |
| Negative array index | Error | Counts from the end in some `array.*()` functions | Code that relied on the error as a guard |
| Colour constants | `color.teal` is `#00897B` | `color.teal` is `#089981` | Purely 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](https://www.tradingview.com/pine-script-docs/migration-guides/to-pine-version-6/)).

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](/learn/pine-repainting-lookahead-calc-on-every-tick) 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](/learn/tradingview-webhook-alerts) 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](/learn/tradingview-webhook-alerts) shows what to look for.


  Step 7 is the one people skip. A running alert keeps executing the v5 snapshot
  it was created from, so a migrated script with a changed payload changes
  nothing until the alert is deleted and created again.


## 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.

```pine
//@version=5
strategy("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)
```

```pine
//@version=6
strategy("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](/dashboard/studio).
