na, errors and arithmetic
What na is and how to test for it, what division by zero gives, the difference between % and rem, how rounding works, and why 0.1 plus 0.2 is not 0.3.
Arithmetic in a script follows a few plain rules. Knowing them saves you from the two commonest surprises: a comparison with na that is never true, and a remainder with the wrong sign.
na#
na means "no value here". It is what you get from an average before it has enough bars, from close[5] on the first five bars, from 0 / 0, and from the square root of a negative number. It is not an error and the script keeps running.
| Expression | Result |
|---|---|
na + 1 | na |
0 / 0 | na |
math.sqrt(-1) | na |
na > 1 and na < 1 | false |
na == na | false |
Arithmetic with na gives na; a comparison with na is false, always, in both directions. That is why x == na can never find a missing value:
indicator "Comparing with na"
edition = 1
let average = tech.sma(close, 20)
plot(if average == na then 0 else average)Test with is_na(x). Replace a missing value with ??, which takes the right side when the left is na: x ?? 0 gives zero where x is missing.
indicator "Missing values"
edition = 1
let average = tech.sma(close, 20)
plot(if is_na(average) then 0 else average, name = "Tested with is_na")
plot(average ?? close, name = "Coalesced")a ?? b ?? c takes the first value that is not na. The operator binds tighter than a comparison and looser than +, so x ?? 0 > 5 means (x ?? 0) > 5.
A plot leaves a gap where its value is na. and and or treat na as false: true and na is false and na or true is true.
Division#
/ always divides as decimals: 7 / 2 is 3.5. Dividing a number that is not zero by zero gives infinity, not na: 1 / 0 > 5 is true, and is_na(1 / 0) is false. Only 0 / 0 is na. If the divisor can be zero, test it first:
indicator "Safe ratio"
edition = 1
let range = high - low
let body_share = if range == 0 then na else math.abs(close - open) / range
plot(body_share, name = "Body as a share of the range")To get a whole number from a division, round the result: math.floor(7 / 2) is 3, math.ceil(7 / 2) is 4, and int(7.9) cuts off the decimals and gives 7 (int(-7.9) gives -7).
Remainder: % and rem#
% is the remainder of a division and takes the sign of the divisor, so it is never negative when the divisor is positive. That makes it right for wrapping a number into a range. rem(a, b) is the other remainder, which takes the sign of the dividend.
indicator "Two remainders"
edition = 1
let wrapped = (bar.index - 3) % 7
let cut = rem(bar.index - 3, 7)
plot(wrapped, name = "%: always 0 to 6")
plot(cut, name = "rem: negative on the first bars")| Expression | Result |
|---|---|
-7 % 3 | 2 |
7 % -3 | -2 |
rem(-7, 3) | -1 |
rem(7, -3) | 1 |
5.5 % 2 | 1.5 |
Rounding#
math.round(x) rounds to the nearest whole number and rounds a half away from zero: 2.5 becomes 3 and -2.5 becomes -3. math.round(x, n) keeps n decimals. math.floor goes down and math.ceil goes up, whatever the sign: math.floor(-1.5) is -2.
indicator "Rounding"
edition = 1
let step = 0.25
plot(math.round(close / step) * step, name = "Close on a quarter step")
plot(math.floor(close), name = "Floor")
plot(math.ceil(close), name = "Ceil")Decimals are not exact#
Numbers are binary fractions, so some decimals cannot be stored exactly: 0.1 + 0.2 == 0.3 is false. Compare prices with a small tolerance, or round both sides first:
indicator "Comparing decimals"
edition = 1
let a = 0.1 + 0.2
let same = math.abs(a - 0.3) < 1e-9
plot(if same then 1 else 0, name = "Equal within a tolerance")Random numbers#
math.random(min, max) gives a number from min up to, but not including, max (0 and 1 when you leave them out). The numbers are not truly random: every run of the same script gives the same numbers, on every machine, so two runs of one script agree and a result can be repeated.
- The numbers come from a run seed that the engine makes from the identity of the script (the id of the saved script; for a script that is not saved, the name in its header), the values of its inputs, an optional seed you set, and the identity of the data. Each place in the script that calls
math.randomhas numbers of its own, and a bar gets the same numbers whatever amount of history is loaded. - Rewriting or reformatting the script, or adding a comment, keeps its numbers; changing an input, or the name of an unsaved script, gives new ones. That is the way to try a new draw.
- A call with a
seedof its own,math.random(0, 1, 42), gives numbers that depend on that seed alone, so they survive edits of the script. - When a call has no seed of its own, the engine shows the run seed in a note,
W-ENGINE-RANDOM-SEED, so a result can be named and repeated.
indicator "Fixed noise"
edition = 1
plot(math.random(-1, 1, seed = 42), name = "Same numbers on every run")Without a seed the script runs, and the note is added:
indicator "Noise"
edition = 1
plot(math.random(-1, 1), name = "Noise")The same numbers on every device#
A script gives the same numbers on every device the platform supports. Addition, subtraction, multiplication, division and the square root are exact everywhere. The other functions of mathematics (math.log, math.log10, math.exp, math.pow, math.sin, math.cos, math.tan, math.asin, math.acos, math.atan) are where browsers and servers used to differ in the last digit, so the platform calculates them itself, with one fixed method that it loads as a part of the engine when a script needs it. The size of one tick of an instrument (symbol.tick) is likewise one fixed decimal value, such as 0.00001, whatever the device.
What the platform states about them: one fixed implementation gives the same result on every supported device; the largest error measured on its current test set is at most 0.85 ulp; and correct rounding is not promised. An ulp is one unit of the last digit a number can hold. So an answer may be a little less than one unit away from the exact mathematical answer, and it is away by exactly the same amount everywhere; the figure is what was measured on the platform's test set, not a promise about every possible input.
One caution. A price bucket made with plain division, such as math.floor(price / symbol.tick), can still put a price that lies exactly on the boundary of a bucket into the neighbouring one, because a price such as 1.1 and a tick such as 0.00001 cannot be held exactly in binary. The answer is the same on every device, but it is not always the decimal answer you would write by hand. Leave a little room at the boundary, or round the price first.
Mistakes the engine stops#
Some arithmetic is a mistake in the program rather than in the data. A loop with a step of zero (E-LOOP-STEP), a size of na on an order (E-QTY-NA) and a loop that runs out of budget (E-LIMIT-OPS) stop the script with a message that names the line. A name you did not declare (E3171) is refused before the script runs. Everything about missing values is na, not an error.
See also: Values and types, Choices and loops, Common mistakes.