◆ New — Strategy Validator, Code Fixer & Prop Compliance Checker are live. Try one free →
Home / Learn / Handling na Values in Pine Script: nz(), na(), fixnan() and Silent Bugs
Pine Script

Handling na Values in Pine Script: nz(), na(), fixnan() and Silent Bugs

How na actually behaves in Pine Script v6 and how to handle it correctly with na(), nz(), and fixnan(). The core hazard: na propagates through arithmetic and comparisons, so a condition touching na is neither true nor false — it silently never fires, which is the mechanism behind a large share of 'my strategy never enters' bugs. Covers where na comes from, when nz() is the wrong fix, and a wrong-then-fixed worked example. Educational material only, not financial advice; correct na handling makes code honest, not profitable.

How do I handle na values in Pine Script?

Pine Script gives you three dedicated tools. `na(x)` tests whether a value is na and returns a plain true/false you can safely use in conditions. `nz(x)` replaces na with zero — or with any fallback you pass as the second argument, nz(x, fallback). `fixnan(x)` replaces na with the last non-na value of the same series, carrying it forward. The rule of thumb: use na() to guard logic, nz() when a neutral substitute value is genuinely correct, and fixnan() when "the most recent real value" is the honest stand-in.

What you must not do is compare against na directly. x == na does not work the way it reads: comparing anything with na yields na, not true, so the expression never confirms what you meant it to. The only correct membership test is na(x).

Why this deserves a whole article: na is not an error value in Pine — it is a silent absence that flows through your calculations without raising anything. na + 1 is na. na > 0 is na. A crossover fed one na input returns na. And when a condition ultimately evaluates to na, Pine treats it as not-true: the if branch does not run, the plotshape does not draw, the strategy.entry never fires. Nothing crashes, nothing warns. The script simply does less than you wrote — often only on some bars, which makes the symptom look like a logic bug or a data problem rather than what it is.

The sections below map where na enters a script, how it propagates, what each of the three functions actually promises, and the classic silent-bug pattern with its fix.

Why is na > 0 neither true nor false?

Pine's na is closest in spirit to SQL's NULL or floating-point NaN: it means no value here, and any operation that consumes it produces it. This is called na propagation, and it applies to arithmetic (na * 2 → na), to built-ins fed na inputs, and — critically — to comparisons. na > 0, na < 0, and even na == na all evaluate to na, because a comparison against an unknown cannot honestly answer yes or no.

The subtle part is what happens when that na reaches a decision point. Pine does not throw a runtime error when an if condition or ternary condition is na; it simply does not take the true-branch. Operationally, na behaves like false at every branch — without being false. na > 0 is not-true, and na <= 0 is also not-true. Write an if/else against a possibly-na value and it is entirely possible for neither the condition nor its apparent negation to hold, so an else branch you believed was exhaustive quietly becomes reachable in a third, unconsidered state.

This is the exact mechanism behind the most common forum complaint pattern: "my strategy compiles, plots fine, but never enters." The entry condition includes one term that is na — on all bars, or just on the bars that mattered — and the whole conjunction evaluates to na. A and B and C with a single na member is not-true regardless of A and C. No error, no log line, no visual hint unless you plot the offending series and notice the gap.

The defensive habit that follows: any series that can be na — and the next section shows how many can — must be either guarded with na() or deliberately substituted with nz()/fixnan() before it participates in a condition. Handling na at the decision point, explicitly, is the difference between logic that does what it says and logic that does what it says only when the data cooperates.

Where does na actually come from?

Four sources account for nearly all na in real scripts.

*1. Warm-up periods of ta. functions.** ta.sma(close, 200) is na for the first 199 bars because the window is not yet full. The same applies to ta.rsi, ta.atr, ta.ema, and most rolling calculations. Any condition comparing price to a 200-bar average is therefore na — and silently not-true — for the first 199 bars of every backtest, and for the whole chart if the symbol has fewer bars than the window.

2. History references past the start of data. close[500] is na on any bar with fewer than 500 predecessors. Offsets buried inside expressions inherit this: ta.change(close, 100) needs 100 bars of history too.

3. `var` declarations initialised to na. The idiom var float entryLevel = na is standard for "no level set yet" — but every comparison against that variable is na until the first assignment. This is by far the most common source of the never-enters bug, because the variable is often only assigned inside a condition that itself never fired.

4. `request.security()` and sparse data. A higher-timeframe request returns na while the outer symbol has bars but the requested context does not line up — different sessions, a symbol that started trading later, or merge gaps with barmerge.gaps_on. Illiquid symbols can also simply have holes.

The practical audit technique is unglamorous but decisive: plot(mySeries) in a separate pane, or plotchar(na(mySeries), "na?", "•", location.top), and look at where the gaps are. na bugs survive because the absence is invisible in the finished chart; making it visible for one debugging session usually locates the leak in minutes. ForexCodes' Pine validator flags several of these patterns statically — unguarded var-na comparisons especially — but a plotted series remains the ground truth.

nz() vs fixnan(): they answer different questions

Both functions replace na, so they are often treated as interchangeable. They are not — they encode different claims about what the missing value should have been, and choosing the wrong one produces plausible-looking wrong numbers, which is worse than a visible gap.

`nz(x)` says: "when unknown, use zero" (or the fallback you pass: nz(x, fallback)). This is correct when zero is genuinely neutral — accumulators, counts, volume-like quantities, or a recursive series' previous value on the first bar, as in the classic cum = nz(cum[1]) + volume pattern. It is wrong for prices and price-derived levels. nz(ta.sma(close, 200)) yields 0 during warm-up, and close > nz(ta.sma(close, 200)) is then true on every warm-up bar for any instrument trading above zero — you have converted a silently-false condition into a loudly, wrongly true one. Your backtest now contains a burst of early trades that exist only because a moving average was pretending to be zero.

`fixnan(x)` says: "when unknown, the last real value still stands." It carries the most recent non-na value forward, which is the honest choice for levels that persist between updates — a pivot from ta.pivothigh() (na on every bar except the detection bar), a session open captured once per day, a value returned sparsely by request.security() with gaps. fixnan(ta.pivothigh(10, 10)) turns an event series into a steppy line you can compare against on every bar.

A useful decision test: if this value is missing, is the truthful stand-in "nothing" or "the same as before"? Nothing → nz() with a defensible fallback. Same as before → fixnan(). Neither → do not substitute at all; guard with na() and let the condition be legitimately inactive until real data exists. Substituting a value you cannot defend is not na handling — it is na laundering.

The 'my strategy never enters' bug — wrong, then fixed

Here is the pattern in its natural habitat. A breakout strategy stores a level in a var and enters when price exceeds it:

// ❌ WRONG — breakoutLevel starts as na, so `close > breakoutLevel`
// evaluates to na (not true, not false) and the entry NEVER fires
// until the level is first assigned — and if the assignment condition
// is also broken, it never fires at all. No error is raised.
var float breakoutLevel = na
if ta.pivothigh(high, 10, 10) > 0   // na > 0 is na on most bars too
    breakoutLevel := high[10]
enterLong = close > breakoutLevel

Two na leaks, both silent. ta.pivothigh() returns na on every bar where no pivot is confirmed, so ta.pivothigh(...) > 0 is na — not-true — on those bars (that one happens to still work on detection bars, but it is an unguarded na comparison a validator should and will flag). And enterLong is na until the first assignment succeeds. The fixed version makes every na boundary explicit:

//@version=6
// Educational only — validate before trading; not financial advice.
strategy("Breakout (na-safe)", overlay = true)

stopPts = input.int(300, "Stop distance (points)", minval = 1)

pivot = ta.pivothigh(high, 10, 10)
var float breakoutLevel = na
if not na(pivot)
    breakoutLevel := pivot

plot(breakoutLevel, "Breakout level", color = color.orange, style = plot.style_linebr)

enterLong = not na(breakoutLevel) and close > breakoutLevel

if enterLong and barstate.isconfirmed and strategy.position_size == 0
    strategy.entry("Long", strategy.long)

if strategy.position_size > 0
    strategy.exit("Exit", "Long",
         stop = strategy.position_avg_price - stopPts * syminfo.mintick)

Every decision now touches only guarded values: not na(pivot) gates the assignment, not na(breakoutLevel) gates the entry, the signal acts on confirmed bars only, and the declared stop input is wired into strategy.exit(). The strategy may still be a bad idea — na-safety says nothing about edge — but it now does exactly what it reads as doing, on every bar, which is the property everything else gets validated on top of. Educational only; no claim of profitability is made or implied, and trading carries a real risk of loss.

Key takeaways

Educational only — not financial advice. Trading involves substantial risk of loss.

Catch the bug that compiles.Run auditGet Pro