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 120quick-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:1The 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 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). |
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). |
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). 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). |
alert() / alertcondition() from the file | Nothing 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.