---
title: v2 vs v3
description: A migration guide for v2 script authors. v3 is additive (your scripts keep running), but TA builtins, negative timeseries subscripts, and a few conveniences need checking.
---

## v3 is additive

The good news first: **every v2 script keeps running on v3.** v3 did not break the language, it grew it. Typed structs, collections, self-managing drawings, no-repaint multi-timeframe, and multi-venue aggregation are all new capabilities layered on top of what v2 already did. If your script worked, it still works.

The version marker can stay `//@version=2` or move to `//@version=3`: the engine treats the two identically, routing both to the modern engine, and every v3 feature runs under either line. `//@version=3` is the canonical header (all of the platform's built-in scripts ship with it); there is no downgrade and nothing to migrate in the header itself. What you must NOT do is delete the marker or write `//@version=1`; both route the script to the deprecated legacy v1 engine. For the full tour of what v3 adds, see [What's New in v3](/kscript/getting-started/whats-new-v3).

So migration is not a rewrite. It is a short checklist. Two changes can move your numbers: corrected TA warmup/early values, and the fix for negative timeseries subscripts.

## First check: eight corrected TA builtins

Eight technical-analysis builtins were corrected to their textbook forms. Each now matches an independent reference implementation, so this is a correctness fix, not a regression. The practical effect: **these functions start emitting slightly later and with different early-bar values than they did in v2.** Steady-state output (well past the warmup window) is the same or more correct; the early bars are what changed.

What was corrected:

| Builtin | What changed |
|---|---|
| `ema` | Correct seeding of the first averaged value |
| `rsi` | Proper Wilder smoothing |
| `macd` | Follows the corrected `ema` |
| `psar` | Textbook parabolic SAR initialization |
| `adx` | Correct directional-movement warmup |
| `highest` | Strict window warmup |
| `lowest` | Strict window warmup |
| `sma` | Strict warmup (no partial-window averages) |

**Why you might notice:** if a script keys off an indicator's very first non-empty bars (for example a backtest that starts trading at bar 5, or a cross evaluated near the left edge of history), the first signal may shift by a bar or two. Anything reading these indicators after their warmup window is unaffected. If your logic waits for the indicator to be ready (the usual case), there is nothing to do.

### When each indicator starts emitting

This table is the practical reference: with a period-5 setup, the first bar index at which each builtin produces a value. Use it to reason about where your own warmup edges land.

| Builtin | Call | First value at bar |
| --- | --- | ---: |
| `ema` | `ema(source=closeSeries, period=5)` | `4` |
| `rsi` | `rsi(source=closeSeries, period=5)` | `5` |
| `macd` | `macd(... fastPeriod=3, slowPeriod=6, signalPeriod=3).macd` | `5` |
| `psar` | `psar(source=trade, start=0.02, increment=0.02, maxValue=0.2)` | `1` |
| `adx` | `adx(source=trade, period=5).adx` | `9` |
| `highest` | `highest(source=trade, period=5, priceIndex=2)` | `4` |
| `lowest` | `lowest(source=trade, period=5, priceIndex=3)` | `4` |
| `sma` | `sma(source=closeSeries, period=5)` | `4` |

```javascript title="TA early values" lines wrap
//@version=2
define(title="TA Early Values", position="offchart", axis=true)

timeseries trade = ohlcv(symbol=currentSymbol, exchange=currentExchange)
timeseries closeSeries = trade.close

var emaValue = ema(source=closeSeries, period=5)
var rsiValue = rsi(source=closeSeries, period=5)
var macdValue = macd(source=closeSeries, fastPeriod=3, slowPeriod=6, signalPeriod=3)
var psarValue = psar(source=trade, start=0.02, increment=0.02, maxValue=0.2)
var adxValue = adx(source=trade, period=5)
var highestValue = highest(source=trade, period=5, priceIndex=2)
var lowestValue = lowest(source=trade, period=5, priceIndex=3)
var smaValue = sma(source=closeSeries, period=5)

plotLine(value=emaValue, colors=["#2563eb"], label=["EMA"], desc=["ema period 5"])
plotLine(value=rsiValue, colors=["#dc2626"], label=["RSI"], desc=["rsi period 5"])
plotLine(value=macdValue.macd, colors=["#7c3aed"], label=["MACD"], desc=["macd line 3 6 3"])
plotLine(value=psarValue, colors=["#059669"], label=["PSAR"], desc=["psar default style parameters"])
plotLine(value=adxValue.adx, colors=["#ea580c"], label=["ADX"], desc=["adx period 5"])
plotLine(value=highestValue, colors=["#0891b2"], label=["Highest"], desc=["highest high period 5"])
plotLine(value=lowestValue, colors=["#be123c"], label=["Lowest"], desc=["lowest low period 5"])
plotLine(value=smaValue, colors=["#374151"], label=["SMA"], desc=["sma period 5"])
```

## Compatibility aliases that still work

v3 keeps a set of v2 conveniences so older scripts do not need touching. These are normalized for you, not rejected:

- **Uppercase `yAlign` values.** `plotText(..., yAlign="CENTER")` is accepted and normalized to `middle`; `"ABOVE"` becomes `top` and `"BELOW"` becomes `bottom`.
- **`shape="star"`.** Accepted as a `plotShape` shape (it renders as a diamond marker).
- **Bare `NaN`.** Writing `NaN` as a value is fine and behaves as a gap, the same as `na`.

The following script exercises all of these and runs clean.

```javascript title="Accepted compatibility aliases" lines wrap
//@version=2
define(title="Accepted Compatibility Aliases", position="overlay", axis=true)

timeseries trade = ohlcv(symbol=currentSymbol, exchange=currentExchange)
timeseries closeSeries = trade.close

var maybeClose = barIndex > 10 ? closeSeries : NaN
var shapeValue = closeSeries > trade.open ? trade.high : trade.low

plotText(text="center", color="#111827", price=trade.high, yAlign="CENTER")
plotText(text="above", color="#111827", price=trade.high, yAlign="ABOVE")
plotText(text="below", color="#111827", price=trade.low, yAlign="BELOW")
plotShape(value=shapeValue, shape="star", colors=["#f97316"], width=3, fill=true, label=["Star"], desc=["star shape compatibility alias"])
plotLine(value=maybeClose, colors=["#2563eb"], width=2, label=["NaN"], desc=["bare NaN compatibility value"])
```

## What got stricter

v3 tightened a few inputs that v2 let slide. These surface as compile errors with a line and column, and each error message lists the valid options, so the fix is mechanical.

**`plotText.yAlign` must be a known value.** Valid values are `top`, `middle`, `bottom` (the uppercase aliases above still map onto these). An unknown one like `"SIDEWAYS"` fails with:

```
Invalid plotText.yAlign 'SIDEWAYS'. Expected one of: 'top', 'middle', 'bottom'
```

**`plotShape.shape` must be a known shape.** The accepted set is large (`circle`, `cross`, `triangle`, `diamond`, `arrowUp`, `arrowDown`, `flag`, `square`, `label`, `char`, `star`, and more), but an invalid one like `"hexagon"` is rejected, and the error enumerates every shape the engine accepts so you can pick the right one.

**`define(..., position=...)` must be a string literal.** It has to be a literal `"onchart"` or `"offchart"`, not a variable or expression. A computed value fails with:

```
define(title, position, axis): 'position' must be a string literal ('onchart' | 'offchart')
```

If you hit any error not covered here, the [Common Errors](/kscript/faq/common-errors) page maps real engine messages to their cause and fix.

## Negative timeseries subscripts no longer look ahead

Previously, `close[-1]` could read a future bar. That was a lookahead bug, not a supported feature. Negative timeseries subscripts now return `na`. Array negative indexes still wrap from the tail, so `arr[-1]` remains the last element.

The script below exercises both: negative indexing on the series returns `na`, while negative indexing on the array reads from the tail.

```javascript title="Negative indexes on a series and an array"
//@version=2
define(title="Negative Index Series Array", position="offchart", axis=true)

timeseries trade = ohlcv(symbol=currentSymbol, exchange=currentExchange)
timeseries closeSeries = trade.close
var values = [trade.open, trade.high, trade.low, trade.close]

var seriesCheck = isna(closeSeries[-1]) ? closeSeries : na
var arrayCheck = values[-1] == trade.close ? trade.close : na

plotLine(value=seriesCheck, colors=["#2563eb"], width=2, label=["Series negative"], desc=["negative timeseries history indexes return na"])
plotLine(value=arrayCheck, colors=["#16a34a"], width=2, label=["Array negative"], desc=["negative array indexes wrap from the tail"])
```

## Migration checklist

1. Keep a version marker on line 1: `//@version=2` and `//@version=3` are interchangeable (`=3` is canonical). Never remove it; a missing marker or `//@version=1` routes to the deprecated legacy engine.
2. Re-run scripts that lean on the eight corrected TA builtins near the start of history; confirm any edge-case signals still fire where you expect.
3. Replace any intentional `close[-1]` style lookahead; it now returns `na`. Keep `arr[-1]` only for array tail access.
4. If you wrote `define(position=someVar)`, make it a literal `"onchart"` or `"offchart"`.
5. Everything else, including uppercase `yAlign`, `shape="star"`, and bare `NaN`, keeps working untouched.
6. When you are ready to use the new power, start with [What's New in v3](/kscript/getting-started/whats-new-v3).
