---
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 stays `//@version=2`. That number is the **script-format marker, not the engine version**, so do not bump it to `//@version=3` (the engine does not recognize that and falls back). All of the v3 features run under the `//@version=2` line. 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 3.0.13 (in-flight) 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="scripts/probes/compat-v2v3/ta_early_values.ks" 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="scripts/probes/compat-v2v3/accepted_aliases_nan.ks" 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

Before 3.0.13, `close[-1]` could read a future bar. That was a lookahead bug, not a supported feature. Since 3.0.13 (in-flight), negative timeseries subscripts return `na`. Array negative indexes still wrap from the tail, so `arr[-1]` remains the last element.

The probe below reported `ok: true`; both checks produced 300 finite values from bar 0 to 299, proving `closeSeries[-1]` was `na` while `values[-1]` matched `trade.close`.

```javascript title="scripts/probes/gotchas/negative_index_series_array.ks"
//@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. Leave the version marker at `//@version=2`. Do not change it.
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).
