Debugging
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.
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
lowerline 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
noneoutput 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 inonBar(), prints each bar's line in the editor's Console.
wrunparam("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(). Declarestring("debug", { max_bytes }), build a line with the allocation-freesb_*builder, and send it withstr_debug_sb()(or send a finished string withstr_debug(text)). Write it only on the bars worth reading: a bar that writes nothing prints nothing.wrunparam("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:
text2026-09-30T14:00:00.000Z spike ratio=3.41 volume=1824 2026-09-30T19:00:00.000Z spike ratio=4.07 volume=2210Every 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+ortoString(): 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.labelprint it on the newest bar. It switches the file to the second runtime contract, which Run derives for you:wrunoutput("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.
- A
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:
Stage What it checked Typical message declarationsthe param/input/output/ shape statements, read from the textoption 'top' takes an output handle, not a string literallintraw positional slot literals in your source Raw slot literal 0 passed to getFloat(): raw positional slots rebind silently when the sheet changesmetadatathe sheet derived from your declarations, against the schema Output 'price' (color_by): color_by needs 'colors' with at least 2 entriescompilethe AssemblyScript compiler, with a hint Conversion from type 'f64' to 'i32' requires an explicit cast.validatethe built module's exports and the sandbox console.* is not available in an Indicator (it runs in a sandbox with no console).computethe run itself, after Run AssemblyScript 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
NaNon every bar (onBar()returned before writing it, or wroteNaN), an output declarednone, 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.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 isNaN, andNaNdraws nothing), or switch their plot tonone, 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.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:
textrange_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 declarationsA 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.
/helplists the grammar, and/clearempties the Console.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
NaNuntil it has enough bars, and aNaNoutput 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 inonBar(), 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 oneNaNinput, or a division by zero. Probe each input as anoneoutput and find the firstNaN. - Wrong pane. A percent or a count drawn
overlayhugs the price axis floor. Declare itlower. - A mark on every bar. A
shapeoutput withoutshape_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 declarednone, or the chart's market does not serve the source (the chart names it, for examplecelled source class 'tape' is not served by the browser lane yet, orVolume profile data is unavailable for ...). - A pinned input reads
NaNeverywhere. An index pin (POLYGON_INDICES) readsNaN, and the legend says why; any pin readsNaNuntil 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 itswhenoutput turns from zero to nonzero, so a gate that stays at1fires 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.
- Line starts blank, then appears. Warm-up. Anything with a period is