---
title: "What Indicators add"
description: "kScript (legacy) v3 turned a script from plot-an-indicator into model-a-trading-idea. Indicators keep everything scalar that v3 could do and add what a script…"
order: 1
section: "getting-started"
---

<!-- source: docs/indicators/getting-started/whats-new-v3.md; generated by packages/cli/scripts/gen-indicator-docs.ts, do not edit -->

# What Indicators add

kScript (legacy) v3 turned a script from plot-an-indicator into
model-a-trading-idea. Indicators keep everything scalar that v3 could do and
add what a script held by the platform never could: the file compiles in
your browser, runs on your own machine, every output is a metric id that
alerts, screens, series, and backtests can read, the result is an
installable package, shapes are declared or held by handle instead of
drawn, strategies place their own orders, and celled inputs bring
footprints and order books into one bar. This page is the tour of those
additions, and an honest list of the v3 constructs that stay on kScript
for now.

## The two engines side by side

kScript stays exactly where it is: published kScript indicators keep
charting and hosted-alerting forever on the pinned v3 engine, and nothing
existing breaks. An Indicator is a different artifact; the side-by-side
table and every claim with its proof are on [Why Indicators](why-indicators.md).
In one line each: a file you hold instead of a script the platform holds,
compiled in your browser with the compiler the CLI uses, run on your own
machine, read by metric id, published as a versioned package, drawn by
declaration or by handle, with 52 stateful TA classes and a strategy
channel in the file.

## The headline additions

### It compiles in your browser

Press **Run** and the editor compiles the file with the same compiler
version the CLI uses, so the bytes match, and mounts the draft on the chart
as an overlay. Errors are squiggles with a line and column; nothing is sent
anywhere to compute. The first Indicator you meet is the moving average in
`primer-first-steps.md`, and the headline kScript example, a moving-average
cross that opens on the way up and closes on the way down, is the same
shape with a `Cross` and a position that persists between bars:

```typescript
import { input, line, lower, ohlcv, output, overlay, param } from "./sdk/declare";
import { in_close } from "./gen/inputs";
import { emitRow, out_fast, out_position, out_slow } from "./gen/outputs";
import { p_fast, p_slow } from "./gen/params";
import { Cross, Ema } from "./sdk/ta";

param("fast", 9, { min: 1, max: 200, description: "Fast EMA length" });
param("slow", 21, { min: 2, max: 400, description: "Slow EMA length" });
input("close", ohlcv.close);
output("fast", line, overlay, { color: "#4f8cff", width: 1, description: "9-period EMA of close" });
output("slow", line, overlay, { color: "#f59e0b", width: 1, description: "21-period EMA of close" });
output("position", line, lower, { color: "#16a34a", description: "1 while the fast EMA is above the slow one since a cross, else 0" });

let fast = new Ema(9);
let slow = new Ema(21);
let cross = new Cross();
let fastValue: f64 = NaN;
let slowValue: f64 = NaN;
let position: f64 = 0.0; // persists between bars: the model's one piece of state

export function init(): void {
  fast = new Ema(i32(p_fast()));
  slow = new Ema(i32(p_slow()));
  cross = new Cross();
}

export function state(): i32 {
  const close = in_close();
  fastValue = fast.update(close);
  slowValue = slow.update(close);
  const crossed = cross.update(fastValue, slowValue);
  if (crossed == 1) position = 1.0; // the cross up opens
  if (crossed == -1) position = 0.0; // the cross down closes
  return isNaN(slowValue) ? 0 : 1;
}

export function finalize(): void {
  out_fast(fastValue);
  out_slow(slowValue);
  out_position(position);
  emitRow();
}

export function reset(): void {
  fast.reset();
  slow.reset();
  cross.reset();
  fastValue = NaN;
  slowValue = NaN;
  position = 0.0;
}
```

### It runs on your machine

The same file scaffolds, builds, and installs from a terminal, and every
output becomes a metric you can read the history of, on any market the
platform serves, with the pins the chart cannot honor yet:

```bash
om wrun create @you/ma-cross ./ma-cross --template sma-codefirst
om wrun install ./ma-cross --replace
om metric series --metric wrun/@you/ma-cross/position --params fast=9,slow=21 --symbol BTCUSDT --exchange BINANCE_FUTURES --bars 120
```

`quick-start.md` is the five-step version.

### Metric ids for alerts, screens, series, backtests

An installed output is an ordinary metric id (`wrun/@scope/name/<output>`)
everywhere a metric goes: the CLI takes its params as a `:k=v` suffix, a
watch condition as a `params` object. The `position` output
above turning to `1` is one watch condition, and its edge is read on the
interval you name:

```bash
om watch create "MA cross opened" --condition '{"metric":"wrun/@you/ma-cross/position","params":{"fast":9,"slow":21},"selector":{"symbol":"BTCUSDT","exchange":"BINANCE_FUTURES","interval":"HOUR"},"op":"crosses_above","value":0}'
om metric screen --metric wrun/@you/ma-cross/position:fast=9,slow=21 --exchange BINANCE_FUTURES --top-n 50 --by VOLUME_24H --filter gte:1
```

The same id sits on either side of a `crosses_above` / `crosses_below`
compare (a golden cross between a package's own `fast` and `slow` outputs)
and replays in a backtest. kScript plots were readable in the legend;
Indicator outputs are readable by everything.

### Installable packages

**Publish** on the chart (or `om publish` from your machine, [Publishing](../functions/publishing.md))
puts a versioned package under your scope. A published version is permanent: other
people add it from the Indicators tab, `om install @scope/name` puts it on
any machine, and a watch records the exact version and module hash it was
armed against, so an Indicator's behavior can never change underneath it.
Scopes, visibility, and deleting are in [Publishing](../functions/publishing.md).

### Declared shapes

kScript v3 made drawings handles you create and `.delete()`. An Indicator
declares a shape once and the host evaluates it on every bar from outputs
the module already computes: `box("zone", { top, bottom, when })` is a
rectangle on every bar where `when` is nonzero, a segment with `from: 0,
to: 1` chains into a horizontal level, and a zone that should end is a gate
turning `0` (`functions/drawing-objects.md`; the cookbook's zone tracker is
the worked port).

### Celled inputs: footprints and books in one bar

A scalar input serves one number per bar. A celled input serves a whole
block per bar: `volume_profile.cells` is one `[low, high, buy, sell]` row
per price bucket, `book.cells` one `[price, size, side]` row per level. "Did
buyers or sellers do the volume, and at which prices" is answerable inside
one bar instead of only as a per-bar total. The `vp-buy-share-codefirst`
template is the worked example (`core-concepts/data-sources.md`).

### Context beyond price

`funding`, `oi`, side-split `liquidations`, `implied_volatility` and `skew`
at a tenor, `token_supply`, `odds` (Polymarket probabilities, pinned or
bound per use), and the `time` source for session math; three of the nine
templates combine event odds with price and funding.

## What stays on kScript (legacy) for now

Most of the v3 surface has an Indicator form now (strategies, drawing
handles, labels, polylines, panels and profiles landed with the third and
fourth contracts); each row below names it, and the rows that still say
**Not in Indicators yet** are the honest remainder. The doc treatment is
fixed: a pointer page or an in-page callout, never a promise.

| kScript v3 | Indicators |
| --- | --- |
| `strategy()`, `strategy.entry` / `exit` / `close`, the Strategy Tester, perps, fills, costs, stats | `strategy({ ... })` from `./gen/strategy` beside the outputs, `strategy.long(id)` / `short` / `exit` / `close` builders in `finalize()`, the engine's own broker run by the host, and `om backtest` replaying the trade list; perps, fees, slippage and the stats block are the engine's own ([Strategies overview](../strategies/overview.md)). |
| Drawing handles: `.setTop()`, `.delete()`, a growing list of boxes or lines, 500 per kind | `handles.line` / `box` / `label` / `polyline` declared once, then `draw.box(id).set(...)`, `.setLeftTop()`, `.fill(...)`, `.delete()` under an integer id, 500 live per kind and 1,500 in all; declared boxes and segments (16 each) stay the per-bar form ([Drawing objects](../functions/drawing-objects.md)). |
| Per-bar labels with text, `stickyRight`, labels edited later | `render.text` draws one mark per bar from a string slot, `render.label` one corner label; a label edited later is a `handles.label` moved, re-worded (`.text(str_<slot>_sb)`), anchored and aligned by id. `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-sent as the path grows, at most 256 points per handle; `draw.polyline` from `./sdk/declare` stays the declared form (64 output pairs, placed from the newest bar). |
| `htf()` / `request()` on the chart | On the chart the higher timeframe is a bucket fold on `time.bar_open_sec`; on your machine an `interval` pin reads a real coarser feed as of its close. The chart honoring pins is the open item (`core-concepts/multi-timeframe.md`). |
| `ltf()`, `requestBars()` | **Not in Indicators yet.** No lower-timeframe celled class. |
| Tape by order size (`trade_volume_by_size`) | **Not in Indicators yet.** The class is declared for cross-host parity and refused by name at fetch. |
| Foreign-domain panes (`plotMatrix`, `plotCurve`, `plotTiles`), `plotPie`, `plotMiniChartGrid` | Frame snapshots under the fourth contract: `panel.heatmap`, `panel.bars` / `line` / `scatter` / `histogram`, `panel.tiles`, `panel.pie`, placed below or beside the chart, plus docked level profiles and ladder, feed and meter widgets ([Drawing primitives](../functions/drawing-primitives.md)). `plotMiniChartGrid` has no form: **Not in Indicators yet.** |
| `input` types `select`, `color`, `string`, `source`, `timeframe`, `session` | **Not in Indicators yet.** Every param is an `f64`; a color is declared per output, a venue is a pinned declaration. |
| Runtime color math (`lighten`, `darken`, `blend`, a continuous `colorGradient`) | **Not in Indicators yet.** Color is declared; `color_by` with a `colors` palette is a bucket ladder. |
| `fillBetween` from source, `range()` drawn on the chart | `range("upper", "lower", { colors, opacity })` declares a filled band between two outputs and the chart draws it; a band that should appear on some bars only is a per-bar box with a `when` gate. |
| `import "@scope/lib"` cross-package libraries | **Not in Indicators yet.** Paste the class into the file, or read the other package's output as a `metric` composition input ([Libraries](../functions/libraries.md)). |
| `alert()` / `alertcondition()` from the file | Nothing in the file: install or publish, then a watch condition on the metric id ([Alerts](../functions/alerts.md)); an alert armed from the chart runs hosted where the site has it switched on and reads candles only today. |
| 70+ TA builtins (`atr`, `bb`, `macd`, `stoch`, `adx`, `supertrend`, `vwap`, ...) | One stateful class per builtin ships in `src/sdk/ta.ts` (`Atr`, `Bb`, `Macd`, `Stoch`, `Adx`, `Supertrend`, `Vwap`, ...), each checked bar for bar against the kScript engine; `functions/ta-library.md`. |

## Compatibility

Every kScript keeps running, and a script with a published Indicator port
gets a **New engine** badge on its row (**Use kScript engine** keeps the
original). Warm-up differs between engines (`Ema` seeds with a simple
average, `Rsi` is Wilder-smoothed, an abstaining `state()` draws nothing),
so compare a port from the first ready bar onward. The construct table for
porting is `migrations/from-kscript.md`.

## The language marker

An Indicator in the chart editor starts with `//@lang=wrun-ts` on its first
line; it is how the editor knows the tab holds an Indicator rather than a
kScript. The CLI does not need it: a workspace's `src/indicator.ts` starts
with its imports. There is no version number to bump: the contract the
module runs under is declared by the sheet (`abi_version`, derived from
your declarations), and the frozen first contract keeps every published
package running bit-identically forever.

If something does not behave as documented, `faq/common-errors.md` maps
every real build and chart message to its cause and fix.
