---
title: "From kScript (legacy)"
description: "kScript (legacy) is frozen at v3: published kScript indicators keep charting and hosted-alerting forever on the pinned engine, and nothing existing breaks.…"
order: 90
section: "migrations"
---

<!-- source: docs/indicators/migrations/from-kscript.md; generated by packages/cli/scripts/gen-indicator-docs.ts, do not edit -->

# From kScript (legacy)

kScript (legacy) is frozen at v3: published kScript indicators keep charting and hosted-alerting forever on the pinned engine, and nothing existing breaks. Porting one to an Indicator is how it gains what the kScript lane cannot give it: a file that compiles in the browser and runs on your machine, first-class metric ids for alerts, screens, series, and backtests, and a sandboxed package people install instead of reference. This page is the construct table: for each thing a kScript does, the Indicator form, and the cases that stay on kScript for now.

## Which kScript version you start from does not matter

The kScript tree's own migration guide is v2 to v3, and that split has no counterpart here. A `//@version=2` script and a `//@version=3` script port through the same table below, because the port replaces every construct rather than translating a marker: `define()` becomes declarations, `timeseries` becomes an input, builtins become classes, plots become outputs. An Indicator has no language versions to migrate between; its sheet names one of four ABI contracts (`abi_version: "wrun-1"`, frozen; `"wrun-2"`, additive, for text and declared drawings; `"wrun-3"`, additive again, for drawing handles and the last-bar signal; `"wrun-4"`, additive, for frames: levels, panels and the ladder, feed and meter widgets), and a module written under any of them keeps computing bit-identically, [Script definition](../functions/script-definition.md).

## The construct table

The table below covers the numeric core of a port. The full construct map, including frames, panels, levels, handles and anchors, series inputs and the constructs with no home yet, lives on the [construct map](./construct-map.md).

| kScript construct | Indicator form | Notes |
| --- | --- | --- |
| `//@version=3` on line 1 | `//@lang=wrun-ts` on line 1 | The editor's language marker; the CLI does not need it. |
| `define(title=, position="onchart"/"offchart", axis=)` | `output(name, plot, overlay)` or `output(name, plot, lower)` per output | Placement is per output; the tab title names the package. |
| `input(type="number"/"slider", defaultValue, constraints={min, max, step})` | `param("name", default, { min, max, description })`, read in `init()` via `p_name()` | No `step`; `label` is `description`. Every param is a field in the overlay's settings. |
| `input(type="boolean")` | `param("flag", 1, { min: 0, max: 1 })` turned into a `none` output and used as a `when` or `shape_where` gate | The cookbook's key levels do this with `show_open`. |
| `input(type="color")` | none in the file: `color` is declared per output, box, or segment | A runtime color picker exists only as a style knob in a hand-written sheet, [Styling](../functions/styling.md). |
| `input(type="string")` | none | Params are numbers. A venue symbol is a pinned declaration instead (`input(..., { symbol, exchange })`). |
| `timeseries d = ohlcv(symbol=currentSymbol, exchange=currentExchange)`, `d.close[0]` | `input("close", ohlcv.close)`, read in `state()` via `in_close()` | One input per field you read; an unpinned input follows the chart. |
| `d.close[1]`, `series[n]` | keep the previous value in a module-level variable; a window is a `StaticArray` ring buffer | There is no history array, [Execution model](../core-concepts/execution-model.md). |
| `sma()`, `ema()`, `stddev()`, `rsi()`, `roc()`, `crossover()` / `crossunder()`, every other TA builtin | `Sma`, `Ema`, `Stdev`, `Rsi`, `Roc`, `Cross`, and one class per builtin from `./sdk/ta`, constructed in `init()`, `.update()` once per bar | `NaN` until warm; `Cross.update(a, b)` returns `+1`, `-1`, or `0`. |
| `highest()`, `lowest()`, `vwap(anchor=)`, `atr()`, other builtins | write them: a ring-buffer scan, a sum that resets on a session boundary | The cookbook has `highest` / `lowest` (zone tracker) and an anchored VWAP. |
| `htf(d, "4h")` | bucket bars by `time.bar_open_sec` and fold a bucket in on the next bucket's first bar; on your machine, a second input with `{ interval: "4h" }` | The chart reads its own interval for every input. |
| `source(type="buy_sell_volume", ...)`, `.buy` / `.sell` | `input("buy", trades.volume, { side: "BUY" })` and `{ side: "SELL" }` | `missing: "zero"` for a venue that skips bars. |
| `source(..., symbol=, exchange=)` on another venue | `input(..., { symbol, exchange })`, both together | Honored on your machine; the chart reads its own market. |
| `orderbook()`, volume profile functions | `input("book", book.cells, { max_cells, block_size })`, `input("profile", volume_profile.cells, { max_cells })`, read via `in_book_cells()` / `in_book_read(ptr)` | Celled inputs, [Data sources](../core-concepts/data-sources.md). |
| `currentInterval`, `barIndex`, `d.time[0]` | `input("bar_t", time.bar_open_sec)`; interval math from consecutive bar times | Epoch seconds, UTC. |
| `timestamp()`, `year()`, `dayOfWeek()` | integer math on `bar_open_sec` (`Math.floor(t / 86400)` is the UTC day) | The cookbook's anchored VWAP and key levels. |
| `persist x = 0` | a module-level `let x: f64 = 0.0` | `reset()` must restore it. |
| `type Zone { ... }`, `func` | AssemblyScript `class` and functions | Typed fields; no `any`. |
| `na`, `isnum(x)`, `isna(x)` | `NaN`, `!isNaN(x)`, `isNaN(x)` | |
| `plotLine(value, colors=[c], width=, label=, desc=)` | `output("name", line, panel, { color, width, description })` | |
| `plotLine(..., colors=[a, b], colorIndex=expr)` | a data-only output holding the index plus `color_by` and `colors` on the drawn output | The regime filter and the CVD recipes. |
| `if (cond) plotShape(value, shape=, colors=)` | `output("mark", shape, panel, { shape_where: "gate", color })` plus a 0/1 gate output | The shape kind is the host's default mark; `render.shape` picks one. |
| `hline(value)` | an output written to the same value every bar | |
| `fillBetween(a, b, color, opacity)` | `range("upper", "lower", { colors, opacity })` declares a filled band between two outputs, drawn by the chart | A band that should appear on some bars only is a per-bar box with a `when` gate instead; `fills` exists in hand-written sheets only. |
| `barcolor(color)` | `render.bgcolor("tint", { where: "gate", color })` tints the background per bar; or `color_by` on a line | Candle bodies themselves are not tinted. |
| `plotTable(data=[[...]])` under `isLastBar` | `render.table("stats", { rows, cols, cells })` over string slots written every bar | The newest complete row wins; no `isLastBar` needed. |
| `plotPriceLabel(text, price)` on every signal bar | a `text` renderer over a string slot with `"style": "price_label"` in the sheet | A tag per bar whose slot was written; leave the slot unwritten on quiet bars, [Plotting](../functions/plotting.md). |
| `plotLabel(text, position="top_right")` | a `label` renderer over a string slot with `"position"` in the sheet | A corner readout; the newest bar that wrote the slot wins. |
| `opacity(color, 30)` | the `opacity` option, or an 8-digit hex color | |
| `alert(message, condition)`, `alertcondition()` | nothing in the file; after install or publish, a condition watch on the metric (`om watch create --condition`) with a level (`gt`, `lt`) or an edge (`crosses_above`, `crosses_below`), [Alerts](../functions/alerts.md) | An alert armed from the chart on a published Indicator runs hosted where the site has it switched on, and reads candles only today. |
| `isLastBar` | `bar.isLast()` from `./gen/draw`, true on the newest bar the host holds | Run-level renderers and declared drawings evaluate the newest ready bar without it; use it for a handle that should exist on the newest bar only, [Execution model](../core-concepts/execution-model.md). |
| `box.new(...)` drawn once and left alone | `box("zone", { top, bottom, from, to, when })`, evaluated per bar | Disappearance is the `when` gate turning `0`; the bars it was alive on stay drawn. |
| `box.new(...)` then `.set_lefttop()` / `.set_rightbottom()` / `.set_bgcolor()` / `.delete()`, a growing list of boxes | `handles.box({ ... })` once, then `draw.box(id).set(left, top, right, bottom)`, `.setLeftTop()`, `.setRightBottom()`, `.fill(rgba(...))`, `.delete()` | The same object from creation to deletion, under an integer id you choose; 500 live per kind, [Drawing objects](../functions/drawing-objects.md). |
| `line.new(x1, y1, x2, y2)` | `segment(...)` per bar; `draw.line` for one object placed from the newest bar | `from: 0, to: 1` on one output is a horizontal level (key levels). |
| `line.new(...)` then `.set_xy2()` / `.set_color()` / `.set_extend()` on later bars | `handles.line({ ... })` once, then `draw.line(id).set(...)`, `.setXy2()`, `.color(rgba(...))`, `.extend(Extend.Right)` | Every setter stamps the bar it ran on, as the engine does. |
| `label.new(x, y, text)` once, a price tag | `render.label` over a string slot: one label, the newest bar wins | [Plotting](../functions/plotting.md). |
| `label.new(...)` moved, re-worded, or deleted later | `handles.label({ text: "slot" })` once, then `draw.label(id).set(x, y).text(str_<slot>_sb)`, `.size()`, `.delete()` | The text is the slot's bytes on the bar of the call; `stickyRight` has no form. |
| polylines built point by point across bars | `handles.polyline({ ... })` once, then `draw.polyline(id).setPoints(points, count)` over a `StaticArray<f64>` of x, y pairs | Re-send as the path grows; at most 256 points per handle. |
| `strategy(title, initialCapital, ...)` | `strategy({ initialCapital, qtyType, qtyValue, commissionPercent, slippageBps, ... })` from `./gen/strategy`, one top-level statement beside the outputs | The engine's own setting names, every field optional; a numeric setting may take the handle `param(...)` returns. The file stays an Indicator: its outputs are metrics, and the broker adds `strategy.position` and `strategy.equity`, [Strategies overview](../strategies/overview.md). |
| `strategy.entry(id, "long", qty, limit, stop, ocaName)` | `strategy.long(id).qty(n).limit(px).stop(px).oca(name).send()` in `finalize()`; `strategy.short(id)` for the other side | One builder per call, `send()` places it; the same broker fills it at the next open, [Writing strategies](../strategies/writing-strategies.md). |
| `strategy.exit(id, fromEntry, stop, limit, trailPoints, trailOffset, ...)` | `strategy.exit(id).from(entry).stop(px).limit(px).trail(points, offset).send()` | The legs are one-cancels-all under the exit id, as before; `comment` has no form. |
| `strategy.close(id)`, `strategy.closeAll()`, `strategy.cancel(id)`, `strategy.cancelAll()`, the getters | the same names on `strategy` | Getters answer in `state()` and `finalize()` and read the position after the bar's fills, the engine's rule. |
| the Backtest action and the Strategy Tester | `om backtest @scope/name --asset EXCHANGE:SYMBOL --window 90d`, or the package on a chart | The same panel from the same run key; `--fee-bps` and `--slippage-bps` are refused because costs live in the file, [Reading the Strategy Tester](../strategies/reading-the-tester.md). Finer-bar fills, the book-estimate slippage model and recorded funding are named next steps, each refused or counted by name. |
| `print()` / the console | none | Emit a debug output and read it in the legend or with `om metric series`. |
| `maxBarsBack` | nothing to declare | State lives in your variables; size buffers from a param's `max`. |

Historical raw tape remains a porting gap. The daemon's `tape` buffer serves live prints above a required size threshold, with no historical backfill (per-price footprint and book data port fine, [Data sources](../core-concepts/data-sources.md)). Drawings port whole: a shape that is set once per bar is a declaration, and a shape the script keeps, moves, or deletes is a handle with the same caps kScript had. Everything scalar ports cleanly.

## Warm-up moves by a bar or two

The kScript v3 engine corrected eight builtins to their textbook warm-up, and the same question comes up in a port: on which bar does the Indicator's class first return a number? Every class in `./sdk/ta` returns `NaN` until its window is full and never averages a partial window, so with a period of 5 the first value lands on these 0-based bars:

| Class | First value at bar | Why |
| --- | ---: | --- |
| `Sma(5)` | `4` | five values in the window |
| `Ema(5)` | `4` | seeded with the average of the first five values, then smoothed with `2 / (period + 1)` |
| `Stdev(5)`, `Zscore(5)` | `4` | population standard deviation over the window; `Zscore` returns `0` when the deviation is `0` |
| `Rsi(5)` | `5` | one bar to take a first difference, five differences to seed the Wilder averages |
| `Roc(5)` | `5` | needs the value five bars back |
| `Cross` | the second ready bar | `0` until both inputs have a previous value |

Those match the kScript v3 table for `sma`, `ema` and `rsi`, so a port that waits for its indicator to be ready (the usual case) sees the same first signal. A kScript script that keyed off an indicator's very first bars, or one still running on the v2 engine's earlier partial-window values, shifts by a bar or two at the left edge of history; nothing changes past the warm-up window.

## The port loop

1. Open the kScript in the editor (**kScript (legacy)**, then **My Scripts** or **Community**), or fetch a published one: `om install @scope/their-indicator` is the plumbing (packages are inert text; nothing executes at install), then read the installed `kscript/script.ks`.
2. Start the target: **New indicator** in the editor, or on your machine `om wrun create @you/their-indicator-port --template sma-codefirst`.
3. Translate with the table: settings to params, series reads to inputs, builtins to `./sdk/ta` classes, plots to outputs, per-bar drawings to boxes and segments, owned drawings to handles. Keep the names: the accessors keep the code readable.
4. Run both on the same market and interval and eyeball the two curves before trusting the port. On your machine, `om wrun install . --replace` then `om metric series` on the same market the chart shows.
5. Publish under your scope ([Publishing](../functions/publishing.md)) once it matches.

The loop's machine commands, end to end:

```bash
om install @scope/their-indicator
om wrun create @you/their-indicator-port ./their-indicator-port --template sma-codefirst
om wrun install ./their-indicator-port --replace
om open @scope/their-indicator
```

## A worked port: RSI

The kScript shape being ported (a settings input, one stateful builtin, one lower-pane plot):

```text
study("RSI", overlay=false)
len = input(14, "Length")
plot(rsi(close, len))
```

The Indicator, shown here in its hand-written-sheet form (the shape a machine workspace scaffolded from the `sma` template uses; the declaration form is the same file with `param`, `input`, and `output` lines at the top and no sheet):

```json
{
  "id": "ported-rsi",
  "name": "Ported RSI",
  "abi_version": "wrun-1",
  "warmup_bars": 14,
  "params": [{ "name": "len", "default": 14, "min": 2, "max": 200, "description": "Length" }],
  "inputSources": { "close": { "source": "ohlcv", "field": "close" } },
  "inputs": [{ "index": 0, "name": "close" }],
  "outputs": [{ "index": 0, "name": "rsi", "plot": "line", "panel": "lower" }]
}
```

```typescript
import { in_close } from "./gen/inputs";
import { emitRow, out_rsi } from "./gen/outputs";
import { p_len } from "./gen/params";
import { Rsi } from "./sdk/ta";

let rsi = new Rsi(14);
let value: f64 = NaN;

export function init(): void {
  rsi = new Rsi(i32(p_len()));
}

export function state(): i32 {
  value = rsi.update(in_close());
  return isNaN(value) ? 0 : 1;
}

export function finalize(): void {
  out_rsi(value);
  emitRow();
}

export function reset(): void {
  rsi.reset();
  value = NaN;
}
```

Then prove it moves like the original:

```bash
om metric series --metric wrun/@you/ported-rsi/rsi --params len=14 --symbol BTCUSDT --exchange BINANCE_FUTURES --interval 1h --bars 60
```

Warm-up semantics differ between engines (`Rsi` is Wilder-smoothed and returns `NaN` until its window fills), so compare from the first ready bar onward, not from bar zero.

## Migration checklist

1. Port the file with the table; keep every name so the accessors read like the original.
2. Run both on one market and interval and compare from the first ready bar; the table above says where that bar is.
3. Replace `alert()` with a condition watch on the metric id after install; port `strategy()` and the `strategy.*` calls to the declaration and the builders ([Strategies overview](../strategies/overview.md)), and compare the trade lists on one market and interval: the broker is the same, so they pair.
4. Port owned drawings as handles (create, move, restyle, delete under an id) and keep the pool bounded; historical tape reads remain on kScript, while live prints have the daemon's `tape` buffer.
5. Publish once the curves match, then add the Indicator from the **Indicators** tab and let the kScript row keep its **Use kScript engine** fallback.

## What stays on the kScript engine

Launching the ORIGINAL kScript on a chart stays a reference flow (`om open @scope/their-indicator`), and alerts on it run on the platform's hosted engine, not on your machine. Port when the value itself must be local or alertable from your daemon; keep pointing at the original when it only needs to be drawn, and keep it on kScript when it lives on the constructs the table marks as staying there (the [construct map](./construct-map.md) lists them under no home yet).
