Debugging

View as MarkdownOpen the editor

A practical workflow for finding out why a wrun indicator is wrong: make visible what you cannot see, read the Console, isolate one output, read the numbers at the Console prompt, then check the usual suspects. Work it in order.

Your indicator compiles and runs but the signal is wrong, or it draws nothing, or a line is mysteriously flat. There is no console.log (the module runs in a sandbox with no console), but there is a debug log that prints in the editor's Console, a prompt that reads the last run's numbers, and one fact that makes indicators easy to debug: every value the module computes can be an output.

  1. Emit what you cannot see

    The fastest debugger is an output or a log line. You cannot step through the bar loop, but you can make any intermediate value visible on every bar. Three forms:

    • A lower line shows the shape of a value over every bar. Seeing it usually tells you immediately whether it is doing what you think: pinned at zero, flatlining, a spike where there should be none.
    • A none output is a probe: computed and written every bar, never drawn, and readable by name at the Console prompt (step 4). It keeps the picture clean while you check a number.
    • The debug log prints text: a string slot named debug, written in onBar(), prints each bar's line in the editor's Console.
    param("len", 14, { min: 2, max: 200 });
    // The value under suspicion, drawn in its own pane so its shape is visible on every bar.
    output("range_pct", line, lower, { unit: "%", color: "#ef4444", description: "high minus low as a percent of close" });
    output("smooth", line, lower, { unit: "%", color: "#f59e0b", description: "EMA of the range" });
    // A probe: computed every bar and readable at the Console prompt, never drawn. Delete it when done.
    output("raw_spread", none, lower, { description: "debug: high minus low before the percent" });
    
    let ema = new Ema(14);
    
    function onStart(): void {
      ema = new Ema(i32(p_len()));
    }
    
    function onBar(): void {
      const close = bar.close();
      const spread = bar.high() - bar.low();
      const rangePct = close > 0.0 ? (spread / close) * 100.0 : NaN;
      if (isNaN(rangePct)) return;
      out_range_pct(rangePct);
      out_smooth(ema.update(rangePct));
      out_raw_spread(spread);
    }
    BTCUSDT perpetual on Binance, 1 hour bars, Aug 10 to Aug 18, 2026Real output from OpenMarket's engine

    The debug log is the indicator's print(). Declare string("debug", { max_bytes }), build a line with the allocation-free sb_* builder, and send it with str_debug_sb() (or send a finished string with str_debug(text)). Write it only on the bars worth reading: a bar that writes nothing prints nothing.

    param("len", 20, { min: 2, max: 200, description: "Bars in the volume average" });
    input("volume", ohlcv.volume);
    output("ratio", line, lower, { description: "Volume over its average" });
    // The debug log: a string slot named exactly debug. Each non-empty line prints in the editor's Console.
    string("debug", { max_bytes: 128 });
    
    let average = new Sma(20);
    
    function onStart(): void {
      average = new Sma(i32(p_len()));
    }
    
    function onBar(): void {
      const volume = bar.volume();
      const mean = average.update(volume);
      if (isNaN(mean) || mean <= 0.0) return;
      const ratio = volume / mean;
      out_ratio(ratio);
      // Log only the bars worth reading. The builder allocates nothing, so logging never grows memory.
      if (ratio > 3.0) {
        sb_clear();
        sb_text("spike ratio=");
        sb_f64(ratio, 2);
        sb_text(" volume=");
        sb_f64(volume, 0);
        str_debug_sb();
      }
    }
    BTCUSDT perpetual on Binance, 1 hour bars, Aug 10 to Aug 18, 2026Real output from OpenMarket's engine

    After a Run, the Console prints one line per bar whose slot is not empty, oldest bar first, after the bar's time in ISO 8601 UTC:

    text
    2026-09-30T14:00:00.000Z spike ratio=3.41 volume=1824
    2026-09-30T19:00:00.000Z spike ratio=4.07 volume=2210

    Every run rebuilds the lines, so they always belong to the run on screen, and the Console keeps the newest 400 ("… N earlier print lines truncated to keep the console fast"). The lines print before the chart draws, so they are there even when drawing fails. Build the text with sb_*, not with + or toString(): text built that way allocates on every bar, and memory that grows once the bars start stops the run.

    When you need the exact number on the chart rather than in the Console, a string slot and a render.label print it on the newest bar. It switches the file to the second runtime contract, which Run derives for you:

    output("range_pct", line, lower, { unit: "%", color: "#ef4444" });
    output("tag_x", none, lower, { description: "This bar's open time, the label's x" });
    string("readout", { max_bytes: 32 });
    // One label, on the newest bar that wrote the slot: the exact number, read off the chart.
    render.label("range_readout", { x: "tag_x", y: "range_pct", text: "readout", color: "#f59e0b", size: 12 });
    
    function onBar(): void {
      const close = bar.close();
      const rangePct = close > 0.0 ? ((bar.high() - bar.low()) / close) * 100.0 : NaN;
      if (isNaN(rangePct)) return;
      out_range_pct(rangePct);
      out_tag_x(bar.time());
      sb_clear();
      sb_text("range% = ");
      sb_f64(rangePct, 2);
      str_readout_sb();
    }
    BTCUSDT perpetual on Binance, 1 hour bars, Aug 10 to Aug 18, 2026Real output from OpenMarket's engine

    This works for any expression. Lift the part you doubt into a variable, declare an output or a log line for it, and read the answer instead of guessing. Delete the probe and the log when you are done: whoever adds a published indicator gets everything it declares.

  2. Read the Console

    If Run stops before the chart changes, the message is in the editor's Console, tagged with the stage that produced it, and one problem hides the ones behind it. The status bar's problem counter (its tooltip reads Open Problems) opens the Console, and clicking a row jumps to its line:

    StageWhat it checkedTypical message
    declarationsthe param / input / output / shape statements, read from the textoption 'top' takes an output handle, not a string literal
    lintraw positional slot literals in your sourceRaw slot literal 0 passed to getFloat(): raw positional slots rebind silently when the sheet changes
    metadatathe sheet derived from your declarations, against the schemaOutput 'price' (color_by): color_by needs 'colors' with at least 2 entries
    compilethe AssemblyScript compiler, with a hintConversion from type 'f64' to 'i32' requires an explicit cast.
    validatethe built module's exports and the sandboxconsole.* is not available in an Indicator (it runs in a sandbox with no console).
    computethe run itself, after RunAssemblyScript abort: Index out of range at ~lib/array.ts:...

    Fix the first one and Run again. Every message is listed with its fix in Common errors.

    A run that reaches the chart and draws nothing is usually not an error at all. It is an output that is NaN on every bar (onBar() returned before writing it, or wrote NaN), an output declared none, or a source the chart's market does not serve (the chart says which, by name). For a draft, the Console says which after the run: Output 'range_pct' is NaN on all 480 ready bars, so nothing is drawn for it. Step 5 covers the rest.

  3. Isolate

    When several outputs are wrong at once, stop reasoning about all of them. Reduce the module to one output: leave the others unwritten in onBar() (an unwritten output is NaN, and NaN draws nothing), or switch their plot to none, and keep only the suspect series drawn. Once that one is correct, bring the others back one at a time. This is the cheapest way to find which input poisoned a calculation downstream, because a wrong value in an indicator has exactly one place it can come from: the bar's inputs, the state carried from the previous bar, or the arithmetic between them.

  4. Read the numbers at the Console prompt

    The chart shows shapes; the Console prompt shows numbers. After a Run, type at the prompt ("Type an expression, an output name, or / for commands") to read the last run without compiling anything:

    text
    range_pct                          the newest value
    range_pct[-3]                      the value three bars back
    last 20 raw_spread                 the newest 20 bars (a none output reads like any other)
    at 2026-01-01T00:00Z range_pct     the value on the bar that opened at that time
    rows                               the bar count, the first and last bar, the warm-up rows
    params                             each param and the value the last run used
    outputs                            every output and its last value
    sheet                              the sheet derived from your declarations

    A range reads over the ready bars; warm-up rows are left out. Anything else you type is an expression: it compiles into the current source, runs once per ready bar after your own code for the bar, over the chart's candles, and leaves the overlay alone, so your module-level variables and the kit are in scope. Expressions run over candles only: a strategy indicator, or one that reads another source, is refused ("The console does not run strategy Indicators; Run the tab and inspect its outputs."), and its outputs stay readable by name after a Run. /help lists the grammar, and /clear empties the Console.

  5. The usual suspects

    Most "wrong indicator" bugs are one of a handful of patterns. Scan this list against your symptom:

    • Line starts blank, then appears. Warm-up. Anything with a period is NaN until it has enough bars, and a NaN output draws nothing on those bars. That leading gap is expected; if the line never appears, the chart has fewer bars loaded than the window needs.
    • Flat line. A module-level variable assigned in onStart() and never updated in onBar(), or an output written from a different variable than the one the bar updates. Log the value per bar and check that it moves.
    • Everything is NaN. A secondary input under missing: "nan" on bars without an observation, an arithmetic chain fed by one NaN input, or a division by zero. Probe each input as a none output and find the first NaN.
    • Wrong pane. A percent or a count drawn overlay hugs the price axis floor. Declare it lower.
    • A mark on every bar. A shape output without shape_where. Add the gate output.
    • The newest bar jumps around. It is still forming: the chart re-evaluates it as live updates arrive, at most about once a second, starting each time from the module's state as it stood after the last closed bar. A bar's value settles when the bar closes.
    • Nothing at all, no error. onBar() never reaches a write (a class whose period exceeds the loaded history), the only rendered output is declared none, or the chart's market does not serve the source (the chart names it, for example celled source class 'tape' is not served by the browser lane yet, or Volume profile data is unavailable for ...).
    • A pinned input reads NaN everywhere. An index pin (POLYGON_INDICES) reads NaN, and the legend says why; any pin reads NaN until its first candle closes. A misspelled pin is refused instead: Pinned market candle (...) data is unavailable for ... (Exchange and symbol format).
    • An alert does not match the chart. An alert runs the published version and the settings of the overlay it was set on, in OpenMarket's cloud, on the chart's own market; the draft in your editor is not what it evaluates. Publish the change, add the new version to the chart, and set the alert on it. A declared alert(...) fires on the bar its when output turns from zero to nonzero, so a gate that stays at 1 fires once (Alerts).

    Walk these top to bottom. The fix for each is one line, and the probe from step 1 usually tells you which one you are looking at.

See also