First steps

Write your first Indicator and see a line appear on the chart. No prior experience with Indicators, or with kScript (legacy), is needed: by the end of this…

Write your first Indicator and see a line appear on the chart. No prior experience with Indicators, or with kScript (legacy), is needed: by the end of this short page you will have a moving average drawing on a live chart, compiled in your own browser, and you will know the three parts every Indicator shares.

What is an Indicator?

An Indicator is one TypeScript file. You write it in the chart's editor, Run compiles it in your browser and draws it on the chart, and the same file publishes to the registry and runs on your own machine unchanged. Every output is a number series: a line, a shape, a box coordinate, a metric an alert can watch. The module has no filesystem, no network, and no order capability; the host feeds it one bar at a time and reads the numbers it writes back.

Nothing to install: you write the file in the browser and it runs against live and historical market data immediately (the terminal path, the same file with the om CLI, is quick-start.md).

Where it lives

  1. On the chart toolbar, open Indicators. The dialog opens on the Indicators tab: the packages you can add straight to the chart.
  2. The kScript (legacy) tab holds My Scripts, Community, and Marketplace: the scripts that run on the kScript engine.
  3. Build opens the editor. In its sidebar, New indicator starts a TypeScript Indicator; New kScript (legacy) starts a kScript.

A kScript row that has a proven Indicator port shows a New engine badge. Adding that row mounts the Indicator by default; Use kScript engine on the row adds the kScript version instead and keeps the kScript engine for that indicator.

Everything happens in the editor. Type the file, press Run, and your Indicator appears on (or below) the chart right away. Change a line, press Run again, and the chart updates. That tight loop is the whole workflow: write, run, look at the chart, repeat.

The shape of every Indicator

Before writing anything, here is the mental model. Every Indicator has the same three moving parts:

  1. Declarations. A few plain statements at the top of the file say what the Indicator reads and writes: param(...) for a setting the chart user can change, input(...) for a per-bar number the chart feeds in, output(...) for a per-bar number the Indicator sends back and the chart draws. No output, nothing on the chart.
  2. State. Module-level variables hold whatever must survive from one bar to the next: a moving average's window, the previous close, a counter. There is no history array to index; what you want to remember, you keep.
  3. Four functions. init() runs once before the first bar, state() runs once per bar, finalize() writes the outputs for a bar that has a value, and reset() puts the state back to its starting point. The host calls them; you never call them yourself.

There is one more rule, and it is easy: the first line of the file is the language marker //@lang=wrun-ts. It is how the editor knows the tab holds an Indicator rather than a kScript. Leave it alone; everything you write goes under it.

Step 1: New indicator

The editor opens a file whose first line is the language marker:

//@lang=wrun-ts

Everything under it is the sma-codefirst template, a complete moving average in 25 non-blank lines, a comment on every statement. Read it top to bottom, then press Run:

// A simple moving average, declared and computed in one file. Read it top to bottom.
import { input, line, ohlcv, output, overlay, param } from "./sdk/declare"; // the declaring words
import { in_close } from "./gen/inputs"; // generated: an in_ reader for each input declared below
import { emitRow, out_sma } from "./gen/outputs"; // generated: an out_ writer per output, plus emitRow
import { p_period } from "./gen/params"; // generated: a p_ reader for each param declared below
import { Sma } from "./sdk/ta"; // a moving-average helper that keeps its own window of values

// A param is a setting the chart user can change: its name, its default, then the allowed range.
param("period", 20, { min: 1, max: 200 });
// An input is a per-bar number the chart feeds in; ohlcv.close is each candle's close price.
input("close", ohlcv.close);
// An output is a per-bar number the Indicator sends back, here drawn as a line on the price panel.
// output() returns a handle; bind it with a const only when a box() or segment() needs to name it.
output("sma", line, overlay);

let sma = new Sma(20); // module state lives between bars; init() rebuilds it at the chosen period
let value: f64 = NaN; // this bar's average (f64 is the number type), NaN until the window is full

// init() runs once before the first bar: read each setting through its p_ reader (i32() = whole bars).
export function init(): void { sma = new Sma(i32(p_period())); }
// state() runs once per bar: read the bar through its in_ reader and fold it into the window.
// Warmup rows are NaN: until the window holds period bars there is no average yet, so return 0
// and the chart draws nothing for that bar; return 1 once the value is real.
export function state(): i32 { value = sma.update(in_close()); return isNaN(value) ? 0 : 1; }
// finalize() runs after state() returns 1: write each output through its out_ writer, then emitRow().
export function finalize(): void { out_sma(value); emitRow(); }
// reset() runs when the chart restarts the series: put the state back to what init() built.
export function reset(): void { sma.reset(); value = NaN; }

All three parts are there. Let's walk through them.

Read the template

Three declarations at the top say what the Indicator reads and writes. They are plain statements, extracted from the file before it compiles, so every name must be a string literal and every default a literal number:

  • param("period", 20, { min: 1, max: 200 }) is a setting. It shows in the overlay's settings dialog with that default and range.
  • input("close", ohlcv.close) is a feed: the chart's own close price, one value per bar.
  • output("sma", line, overlay) is what the chart draws: a line on the price pane. lower instead of overlay puts it in its own pane; none instead of line computes it without drawing it.

The generated imports (./gen/params, ./gen/inputs, ./gen/outputs) are how the code reads and writes those names: p_period(), in_close(), out_sma(value). Rename a declaration and its accessor renames with it; the compiler then points at every stale import.

Four functions do the work, and the build refuses any other signature:

FunctionRunsDo this here
init()once, before the first barread params, size your averages
state()once per barread inputs, update your state, return 1 when the bar has a value or 0 while warming up
finalize()once per bar that returned 1write every output, then emitRow() last
reset()when the chart replays the forming barclear every module-level variable and .reset() every TA object

Every TA class (Sma, Ema, Rsi, Cross, and the rest of the kScript catalog) comes from ./sdk/ta: construct them in init(), feed them one value per bar with .update(), and they return NaN until their window is full. Returning 0 from state() on those bars is what keeps the line empty there instead of drawing garbage (core-concepts/execution-model.md).

Step 2: Run

Press Run. The editor compiles the file in your browser (the same compiler version the CLI uses, so the bytes match) and mounts the draft on the chart as an overlay. Errors show as squiggles under the offending characters and as rows in the Problems lane; fix them and Run again. The build has a few stages, so a message may come from the declaration extractor, the sheet validator, or the compiler; faq/common-errors.md lists the ones you will meet.

Indicators run in your browser: every input reads the chart's own market and interval, the forming bar re-evaluates on every tick, and nothing is sent anywhere to compute.

What you'll see

A line tracing the 20-bar average of the close, sitting right on the price chart. The first nineteen bars of loaded history are empty: the window is not full yet, state() returned 0, and the chart drew nothing. From the twentieth bar on the line follows price, smoothed. It is not a fancy indicator, but it proves the whole pipeline works: your code received real bars, computed a number for each, and the chart drew them. That is the foundation for everything else.

One small thing to notice

Look at the output line again: output("sma", line, overlay). Every output needs a name, and the name does three jobs at once. It is the legend entry on the chart, it is the writer you call in finalize() (out_sma), and once the Indicator is published and installed it is the tail of a metric id (wrun/@you/my-sma/sma) that an alert, a screen, or a backtest can read. Names are unique per Indicator and lowercase with underscores; pick clear ones from the start.

That's it. You have a running Indicator. If your line showed up, the hard part is over. Everything from here is doing more interesting things with the same three pieces.

Next

Now let's turn this into something you would actually use. On the next page we build a real indicator step by step: a moving-average crossover signal with a mark on every cross.

Build your first Indicator