Your first indicator

View as MarkdownOpen the editor

From an empty editor to a published indicator with an alert, in six steps. Run the Moving Average starter, grow it into an EMA crossover with a mark on every cross, then publish it and let OpenMarket's cloud watch it for you.

Two moving averages with marks on each cross

Before you start

  • Sign in. The editor asks a signed-out visitor to sign in, and your account needs a verified email.
  • Pick a market that is not CME. Run is paused on CME markets.
  1. Open the editor and run the starter

    1. On the chart toolbar, press Editor (the code icon). When it is not pinned, it is in the Panels menu.
    2. In the editor's Explorer, press New indicator and pick Moving Average under Starters.
    3. Press Run. A 20-bar average draws on the chart.

    The tab's first line is the language marker, //@lang=wrun-ts, and the starter sits under it, a comment on every statement:

    // A simple moving average, declared and computed in one file. Read it top to bottom.
    // Nothing is imported: every word below (param, output, Sma, bar, out_sma) is already there.
    
    // A param is a setting the chart user can change, declared by its kind: a whole number here, with its name, its default, then the allowed range and the label the dialog shows.
    param.int("period", 20, { min: 1, max: 200, label: "Period in bars" });
    // An output is a per-bar number the Indicator sends back, here drawn as a line on the price panel under the label the legend shows.
    // output() returns a handle; bind it with a const only when a box() or segment() needs to name it.
    output("sma", line, overlay, { label: "Moving average" });
    
    let sma = new Sma(20); // a moving-average helper that keeps its own window; onStart() rebuilds it at the chosen period
    
    // onStart() runs once before the first bar: read each setting through its p_ reader (i32() = whole bars).
    function onStart(): void { sma = new Sma(i32(p_period())); }
    // onBar() runs once per bar: bar.close() is this candle's close, and out_sma() writes the output.
    // Until the window holds period bars the average is NaN, and a NaN is not drawn.
    function onBar(): void { out_sma(sma.update(bar.close())); }
    BTCUSDT perpetual on Binance, 1 hour bars, Aug 10 to Aug 18, 2026Real output from OpenMarket's engine
    • The parts are the ones from What is wrun: declarations, state, onStart() and onBar().
    • The first bars are empty. The average is NaN until its window fills, and NaN draws nothing.
  2. Swap in an EMA

    Replace the file under the marker with this, and press Run:

    param("period", 20, { min: 1, max: 200, description: "EMA length in bars" });
    output("avg", line, overlay, { color: "#2563eb", width: 2, description: "EMA of the close" });
    
    let ema = new Ema(20);
    
    function onStart(): void {
      ema = new Ema(i32(p_period()));
    }
    
    function onBar(): void {
      out_avg(ema.update(bar.close()));
    }
    BTCUSDT perpetual on Binance, 1 hour bars, Aug 10 to Aug 18, 2026Real output from OpenMarket's engine
    • Ema weights recent bars more than Sma, so it turns sooner.
    • The look is declared: color and width style the line, and each description labels a setting or a line.
  3. Add a faster average

    param("fast", 9, { min: 1, max: 200, description: "Fast EMA length" });
    param("slow", 21, { min: 2, max: 400, description: "Slow EMA length" });
    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);
    
    function onStart(): void {
      fast = new Ema(i32(p_fast()));
      slow = new Ema(i32(p_slow()));
    }
    
    function onBar(): void {
      const close = bar.close();
      const fastValue = fast.update(close);
      const slowValue = slow.update(close);
      if (isNaN(slowValue)) return;
      out_fast(fastValue);
      out_slow(slowValue);
    }
    BTCUSDT perpetual on Binance, 1 hour bars, Aug 10 to Aug 18, 2026Real output from OpenMarket's engine
    • A new param comes with a p_<name>() reader, and a new output with an out_<name>() writer.
    • onBar() returns early until the slow average has a value, so both lines start on the same bar.
  4. Mark every cross

    // 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. 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" });
    // 3. 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" });
    const isCrossUp = output("is_cross_up", none);
    // 4. A signal the alert dialog offers once the indicator is published.
    alert("crossed_up", { when: isCrossUp, message: "{{symbol}}: fast EMA crossed above the slow EMA at {{close}}", description: "Fast EMA crossed above slow EMA" });
    
    let fast = new Ema(9);
    let slow = new Ema(21);
    let cross = new Cross();
    
    function onStart(): void {
      fast = new Ema(i32(p_fast()));
      slow = new Ema(i32(p_slow()));
      cross = new Cross();
    }
    
    function onBar(): void {
      // The chart's own close for the averages, and its low to place the mark.
      const close = bar.close();
      const low = bar.low();
      const fastValue = fast.update(close);
      const slowValue = slow.update(close);
      const crossed = cross.update(fastValue, slowValue);
      if (isNaN(slowValue)) return;
      out_fast(fastValue);
      out_slow(slowValue);
      out_cross_up(low);
      out_is_cross_up(crossed == 1 ? 1.0 : 0.0);
    }
    BTCUSDT perpetual on Binance, 1 hour bars, Aug 10 to Aug 18, 2026Real output from OpenMarket's engine
    • Cross answers 1 on the bar the fast average crosses above the slow one, -1 on a cross down, 0 otherwise.
    • is_cross_up (plot kind none) holds that decision without drawing it; cross_up (plot kind shape) draws a mark at the bar's low only where shape_where says so.
    • alert(...) turns the decision into a signal the alert dialog offers once the indicator is published.
  5. Change a setting

    Open the indicator's settings on the chart. Each param is a row labelled by its description: set Fast EMA length to 12 and Slow EMA length to 26, and the chart reruns without compiling again.

  6. Publish it and add an alert

    1. Press Publish, type a name after @yourname/ (for example ema-cross), and publish. The first version is 0.1.0 (Publishing).
    2. On the receipt, press Open on chart.
    3. On that published indicator, open the alert dialog from its bell in the legend, pick the signal the file declared ("Fast EMA crossed above slow EMA"), and save.

    OpenMarket's cloud evaluates the alert and notifies you when it fires (Alerts). A draft can't carry one, so publish first.

If something goes wrong

  • Run stops. The Console names the problem and a squiggle marks the line: fix the first one and press Run again (Common errors).
  • Nothing draws. It is usually warm-up, and the Console says so: "Output 'sma' is NaN on all N ready bars, so nothing is drawn for it."

Next

Mark the crosses down too

A second gate on crossed == -1 and a second shape output at the bar's high.

Build something real

The Cookbook has complete recipes to read and adapt.

Read more than candles

What you can read.

See how it runs

Execution model.