Math, format & time helpers
Element-wise math, branching helpers (iff/mask/nz/fixnan/clamp/lerp/holdSign), string formatting (tostring/padLeft/padRight), and UTC time helpers (year/month/dayofweek/hour/...).
This page documents every helper in the math, branching, format, and time families. Every function listed here:
- Is broadcast-aware unless noted — pass a scalar OR a series, get the matching shape back.
- Is NaN-safe — NaN inputs propagate cleanly through arithmetic; comparison with NaN returns
false. - Is pure — no side effects, no hidden state across bars.
Element-wise math
The basic arithmetic operators (+, -, *, /, %) are already covered in the language reference. The functions below complement them.
abs(x)
Absolute value.
deviation = abs(close - sma(close, 50))
floor(x), ceil(x), round(x)
Standard rounding. round uses half-away-from-zero semantics (so round(0.5) == 1, round(-0.5) == -1).
ticks = floor(close * 100) // express price in ticks of 0.01
sign(x)
Returns +1 for positive, -1 for negative, 0 for zero, NaN for NaN.
direction = sign(change(close))
sqrt(x), pow(base, exp), exp(x), log(x), log10(x)
Power / exponential / logarithm. log is natural log; log10 is base-10. Negative inputs to log / sqrt produce NaN.
geomeanReturn = exp(sma(log(close / close[1]), 14))
sin, cos, tan, asin, acos, atan, atan2
Trigonometry. All angles are in radians. atan2(y, x) handles quadrant correctly.
min(a, b, …), max(a, b, …)
Element-wise N-ary min / max. Any argument may be a scalar or a series; the result is a series if at least one argument is.
upperBound = max(high, high[1], high[2]) // 3-bar high
For rolling min/max over a window, use highest / lowest — those are O(1) per bar, while a manual max(...) over many shifts is O(n).
Branching helpers
These are the per-bar equivalents of if / else blocks. They're how you build conditional plots, two-tone lines, and "draw only when …" markers.
iff(cond, a, b)
Vectorised if-then-else. cond is a per-bar boolean (or scalar); a and b are values selected per bar.
filled = iff(close >= ma, close, na) // close where above MA, NaN below
plotLine(filled, color="#0ECB81", width=2) // line breaks where condition is false
When all three arguments are scalars, returns a scalar. When any one is a series, returns a series. Strings, numbers, booleans all work for the branches.
Note on per-bar colour.
plotLine'scolor=argument is a single static string — passing a series of colours falls back to the default (the engine readscoloronce at draw-call time). To paint different segments in different colours, split the source into masked series and emit oneplotLineper colour. Seemaskbelow.
mask(series, cond)
Pass-through where cond is true; NaN otherwise. Pair with plotLine to render a series that breaks into segments:
bullSeg = mask(close, close > sma(close, 50))
bearSeg = mask(close, close < sma(close, 50))
plotLine(bullSeg, color="#0ECB81", width=2)
plotLine(bearSeg, color="#F6465D", width=2)
The renderer treats NaN as a gap — the line disappears where cond is false.
holdSign(up, down, init=0)
Sticky +1 / -1 trend tracker. Flips to +1 on every truthy up[i] and -1 on every truthy down[i]; carries the previous value forward in between. Pass init=1 to start the trend in the bull state, init=-1 for bear.
ma = sma(close, 20)
trend = holdSign(crossover(close, ma), crossunder(close, ma), 0)
bullMa = mask(ma, trend == 1)
bearMa = mask(ma, trend == -1)
plotLine(bullMa, color="#0ECB81", width=2)
plotLine(bearMa, color="#F6465D", width=2)
Without holdSign, this pattern would require a for loop walking every bar. The function is O(n) but in C-level vector form, not script-eval-time.
nz(value, fallback=0)
Replace NaN entries with fallback. Standard null-safety helper.
safePnl = nz(pnl, 0) // treat missing pnl as zero
nz accepts a scalar OR a series for both arguments.
fixnan(value)
Carry the last finite value forward across NaN gaps. Useful for sparse signals (pivots, divergences) where you want a continuous step line instead of a series of disconnected dots:
ph = pivothigh(high, 5, 5) // NaN on non-pivot bars
phLine = fixnan(ph) // step line that updates on each new pivot
plotLine(phLine, color="#F0B90B", width=1.5)
clamp(src, lo, hi)
Constrain src into [lo, hi] element-wise. Any of the three may be a series.
boundedRsi = clamp(rsi(close, 14), 5, 95) // hide noise at extremes
volScale = clamp(volume / sma(volume, 50), 0.5, 3)
NaN inputs propagate (NaN clamps to NaN). When lo > hi, the result is lo (the lower bound wins).
lerp(a, b, t)
Linear interpolation: a + (b - a) * t. Element-wise on series.
midband = lerp(bb_lower(close, 20, 2), bb_upper(close, 20, 2), 0.5)
t outside [0, 1] extrapolates — there is no implicit clamp. Combine with clamp(t, 0, 1) if you want bounded interpolation.
Format helpers
Building the dynamic strings that go inside plotLabel / labelNew / paneLabel chips.
tostring(value, fmt)
Format a scalar OR a series as a string. Returns the matching shape (scalar in → string out; series in → string-array out). Combine with + for concatenation:
plotLabel(time[0], close[0],
"RSI: " + tostring(rsi(close, 14)[0], "0.0"),
color="#F0B90B")
Format patterns
| Pattern | Effect | 12.345 becomes |
|---|---|---|
| omitted | Number.toString() (drops trailing zeros, scientific for large/small) | "12.345" |
"0" | Integer, half-away-from-zero | "12" |
"0.0" | Fixed-1 decimal | "12.3" |
"0.00" | Fixed-2 decimals (price ticks) | "12.35" |
"0.000000" | Fixed-N up to 8 decimals | "12.345000" |
"#.##" / "##.##" | Fixed-N decimals, trim trailing zeros | "12.35" |
"%" | Multiply by 100, append % (decimals: 0) | "1234%" |
"0.0%" / "0.00%" | Percent with N decimals | "1234.5%" |
Unknown patterns fall back to Number.toString() — typos never throw.
tostring(0.5, "%") // "50%"
tostring(0.1234, "0.00%") // "12.34%"
tostring(1.50, "##.##") // "1.5"
tostring(NaN, "0.00") // "NaN"
padLeft(s, width, fill=" "), padRight(s, width, fill=" ")
Pad a string to a target width. Useful for table-style HUDs inside labelNew / paneLabel:
hud = "RSI: " + padLeft(tostring(r[0], "0.0"), 5) + "\n"
+ "ATR: " + padLeft(tostring(atr(high,low,close,14)[0], "0.00"), 6)
labelNew("hud", time[0], high[0], hud, anchor="above", align="left")
If s is already at least width characters long, the input is returned unchanged. Only the first character of fill is used.
Time helpers
All seven helpers accept the time series OR a scalar epoch timestamp in either seconds or milliseconds — the implementation magnitude-detects (any value below 1e11 is treated as seconds and scaled to ms internally). Output is UTC so cross-exchange comparisons stay consistent.
The time series itself carries SECONDS (engine X-axis units — same shape lineNew / boxNew / labelNew consume). Don't manually convert time before calling these helpers; they handle the rescale for you. See pitfall §12 for details.
year(t)
UTC calendar year (4-digit). 2026 for an October 2026 bar.
month(t)
UTC month, 1..12 (NOT 0-indexed — matches Pine Script semantics). January = 1.
dayofmonth(t)
UTC day of month, 1..31.
dayofweek(t)
UTC day of week, 0..6 where 0=Sun, 1=Mon, …, 6=Sat. Same indexing as JavaScript's Date.prototype.getUTCDay.
// Weekday-only filter (Mon-Fri):
isWeekday = dayofweek(time) >= 1 and dayofweek(time) <= 5
plotBgColor(not isWeekday, color="rgba(255,255,255,0.05)")
hour(t), minute(t), second(t)
UTC hour 0..23, minute 0..59, second 0..59.
// Highlight London open (08:00 UTC):
isLondonOpen = hour(time) == 8 and minute(time) == 0
plotShape(isLondonOpen, low - atr(high,low,close,14) * 0.5,
shape="circle", color="#F0B90B", size=4)
weekofyear(t)
ISO 8601 week number, 1..53. Monday-start week; the week containing Thursday is week 1. Same convention as Excel, Postgres, Wikipedia.
NaN handling
Non-finite or non-positive timestamps return NaN (no crash). Filter with iff(year(time) > 0, …) if you need to gate on real timestamps.
Worked example: a multi-line HUD
Combining tostring, padLeft, time helpers, and a label slot:
@name "Trading HUD"
r = rsi(close, 14)
a = atr(high, low, close, 14)
v20 = sma(volume, 20)
hud = "RSI: " + padLeft(tostring(r[0], "0.0"), 5)
+ " ATR: " + padLeft(tostring(a[0], "0.00"), 7)
+ " Vol: " + padLeft(tostring(volume[0]/v20[0], "0.00"), 5) + "x"
+ "\n"
+ "Day: " + tostring(dayofmonth(time[0]))
+ " H: " + padLeft(tostring(hour(time[0]), "0"), 2) + ":"
+ padLeft(tostring(minute(time[0]), "0"), 2)
labelNew("hud", time[0], high[0],
hud, anchor="above", align="left",
color="#F0B90B", bg="rgba(0,0,0,0.6)")
The HUD updates on every redraw with the latest values, anchored to the right-most bar's high.
Next
- Series operations — lookback, cross detection, rolling stats.
- Indicators — moving averages, oscillators, volatility.
- Statistics —
stdev,correlation,linreg,polyreg2,zscore.