---
title: "Build your first Indicator"
description: "Build a real EMA crossover step by step, one idea at a time, from a single average to a finished Indicator with a mark on every cross. On the last page you…"
order: 6
section: "getting-started"
---

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

# Build your first Indicator

Build a real EMA crossover step by step, one idea at a time, from a single
average to a finished Indicator with a mark on every cross. On the last page
you drew one line. Now we build something traders actually watch: when a
fast average rises above a slow one, momentum is turning up. We get there
in four small steps, each adding exactly one idea. Type along, press Run
after each step, and watch the chart change. Everything on this page is a
complete file: paste it under the `//@lang=wrun-ts` marker and it runs.

## Step 1: One moving average

Start from the template and change one thing: swap the simple average for an
exponential one. `Ema` weights recent bars more heavily, so it turns a
little sooner than `Sma`. The declarations, the state, and the four
functions are the ones you already know.

```typescript
import { input, line, ohlcv, output, overlay, param } from "./sdk/declare";
import { in_close } from "./gen/inputs";
import { emitRow, out_avg } from "./gen/outputs";
import { p_period } from "./gen/params";
import { Ema } from "./sdk/ta";

param("period", 20, { min: 1, max: 200, description: "EMA length in bars" });
input("close", ohlcv.close);
output("avg", line, overlay, { color: "#2563eb", width: 2, description: "EMA of the close" });

let ema = new Ema(20);
let value: f64 = NaN;

export function init(): void {
  ema = new Ema(i32(p_period()));
}

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

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

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

Same three parts as before, with two small changes. `new Ema(...)` replaces
`new Sma(...)`, and the output carries a `color`, a `width`, and a
`description`: the description is the legend text, and the color and width
are the line's look. Styling is declared on the output, never computed in
the code, which is why the chart can restyle a line without recompiling.

On the chart you'll see the jagged candles and, riding through the middle of
them, a calm blue line. That blue line is the trend.

## Step 2: A second, faster average

A moving average is an average that slides forward one bar at a time. The
period controls how much it smooths: a large period (like 21) reacts slowly
and shows the broad trend, while a small period (like 9) hugs price closely
and reacts fast.

That difference in speed is the whole idea behind a crossover. If we draw a
**fast** average and a **slow** average together, the fast one leads and the
slow one lags. The moment the fast crosses above the slow is the moment
short-term momentum has pulled ahead of the longer trend. Here are both,
each with its own setting:

```typescript
import { input, line, ohlcv, output, overlay, param } from "./sdk/declare";
import { in_close } from "./gen/inputs";
import { emitRow, out_fast, out_slow } from "./gen/outputs";
import { p_fast, p_slow } from "./gen/params";
import { 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: "#2563eb", width: 2, description: "Fast EMA of the close" });
output("slow", line, overlay, { color: "#f97316", width: 2, description: "Slow EMA of the close" });

let fast = new Ema(9);
let slow = new Ema(21);
let fastValue: f64 = NaN;
let slowValue: 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();
  fastValue = fast.update(close);
  slowValue = slow.update(close);
  return isNaN(slowValue) ? 0 : 1;
}

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

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

Every new output needs its `out_<name>` import and a write in `finalize()`;
every new param needs its `p_<name>` import and a read in `init()`. Notice
that `state()` waits for the slower average (`isNaN(slowValue)`), so both
lines start on the same bar instead of the fast one appearing twelve bars
early.

Now there are two lines: a blue fast EMA (period 9) and an orange slow EMA
(period 21). Watch how the blue line whips around more, while the orange one
glides. Where the blue line pokes up through the orange one, momentum is
flipping upward. That crossing point is what we want to catch.

## Step 3: Detecting the cross

Now we want to know the exact moment the fast line crosses above the slow
line, not eyeball it. The TA kit has a class for precisely this: `Cross`.
Construct one in `init()`, feed it both values once per bar, and it answers
`+1` on the single bar where the first value rises from at-or-below the
second to above it, `-1` on the opposite cross, and `0` everywhere else
(including any bar where either side is still `NaN`).

```text
let cross = new Cross();
let crossed: i32 = 0;

// in state(), after both averages are updated:
crossed = cross.update(fastValue, slowValue);
```

Read it out loud: "crossed is plus one when fast crosses over slow." On most
bars `crossed` is `0`. On the one bar where the blue line breaks above the
orange line, it flips to `1`. That single `1` is our signal. Next we draw
something there.

There is no `if` around a plot in an Indicator, because an Indicator never
calls a plot function. A decision is a number, so the cross becomes an
output of its own: `1` on the signal bar, `0` otherwise. Outputs that exist
only to carry a decision are declared with the plot kind `none`: computed
every bar, drawn never.

## Step 4: Marking the cross on the chart

A `0` or `1` is invisible. To see the signal we draw a mark, but only on the
bars where the cross happened. An output declared with the plot kind
`shape` draws a mark at its value on every bar it has one; the `shape_where`
option names a gate output, and the mark draws only on bars where the gate
is nonzero. Put the mark's value at the bar's low so it sits just under the
candle, out of the way of the price action.

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

// 1. Two settings.
param("fast", 9, { min: 1, max: 200, description: "Fast EMA length" });
param("slow", 21, { min: 2, max: 400, description: "Slow EMA length" });
// 2. The chart's own close for the averages, and its low to place the mark.
input("close", ohlcv.close);
input("low", ohlcv.low);
// 3. Two lines on the price pane.
output("fast", line, overlay, { color: "#2563eb", width: 2, description: "Fast EMA of the close" });
output("slow", line, overlay, { color: "#f97316", width: 2, description: "Slow EMA of the close" });
// 4. A mark at the bar's low, drawn only where the gate is 1.
output("cross_up", shape, overlay, { color: "#16a34a", shape_where: "is_cross_up", description: "Fast EMA crossed above slow EMA" });
output("is_cross_up", none);

let fast = new Ema(9);
let slow = new Ema(21);
let cross = new Cross();
let fastValue: f64 = NaN;
let slowValue: f64 = NaN;
let low: f64 = NaN;
let crossed: i32 = 0;

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();
  low = in_low();
  fastValue = fast.update(close);
  slowValue = slow.update(close);
  crossed = cross.update(fastValue, slowValue);
  return isNaN(slowValue) ? 0 : 1;
}

export function finalize(): void {
  out_fast(fastValue);
  out_slow(slowValue);
  out_cross_up(low);
  out_is_cross_up(crossed == 1 ? 1.0 : 0.0);
  emitRow();
}

export function reset(): void {
  fast.reset();
  slow.reset();
  cross.reset();
  fastValue = NaN;
  slowValue = NaN;
  low = NaN;
  crossed = 0;
}
```

A few things to unpack here:

- **`output("cross_up", shape, overlay, ...)`** draws a mark instead of a
  line. Its value is the bar's low, so the mark sits under the candle; its
  `color` is the mark's color.
- **`shape_where: "is_cross_up"`** names the gate. The mark draws only on
  bars where `is_cross_up` is nonzero. Everywhere else, nothing is drawn.
- **`output("is_cross_up", none)`** is the decision itself: `1` on the cross
  bar, `0` otherwise. It never draws, but it exists, and once the Indicator
  is published and installed it is a metric an alert can watch crossing
  above `0`.
- **`crossed == 1 ? 1.0 : 0.0`** turns the `Cross` answer into that output.
  Outputs are always `f64`, so the integer answer becomes a float here.

Press Run, and you'll see the two EMAs weaving across each other, with a
green mark under the candle at each upward cross. That is a genuine
momentum signal, built from the same three parts you learned on page one
plus two new ideas: a class that detects a condition (`Cross`) and an output
that draws only when another output says so (`shape` with `shape_where`).

## Step 5: Change a setting without recompiling

Open the overlay's settings on the chart. Every `param(...)` is a setting
there: change `fast` to 12 and `slow` to 26 and both lines recompute without
recompiling. To change what the setting offers (its default or its range),
edit the declaration and Run again.

## Save, download, publish

**Save** keeps the file under **My Scripts**, beside your kScript (legacy)
scripts. **Download package** hands you `<name>-<version>.tgz` once the
Indicator compiles cleanly: the built module, its sheet (`wrun/metadata.json`,
derived from your declarations), and the manifest (`om-package.json`),
exactly what the registry and the `om` CLI consume. **Publish** opens the
registry in a popup: a package name (`/name`), a version
(`MAJOR.MINOR.PATCH`), a visibility, a description. A published version is
permanent (bump the version to revise) and it is what unlocks everything
beyond the draft: other people add it from the Indicators tab, alerts can
watch it (an alert on a draft is refused with "Publish the Indicator before
adding an alert"), and `om install /name` puts it on any machine
(`functions/publishing.md`).

## Next

You now know enough to read most Indicators and write simple ones. The next
page points you to where to go from here: complete real-world recipes, how
the host runs your code, and the references you will reach for most.

[Next steps](primer-next-steps.md)
