Missing values (NaN)

View as MarkdownOpen the editor

NaN is a wrun indicator's "no value here": a TA class that has not warmed up yet, a sparse source with no observation on this bar, a ratio with a zero denominator. This page is how to test for it, fill it, hold the last good value across it, and what it does in arithmetic and on the chart.

Why NaN matters

The important rule is that NaN is contagious in arithmetic: any expression touching a NaN becomes NaN. Add 5 to a not-yet-warm SMA and you get NaN, and a NaN written to an output draws as a gap, not a zero. So the skill is detecting NaN and deciding what to do about it before it poisons a calculation or blanks a line.

The helpers, and what they are in an indicator:

HelperUse it to
isNaN(x)branch on whether a value is missing
isFinite(x) (!isNaN(x) when infinities are impossible)the inverse: is this a usable number
nz: isNaN(x) ? fallback : xreplace a missing value with a fallback
fixnan: a module-level held updated only when the new value is finitehold the last good value forward over gaps
isNaN(x); never x == NaNNaN is not equal to anything, itself included

Comparisons with NaN are always false: NaN > 0.0, NaN < 0.0, and NaN == NaN all evaluate false. That is why a condition built over a warming input never fires by accident, and why isNaN is the only test that works.

Filling and forward-holding

nz is the everyday tool and it is one ternary: if x is NaN, take the fallback, otherwise take x. It is how you keep a plot continuous or keep arithmetic finite.

fixnan is the series-level cousin: wherever the series is NaN, substitute the most recent finite value, so a gappy series becomes a stepped, continuous one. In an indicator that is a module-level variable you overwrite only when the new value is finite.

Both, plus the two ways a row can be missing, in one file. The propagated output is deliberately NaN for the first 20 bars: the SMA has not warmed up, and adding 5 to NaN stays NaN. filled patches those bars with the close, and held carries the last finite liquidation reading across quiet bars (missing: "nan" keeps the source honest; the holding is the module's decision).

param("period", 20, { min: 1, max: 200, description: "SMA length" });
input("close", ohlcv.close);
input("liqs", liquidations.liquidations, { missing: "nan", description: "Liquidation volume, NaN on bars without any" });
output("propagated", line, overlay, { color: "#94a3b8", description: "SMA + 5: NaN until the window is warm, and NaN stays NaN" });
output("filled", line, overlay, { color: "rgb(37, 99, 235)", width: 2, description: "The same series with the close as a fallback while warming (nz)" });
output("held", line, lower, { color: "#7c3aed", description: "The last finite liquidation reading, held across quiet bars (fixnan)" });
const bandHi = output("band_hi", line, overlay, { color: "#2563eb66", description: "Filled series plus 1%" });
const bandLo = output("band_lo", line, overlay, { color: "#2563eb66", description: "Filled series minus 1%" });
// A box fill takes the opacity, so its color is hex, rgb(), or hsl(): a named color is refused here.
box("band", { top: bandHi, bottom: bandLo, color: "hsl(221, 83%, 53%)", opacity: 0.1, borderWidth: 0 });

let sma = new Sma(20);
let held: f64 = NaN; // the fixnan state: overwritten only by a finite value

function nz(x: f64, fallback: f64): f64 {
  return isNaN(x) ? fallback : x;
}

function onStart(): void {
  sma = new Sma(i32(p_period()));
}

function onBar(): void {
  const close = bar.close();
  const propagated = sma.update(close) + 5.0; // NaN + 5 is NaN: the contagion
  const liqs = in_liqs();
  if (isFinite(liqs)) held = liqs; // fixnan: hold the last good value forward
  const filled = nz(propagated, close);
  out_propagated(propagated);
  out_filled(filled);
  out_held(held);
  out_band_hi(filled * 1.01);
  out_band_lo(filled * 0.99);
}
BTCUSDT perpetual on Binance, 1 hour bars, Aug 10 to Aug 18, 2026Real output from OpenMarket's engine

Walking the key lines:

  • propagated = sma.update(close) + 5.0 is NaN for the first 20 bars. Written to its output, those bars are a gap on the grey line, while the other outputs still draw.
  • nz(propagated, close) patches the early bars with the close, so the blue line is continuous from bar 0 instead of starting blank at bar 20.
  • if (isFinite(liqs)) held = liqs; is fixnan: a bar with no liquidations reads NaN under missing: "nan", and the held value steps only when a real reading arrives. Before the first reading held is NaN, an honest gap rather than an invented zero.

What you'll see: a grey line that starts at bar 20 and a blue one that starts at bar 0, a faint band around the blue one, and a stepped purple line in the lower pane.

Abstaining is the other option

Writing NaN to one output leaves the other outputs drawing. Returning early from onBar() before any write leaves the whole bar blank: nothing is drawn and no output has a value there. Use NaN when one line warms slower than another; return early when nothing on the bar is meaningful yet (Execution model).

A color is a declaration, not a value, so NaN never reaches one directly. What a NaN index on a color_by output draws, and every other color rule, is on Colors.