Quick start: author on your machine

Build, install, and read an Indicator from a terminal in five steps. The same file the chart editor compiles authors from your machine with the om CLI and bun…

Build, install, and read an Indicator from a terminal in five steps. The same file the chart editor compiles authors from your machine with the om CLI and bun: scaffold a workspace from a template, build it, install it, and read its values as an ordinary metric. If you came from kScript (legacy), this is the flow that has no equivalent there: an Indicator is a package you hold, not a script the platform holds for you.

Anatomy of a workspace

Every Indicator workspace has three main parts:

  1. The sheet. wrun/metadata.json declares what the module reads and writes: params, inputs and their sources, outputs and their looks. In a code-first workspace it is derived from the declarations in your source; in a metadata-first workspace you write it by hand.
  2. The source. src/indicator.ts: the four functions and the state between them.
  3. The generated accessors. src/gen/: one reader or writer per declared name, regenerated before every build, so the code stays attached to names while the runtime passes values by position.

Step 1: Scaffold from a template

om wrun templates --format json
om wrun create @you/my-sma ./my-sma --template sma

om wrun templates lists the starting points: sma, sma-codefirst (the chart's New indicator seed), sma-box (the second starter: the same SMA with a box shading a band around it), vp-buy-share-codefirst (a celled volume-profile input with a text renderer, the worked footprint example in core-concepts/data-sources.md), polymarket-odds, conviction-score, event-asset-divergence, escalation-risk, and hud-terminal (a terminal-style readout drawn with anchored label handles). Every template scaffolds a complete workspace:

om-package.json            package manifest (@scope/name, version, kind)
package.json               local build scripts
asconfig.json              compiler settings
wrun/metadata.json         the sheet (what the module reads and writes)
src/indicator.ts           your file
src/sdk/sdk.ts             raw host imports (advanced; variable-index access)
src/sdk/ta.ts              stateful TA classes, one per kScript builtin (Sma, Ema, Rsi, ...)
src/sdk/declare.ts         GENERATED declaration SDK (param/input/output/box/segment);
                           regenerated by scaffold migrations, never edit
src/gen/                   GENERATED name-attached accessors; never edit
                           (strings.ts appears when the sheet declares string slots)
scripts/wrun-prebuild.mjs  GENERATED prebuild step run by `bun run build`; never edit
.wrun-scaffold             scaffold contract version
README.md, .gitignore

om wrun migrates a workspace scaffolded by an older release onto the current contract once, non-destructively: your source and sheet are never touched, the SDK files and build scripts are refreshed, and the migration is reported in the next build's warnings.

Step 2: Read the sheet and the source

The sma template is the moving average with a hand-written sheet instead of declarations. The sheet declares one param, one input, one output:

{
  "id": "my-sma",
  "name": "My Simple Moving Average",
  "abi_version": "wrun-1",
  "warmup_bars": 1,
  "params": [{ "name": "period", "default": 20, "min": 1, "max": 200 }],
  "inputSources": { "close": { "source": "ohlcv", "field": "close" } },
  "inputs": [{ "index": 0, "name": "close" }],
  "outputs": [{ "index": 0, "name": "sma", "plot": "line", "panel": "overlay", "unit": "price" }]
}

And the source reads and writes those names through the same accessors the chart's template uses:

import { in_close } from "./gen/inputs";
import { emitRow, out_sma } from "./gen/outputs";
import { p_period } from "./gen/params";
import { Sma } from "./sdk/ta";

let sma = new Sma(20);
let value: f64 = NaN;

export function init(): void {
  sma = new Sma(i32(p_period()));
}

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

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

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

Editing the sheet moves the SLOT inside each accessor while your source keeps the NAME, so reordering params or inputs never changes what the module computes. With declarations in the source the sheet is DERIVED at every build (it records generated_from: "declarations" plus a source_digest) and hand edits to it are refused; without declarations the hand-written sheet is the authority, and that is the only mode for languages without an extractor. Raw positional literals (getFloat(0), setOutput(0, ...)) are a build error either way; src/sdk/sdk.ts exists for variable-index access only. The full grammar is in functions/ta-library.md and functions/script-definition.md.

Step 3: Build, install, check

om wrun build ./my-sma
om wrun install ./my-sma --replace
om wrun validate ./my-sma

om wrun build regenerates src/gen from the sheet (deriving the sheet from declarations first), runs the local build (the receipt prints Compiler: assemblyscript (bun run build)), checks the module statically against the contract, and exports dist/package. The scaffold's local build runs bun install once per workspace (the compiler) and bun run build per compile; om wrun build drives both, and a plain bun run build inside the workspace runs the same prebuild plus compile without om on PATH. om wrun install is the same pipeline plus the local install under ~/.openmarket/packages, with a receipt naming the install path and the undo (om wrun remove @you/my-sma). On a compile error the command exits with the compiler's diagnostics: read them, edit, build again.

Step 4: See a value, see it move, see it drawn

om wrun list
om metric get --metric wrun/@you/my-sma/sma:period=20 --symbol BTCUSDT --exchange BINANCE_FUTURES
om metric series --metric wrun/@you/my-sma/sma --params period=20 --symbol BTCUSDT --exchange BINANCE_FUTURES --bars 60
om chart indicator preview --type wrun/@you/my-sma/sma --bars 300

om metric get returns the newest value; om metric series returns one [barOpenSec, value] pair per bar (oldest first, newest = the still-forming bar) and renders a sparkline in text mode; om chart indicator preview draws the draft on your active chart workspace without publishing. An installed output is an ordinary metric id everywhere else too: watch conditions (including crosses_above / crosses_below edges), screens, and backtests.

Step 5: Make it yours

Hard-coded periods are not very flexible, and one average is not much of a study. Scaffold a second workspace from the code-first template and replace src/indicator.ts with the classic off-chart study from the kScript quick start: the difference between a fast and a slow EMA, drawn as a histogram that is green above zero and red below. Params make the periods settings, and a data-only sign output feeds the histogram's per-bar color:

om wrun create @you/ema-diff ./ema-diff --template sma-codefirst
import { histogram, input, lower, none, ohlcv, output, param } from "./sdk/declare";
import { in_close } from "./gen/inputs";
import { emitRow, out_diff, out_sign } from "./gen/outputs";
import { p_fast, p_slow } from "./gen/params";
import { Ema } from "./sdk/ta";

param("fast", 7, { min: 1, max: 200, description: "Fast period" });
param("slow", 14, { min: 2, max: 400, description: "Slow period" });
input("close", ohlcv.close);
// A histogram in its own pane, colored per bar by the sign output: entry 0 below zero, entry 1 above.
output("diff", histogram, lower, { color_by: "sign", colors: ["#dc2626", "#16a34a"], description: "Fast EMA minus slow EMA" });
output("sign", none, lower);

let fast = new Ema(7);
let slow = new Ema(14);
let diff: f64 = NaN;

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

export function state(): i32 {
  const close = in_close();
  diff = fast.update(close) - slow.update(close);
  return isNaN(diff) ? 0 : 1;
}

export function finalize(): void {
  out_diff(diff);
  out_sign(diff > 0.0 ? 1.0 : 0.0);
  emitRow();
}

export function reset(): void {
  fast.reset();
  slow.reset();
  diff = NaN;
}

The color_by option names another output whose per-bar value indexes the colors palette (floored), so sign at 0 paints red and at 1 paints green; an output cannot color itself, which is why the decision is its own data-only output. Every param is a field in the overlay's settings dialog on the chart and a --params value on your machine.

Build and install it, then read the difference back with its params:

om wrun install ./ema-diff --replace
om metric series --metric wrun/@you/ema-diff/diff --params fast=7,slow=14 --symbol BTCUSDT --exchange BINANCE_FUTURES --bars 48

The moment the histogram turns positive is a one-line watch on the same metric id: params travel as a params object, and a condition with an edge operator names the interval the cross is read on:

om watch create "EMA difference turned positive" --condition '{"metric":"wrun/@you/ema-diff/diff","params":{"fast":7,"slow":14},"selector":{"symbol":"BTCUSDT","exchange":"BINANCE_FUTURES","interval":"HOUR"},"op":"crosses_above","value":0}'

Congratulations: you have built, installed, read, and armed an Indicator without opening a browser. Publishing from the CLI, scopes, visibility, and deleting are in functions/publishing.md; every verb under its om indicator spelling is getting-started/cli.md.

What's next

  • core-concepts/execution-model.md: the per-bar function, warm-up, no lookahead, and which host runs the file.
  • functions/moving-averages.md: Sma, Ema, and the averages you write yourself.
  • functions/plotting.md: every plot kind, panel, and styling option.
  • core-concepts/core-variables.md: what to keep between bars and how.