---
title: "Debugging"
description: "A practical workflow for finding out why an Indicator is wrong: emit what you cannot see, read the Problems lane, isolate one output, read the numbers on your…"
order: 75
section: "faq"
---

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

# Debugging

A practical workflow for finding out why an Indicator is wrong: emit what you cannot see, read the Problems lane, isolate one output, read the numbers on your machine, compare against the kScript (legacy) original on one market, 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 `print()` and no console, but there is a fast, reliable workflow, and it starts from the one fact that makes Indicators easy to debug: every value the module computes can be an output, and every output is readable as numbers.

## 1. Emit what you cannot see

The fastest debugger is an output. You cannot step through the bar loop, but you can declare any intermediate value as an output and look at it on every bar. Two 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, readable in the legend and as a metric, never drawn. It keeps the picture clean while you check a number.

```typescript
import { input, line, lower, none, ohlcv, output, param } from "./sdk/declare";
import { in_close, in_high, in_low } from "./gen/inputs";
import { emitRow, out_range_pct, out_raw_spread, out_smooth } from "./gen/outputs";
import { p_len } from "./gen/params";
import { Ema } from "./sdk/ta";

param("len", 14, { min: 2, max: 200 });
input("close", ohlcv.close);
input("high", ohlcv.high);
input("low", ohlcv.low);
// 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 and readable in the legend and as a metric, never drawn. Delete it when done.
output("raw_spread", none, lower, { description: "debug: high minus low before the percent" });

let ema = new Ema(14);
let spread: f64 = NaN;
let rangePct: f64 = NaN;
let smooth: f64 = NaN;

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

export function state(): i32 {
  const close = in_close();
  spread = in_high() - in_low();
  rangePct = close > 0.0 ? (spread / close) * 100.0 : NaN;
  smooth = isNaN(rangePct) ? NaN : ema.update(rangePct);
  return isNaN(rangePct) ? 0 : 1;
}

export function finalize(): void {
  out_range_pct(rangePct);
  out_smooth(smooth);
  out_raw_spread(spread);
  emitRow();
}

export function reset(): void {
  ema.reset();
  spread = NaN;
  rangePct = NaN;
  smooth = NaN;
}
```

When you need the exact number on the chart rather than the shape, a string slot and a `render.label` print it on the newest bar, the way a kScript `plotText` under `isLastBar` did. It switches the file to the second runtime contract, which the build does for you:

```typescript
import { input, line, lower, none, ohlcv, output, render, string, time } from "./sdk/declare";
import { in_bar_t, in_close, in_high, in_low } from "./gen/inputs";
import { emitRow, out_range_pct, out_tag_x } from "./gen/outputs";
import { sb_clear, sb_f64, sb_text, str_readout_sb } from "./gen/strings";

input("close", ohlcv.close);
input("high", ohlcv.high);
input("low", ohlcv.low);
input("bar_t", time.bar_open_sec);
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 });

let rangePct: f64 = NaN;
let barTime: f64 = NaN;

export function init(): void {}

export function state(): i32 {
  const close = in_close();
  barTime = in_bar_t();
  rangePct = close > 0.0 ? ((in_high() - in_low()) / close) * 100.0 : NaN;
  return isNaN(rangePct) ? 0 : 1;
}

export function finalize(): void {
  out_range_pct(rangePct);
  out_tag_x(barTime);
  sb_clear();
  sb_text("range% = ");
  sb_f64(rangePct, 2);
  str_readout_sb();
  emitRow();
}

export function reset(): void {
  rangePct = NaN;
  barTime = NaN;
}
```

This works for any expression. Lift the part you doubt into a module-level variable, declare an output for it, and read the answer off the chart instead of guessing. Delete the probe when you are done; every output is a metric, and a debug metric in a published package is noise.

## 2. Read the Problems lane

If Run stops before the chart changes, the message is in the Problems lane, tagged with the stage that produced it, and one problem hides the ones behind it:

| Stage | What it checked | Typical message |
| --- | --- | --- |
| `declarations` | the `param` / `input` / `output` / shape statements, read from the text | `option 'top' takes an output handle, not a string literal` |
| `lint` | raw positional slot literals in your source | `raw positional slot literals silently rebind` |
| `metadata` | the derived sheet against the schema | `outputs.0.color_by: color_by needs 'colors' with at least 2 entries` |
| `compile` | the AssemblyScript compiler | `ERROR AS200: Conversion from type 'f64' to 'i32' requires an explicit cast.` |
| `validate` | the built module against the four-export contract | `WRUN export 'finalize' has signature () -> f64` |

Fix the first one, Run again. On your machine `om wrun build` prints the same messages and exits non-zero; every one of them is listed with its fix in [Common errors](common-errors.md).

A run that reaches the chart and draws nothing is usually not an error at all. It is `state()` returning `0` on every bar, an output declared `none`, or a source the chart's market does not serve (the chart says which, by name). Step 5 covers those.

## 3. Isolate

When several outputs are wrong at once, stop reasoning about all of them. Reduce the module to one output: write `NaN` to the others in `finalize()` (a `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 it as numbers on your machine

The chart shows shapes; the daemon shows numbers. Install the draft and read the output bar by bar, oldest first, newest last (the still-forming bar):

```bash
om wrun install ./debug-probe --replace
om metric series --metric wrun/@you/debug-probe/range_pct --symbol BTCUSDT --exchange BINANCE_FUTURES --interval 1h --bars 60
om metric series --metric wrun/@you/debug-probe/raw_spread --symbol BTCUSDT --exchange BINANCE_FUTURES --interval 1h --bars 60 --format json
```

A `none` output is a metric like any other, so a probe is readable here without ever drawing. Text mode renders a sparkline; `--format json` gives the `[barOpenSec, value]` pairs to diff against a spreadsheet or the original script's export.

## 5. Compare against the kScript original on one market

When the Indicator is a port, the original is the oracle. Open both on the same market and interval and read them side by side: on the chart, add the kScript (legacy) row with **Use kScript engine** beside your draft; from the terminal, `om open` launches a published kScript on a chart in your browser. Compare from the first bar where both are warm, not from bar zero: the shipped TA classes warm up differently from the kScript builtins (`Rsi` is Wilder-smoothed and `NaN` until its window fills; `Ema` seeds from a simple average), so the first `period` bars differ by design and everything after them should not.

```bash
om open @scope/their-indicator
```

If the two agree on closed bars and disagree only on the newest one, the difference is the forming-bar replay, and the fix is almost always a field missing from `reset()`.

## 6. 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 `state()` that returns `0` abstains those rows. 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 that is assigned in `init()` and never updated in `state()`, or an accumulator that `reset()` restores on every replay of the forming bar. Check that the value is written every bar and that `reset()` restores the starting value, not the current one.
- **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.
- **Stale values after a tick.** A field `reset()` forgot. Reassign every module-level variable and `.reset()` every TA object.
- **Nothing at all, no error.** `state()` never returns `1` (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: `trades volume needs side 'BUY' or 'SELL'`, `source class 'odds' is not served by the browser lane yet`).
- **Different on the chart and on your machine.** A pin. The chart reads its own market and interval for every input; your machine honors `symbol`, `exchange`, and `interval` pins. A higher timeframe on the chart is a bucket fold inside the module ([Multi-timeframe](../core-concepts/multi-timeframe.md)).
- **A watch never fires.** The condition names the wrong output, the wrong venue spelling, or an edge operator without an `interval` in the selector. `om watch stats <id>` reads `condition_text` and `last_error` back; `om watch repair <id>` names the repair.

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

- [Common errors](common-errors.md) for every build and runtime message with its fix.
- [Execution model](../core-concepts/execution-model.md) for warm-up, the forming-bar replay, and where an Indicator runs.
- [Repainting](../core-concepts/repainting.md) for why a stale field in `reset()` repaints and a bucket fold does not.
