Language tour

Execution model

The script body runs once per bar, oldest → newest, over the pane’s real candles. A bare series name (close) is the current bar’s value; outputs capture each bar’s contribution and the chart assembles the full series afterwards. There is no look-ahead anywhere — every built-in is causal.

Syntax

  • pulse 1 — optional version header on the first line (unknown versions fail loudly).
  • # starts a line comment. Newlines separate statements; expressions inside (…)/[…] wrap freely.
  • Blocks use { }, or the low-indent colon form — when cond: mark buy — one statement on the same line.
  • Strings use double quotes; meta(name: "My Study", overlay: true) declares the script.
  • Named call arguments use name:draw line(fast, color: "#38bdf8", title: "Fast").

Series & history

expr[n] is the history operator: the value of any expression n bars back. Out-of-range history is none, which propagates through arithmetic and compares false — use nz(x, fallback) and na(x) to handle warmup.

PulseScript
pulse 1
# expr[n] reads a value n bars back. Out-of-range history is none.
gap = close - close[1]
threeBarMean = (close + close[1] + close[2]) / 3
draw line(threeBarMean, title: "3-bar mean")
when gap > 0 and gap[1] > 0: mark note at high "2 up bars"

Declarations & state

Bare assignment declares a per-bar series. let makes it reassignment-protected. persist is the state primitive: initialised once, carrying its value across bars — counters, trailing stops, regime flags.

PulseScript
pulse 1
# Bare assignment declares (recomputed every bar):
spread = high - low

# let = same, but protected from reassignment:
let basis = hlc3

# persist = initialised ONCE, carries across bars (state):
persist upBars = 0
when close > open: upBars = upBars + 1
draw line(upBars, title: "cumulative up bars")

Control flow

if / else if / else branches; when is the event-style if (no else); for i = a to b, for v in list, and while loop with hard runaway guards; ? : is the ternary. Only real booleans drive conditions — 1 is not true.

PulseScript
pulse 1
mut zone = 0
r = rsi(close, 14)

# Colon form: one statement, no braces.
when r > 70: zone = 1
when r < 30: zone = -1

# Brace form: multiple statements.
if zone == 1 {
  paint bg(rgba(239, 68, 68, 0.08))
} else if zone == -1 {
  paint bg(rgba(34, 197, 94, 0.08))
}
draw hist(r - 50, title: "RSI distance from 50")

Functions

PulseScript
pulse 1
# Expression body:
fn mid(a, b) = (a + b) / 2

# Block body — the last expression is the return value:
fn pctOf(part, whole) {
  ratio = part / whole
  ratio * 100
}

# Compute studies at the top level (they cache once per run), then pass values in.
band = ta.stdev(close, 20) * 2
draw line(mid(high, low), title: "midpoint")
draw line(pctOf(band, close), title: "stretch %")

Built-in series & context

Price series: open high low close volume hl2 hlc3 ohlc4 hlcc4. Bar context: time barIndex barCount lastBarIndex isFirstBar isLastBar, plus UTC clock fields year month day weekday hour minute second. The TA library (sma ema rsi atr vwap … and 60+ ta.* functions, many returning multi-field records like ta.bands(20, 2).upper) reuses the exact implementations the chart indicators use. The full per-function reference with runnable examples ships with the next docs update.

Outputs

draw line/area/steps/dots/hist/band/level/marker plot on the chart; mark buy/sell/note drop signal markers (they are also what the backtester trades and the scanner matches); paint bg(…) and paint candles(…) tint bars; alert("…") raises alert events.

PulseScript
pulse 1
meta(name: "ATR trail flip", overlay: true)

trail = ta.supertrend(10, 3)
draw line(trail.line, color: "#a78bfa", title: "trail")
when trail.dir > trail.dir[1]: mark buy at low "flip up"
when trail.dir < trail.dir[1]: mark sell at high "flip down"

Multi-timeframe

onTf(tf, expr) evaluates an expression on a higher timeframe and maps back only completed HTF bars — the strict no-repaint choice: a 4h value never changes mid-bucket.

PulseScript
pulse 1
meta(name: "HTF trend gate", overlay: true)

# onTf reads a COMPLETED higher-timeframe series — no repaint, ever.
htfTrend = onTf("4h", ema(close, 20))
draw line(htfTrend, color: "#a78bfa", title: "4h EMA 20")

goLong = close > htfTrend and crossOver(ema(close, 9), ema(close, 21))
when goLong: mark buy at low "with 4h trend"

Safety & limits

  • Wall-clock budget (2s on the chart) and loop caps abort runaway scripts with a line-numbered error.
  • No host access: no network, no page, no globals — unknown identifiers fail loudly.
  • Assigning to a built-in name (close = 5) errors with guidance instead of silently shadowing.
  • No randomness, no wall clock: runs are reproducible by construction.