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…

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

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:

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:

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

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 v3Indicators
strategy(), strategy.entry / exit / close, the Strategy Tester, perps, fills, costs, statsstrategy({ ... }) 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).
Drawing handles: .setTop(), .delete(), a growing list of boxes or lines, 500 per kindhandles.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).
Per-bar labels with text, stickyRight, labels edited laterrender.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 barshandles.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 chartOn 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, plotMiniChartGridFrame 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). plotMiniChartGrid has no form: Not in Indicators yet.
input types select, color, string, source, timeframe, sessionNot 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 chartrange("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 librariesNot in Indicators yet. Paste the class into the file, or read the other package's output as a metric composition input (Libraries).
alert() / alertcondition() from the fileNothing in the file: install or publish, then a watch condition on the metric id (Alerts); 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.