Docs·Delta DSL Script·Concepts

Alerts

Wire your script conditions into push / Telegram / in-app notifications via alertcondition. Backend evaluation, frequency modes, message placeholders, and the FE preview no-op.

alertcondition lifts a boolean expression into the alert builder. The user wires it to push, Telegram, or in-app notifications from the per-indicator alerts dialog the same way they wire alerts on any built-in indicator (MACD signal cross, Bollinger break, Supertrend flip, etc.). Alert evaluation happens on the backend, not in the chart frontend — so the alert fires whether or not the user has the chart open.

How alerts flow

  1. The script is saved (or auto-compiled) in the editor.
  2. The BE compiler scans for every alertcondition(...) call and writes an entry to the script's alert_manifest.
  3. The user opens the alert builder (per-indicator alerts dialog, or /trading/alerts). The manifest's entries appear as wire-up options.
  4. The user picks channels (push / Telegram / in-app), sets frequency, hits enable.
  5. The BE indicator-engine evaluates the condition on every bar close (or every tick, depending on frequency). Hits stream to the user's chosen channels.

The frontend's job is purely visual — it shows you the conditions that exist and lets the user wire them. It does NOT fire alerts itself.

Frontend preview. The FE interpreter exposes alertcondition (and alert) as no-op functions so the same script source compiles in both the BE alert evaluator and the FE chart preview. You can call them anywhere; in preview they have zero effect.

alertcondition

Delta DSL
alertcondition(condition,
               title="…",
               message="…",
               frequency="once_per_close",
               name="…")
ArgumentTypeDefaultDescription
conditionseries<boolean> (positional, required)Per-bar truth value. The alert fires when this is true for the most recent (or just-closed) bar.
titlestring<script @name>Alert title shown in the wire-up UI. Pick something short and descriptive — this is what the user sees in the alert builder.
messagestring""Notification body. Supports {{…}} placeholders (see below).
frequencystring"once_per_close"When the BE evaluates and (if true) fires. See below.
namestringslug(<title>)Stable identifier for the entry in the manifest. Derivation order: explicit name=slug(title)alert_<index>. Must be unique within the script. See Naming & duplicates for why dynamic titles often need an explicit name=.

Message placeholders

The BE substitutes the following placeholders in the message string at fire time:

PlaceholderSubstituted with
{{symbol}}The chart's symbol (e.g. BTCUSDT)
{{exchange}}The exchange id (e.g. binance, bybit)
{{interval}} / {{timeframe}}The chart's timeframe (e.g. 1h)
{{close}} / {{price}}The closing price of the firing bar
{{open}} / {{high}} / {{low}}OHLC of the firing bar
{{volume}}Bar volume
{{time}}Bar timestamp (ISO 8601 UTC)
Delta DSL
alertcondition(crossover(close, sma(close, 50)),
               title="Cross above MA50",
               message="{{symbol}} crossed above MA50 at {{price}} ({{time}})")

Frequency modes

FrequencyFires when
"once_per_close" (default)The condition is true on a CLOSED bar. Default — typically what users want for "trend flipped" / "signal crossed back" / "breakout confirmed" alerts.
"once_per_bar"The condition becomes true for the first time on the current (still-forming) bar — fires once per bar even if the condition flickers in and out as ticks arrive.
"every"Every tick where the condition is true. Use sparingly — pair with a real cooldown elsewhere or you'll get a notification stream.
"once"Fires the FIRST time the condition becomes true after the alert is enabled, then disables itself.

"once_per_close" is the safest default for most strategies. Switch to "once_per_bar" only if your traders need intra-bar firing and you've thought about the duplicate-event story (e.g. a fast-moving cross can fire then un-fire then fire again before the bar closes — "once_per_close" waits for the truth and "once_per_bar" accepts the first hit).

alert

alert(message, frequency="once_per_close") is the imperative Pine-style sibling of alertcondition. The FE no-ops it the same way; the BE may treat it as a higher-frequency channel without a per-bar gate.

Delta DSL
if crossover(close, sma(close, 50))
  alert("MA50 cross on {{symbol}}", frequency="once_per_bar")
end

For most workflows, prefer alertcondition — it's declarative and the alert builder UI knows how to render it as a wire-up option.

Worked example: MACD zero-cross with trend filter

A complete, copy-pasteable alert script. Fires when MACD histogram crosses zero (signal/main line agreement) AND price is on the right side of a slow EMA (trend filter). Two alerts — one for the long flip, one for the short flip.

Delta DSL
@name "MACD Zero-Cross + Trend Filter"
@pane "overlay"

@input fastLen  = input.int(12, "MACD fast",   minval=2,  maxval=200)
@input slowLen  = input.int(26, "MACD slow",   minval=2,  maxval=400)
@input sigLen   = input.int(9,  "MACD signal", minval=1,  maxval=100)
@input trendLen = input.int(200, "Trend EMA",  minval=20, maxval=400)

macdLine   = ema(close, fastLen) - ema(close, slowLen)
signalLine = ema(macdLine, sigLen)
hist       = macdLine - signalLine

trend = ema(close, trendLen)

longFlip  = crossover(hist, 0)  and close > trend
shortFlip = crossunder(hist, 0) and close < trend

plotLine(trend, color="#F0B90B", width=1.5)
plotShape(longFlip,  low,  shape="arrowUp",   color="#0ECB81", size=10)
plotShape(shortFlip, high, shape="arrowDown", color="#F6465D", size=10)

alertcondition(longFlip,
               title="MACD long flip",
               message="LONG flip on {{symbol}} ({{interval}}) at {{price}} — above EMA",
               name="macd_long_flip",
               frequency="once_per_close")

alertcondition(shortFlip,
               title="MACD short flip",
               message="SHORT flip on {{symbol}} ({{interval}}) at {{price}} — below EMA",
               name="macd_short_flip",
               frequency="once_per_close")

Save the script. The BE compiler picks up both alertcondition calls, registers them in the manifest, and the alert builder shows "MACD long flip" and "MACD short flip" as two separate wire-up entries. The user wires the long flip to push, the short flip to Telegram, hits enable on both, walks away. On every bar close the BE evaluates both conditions; whichever fires sends to its wired channels with the rendered message.

Multiple conditions in one script

You can declare any number of alertcondition calls — each one becomes its own entry in the manifest:

Delta DSL
ma     = sma(close, 50)
upCross = crossover(close, ma)
dnCross = crossunder(close, ma)

plotLine(ma, color="#F0B90B", width=2)
plotShape(upCross, low,  shape="arrowUp",   color="#0ECB81", size=8)
plotShape(dnCross, high, shape="arrowDown", color="#F6465D", size=8)

alertcondition(upCross, title="MA50 cross up",   message="{{symbol}} crossed up MA50")
alertcondition(dnCross, title="MA50 cross down", message="{{symbol}} crossed down MA50")

Two alert entries appear; the user can wire them independently (different channels, different frequencies, different cooldowns).

Cookbook

Drop-in alert recipes you can paste into a script and adapt. None of these reference indicators we keep proprietary — every primitive comes from the stdlib.

Bollinger band breakout with volume confirmation

Fires when price closes outside the upper / lower band AND volume is at least 1.5× its 20-bar average — a classic breakout-versus-fake filter.

Delta DSL
@input bbLen  = input.int(20,  "BB length",  minval=5,  maxval=200)
@input bbMult = input.float(2.0, "BB mult",   minval=0.5, maxval=5.0)
@input volMul = input.float(1.5, "Vol multi", minval=1.0, maxval=10.0)

basis = sma(close, bbLen)
dev   = stdev(close, bbLen) * bbMult
upper = basis + dev
lower = basis - dev

avgVol  = sma(volume, bbLen)
bigVol  = volume > avgVol * volMul

upBreak = close > upper and bigVol
dnBreak = close < lower and bigVol

plotLine(upper, color="#F6465D", width=1)
plotLine(lower, color="#0ECB81", width=1)
plotLine(basis, color="#F0B90B", width=1)

alertcondition(upBreak,
               title="BB break up",
               message="{{symbol}} broke ABOVE upper band on {{interval}} @ {{price}} (vol confirmed)",
               name="bb_break_up")

alertcondition(dnBreak,
               title="BB break down",
               message="{{symbol}} broke BELOW lower band on {{interval}} @ {{price}} (vol confirmed)",
               name="bb_break_dn")

Supertrend flip

Trend-following classic — fires when the supertrend direction flips. Most traders pair this with a higher-timeframe filter; the once_per_close frequency keeps it from re-firing on intra-bar wiggles.

Delta DSL
@input atrLen = input.int(10,  "ATR length", minval=2,  maxval=100)
@input atrMul = input.float(3.0, "ATR mult",  minval=0.5, maxval=10.0)

atrV = atr(atrLen)
hl2  = (high + low) / 2

upBand   = hl2 - atrV * atrMul
dnBand   = hl2 + atrV * atrMul
trendUp  = close > nz(dnBand[1], dnBand)
trendDn  = close < nz(upBand[1], upBand)

direction = nan
if trendUp
  direction := 1
end
if trendDn
  direction := -1
end

flipUp   = direction == 1  and direction[1] == -1
flipDown = direction == -1 and direction[1] == 1

plotShape(flipUp,   low,  shape="arrowUp",   color="#0ECB81", size=10)
plotShape(flipDown, high, shape="arrowDown", color="#F6465D", size=10)

alertcondition(flipUp,
               title="Supertrend → long",
               message="Supertrend flipped LONG on {{symbol}} ({{interval}}) @ {{price}}",
               name="st_flip_long")

alertcondition(flipDown,
               title="Supertrend → short",
               message="Supertrend flipped SHORT on {{symbol}} ({{interval}}) @ {{price}}",
               name="st_flip_short")

Volume spike (statistical outlier)

Fires when the bar's volume is more than N standard deviations above the rolling mean. Useful as an "unusual activity" alert when paired with a price filter.

Delta DSL
@input look  = input.int(50,   "Lookback",     minval=10, maxval=500)
@input zMin  = input.float(3.0,"Z-score gate", minval=1.5, maxval=10.0)

mean = sma(volume, look)
sd   = stdev(volume, look)
z    = (volume - mean) / sd

bigUp = z > zMin and close > open
bigDn = z > zMin and close < open

plotShape(bigUp, low,  shape="circle", color="#0ECB81", size=8)
plotShape(bigDn, high, shape="circle", color="#F6465D", size=8)

alertcondition(bigUp,
               title="Bullish volume spike",
               message="Unusual BUY volume on {{symbol}} ({{interval}}) @ {{price}} — z>{{zMin}}",
               name="vol_spike_bull")

alertcondition(bigDn,
               title="Bearish volume spike",
               message="Unusual SELL volume on {{symbol}} ({{interval}}) @ {{price}} — z>{{zMin}}",
               name="vol_spike_bear")

Note. The {{zMin}} placeholder in the message above does NOT auto-substitute the input value — only the placeholders in Message placeholders get substituted. Bake the threshold into the literal text if you want it shown (e.g. "z>3.0") or wait for a future BE version that exposes runtime expressions in messages.

EMA ribbon agreement (multi-bar gate)

Fires only when ALL three EMAs are stacked in trend order — a stronger filter than a single MA cross. The condition is gated through barssince so an alert fires once per regime change, not continuously while stacked.

Delta DSL
e21  = ema(close, 21)
e50  = ema(close, 50)
e200 = ema(close, 200)

stackedUp = e21 > e50 and e50 > e200
stackedDn = e21 < e50 and e50 < e200

// Fire only on the FIRST bar the stack flips (regime change)
firstUp = stackedUp and not stackedUp[1]
firstDn = stackedDn and not stackedDn[1]

plotLine(e21,  color="#0ECB81", width=1)
plotLine(e50,  color="#F0B90B", width=1)
plotLine(e200, color="#F6465D", width=1.5)

alertcondition(firstUp,
               title="EMA ribbon stacked long",
               message="EMA ribbon flipped LONG on {{symbol}} ({{interval}}) @ {{price}}",
               name="ema_stack_long")

alertcondition(firstDn,
               title="EMA ribbon stacked short",
               message="EMA ribbon flipped SHORT on {{symbol}} ({{interval}}) @ {{price}}",
               name="ema_stack_short")

Pivot break (manual structure trader)

Fires when price closes through a recent pivot high / low. The pivots come from pivothigh / pivotlow with NaN-aware valuewhen to pin the most-recent confirmed level.

Delta DSL
@input pivotL = input.int(5, "Pivot left",  minval=2, maxval=20)
@input pivotR = input.int(5, "Pivot right", minval=2, maxval=20)

ph = pivothigh(high, pivotL, pivotR)
pl = pivotlow(low,   pivotL, pivotR)

lastPH = valuewhen(ph == ph, high, 0)  // NaN-aware "isPivot"
lastPL = valuewhen(pl == pl, low,  0)

breakUp = crossover(close, lastPH)
breakDn = crossunder(close, lastPL)

plotShape(breakUp, low,  shape="arrowUp",   color="#0ECB81", size=10)
plotShape(breakDn, high, shape="arrowDown", color="#F6465D", size=10)

alertcondition(breakUp,
               title="Pivot break up",
               message="{{symbol}} broke ABOVE last pivot high on {{interval}} @ {{price}}",
               name="pivot_break_up")

alertcondition(breakDn,
               title="Pivot break down",
               message="{{symbol}} broke BELOW last pivot low on {{interval}} @ {{price}}",
               name="pivot_break_dn")

Naming & duplicates

Every alertcondition ends up in the manifest under a stable name. The BE derives it in this order:

  1. Explicit name="…" kwarg — used verbatim (after slugifying).
  2. Slug of title="…" if no name= is given — lowercase, non-alphanumeric → _.
  3. alert_<index> as a last-resort fallback when neither is provided.

Two alertcondition calls that resolve to the same manifest name fail the save with HTTP 422:

Delta DSL
{ "error": "compile_failed", "message": "parse: line 89: duplicate alertcondition name: new" }

There are two ways to trip this rule.

1. Two identical titles

Delta DSL
// ❌ Both slug to "ma_cross" → 422 duplicate.
alertcondition(upCross, title="MA cross")
alertcondition(dnCross, title="MA cross")

Fix: give each call a unique title (preferred — the user sees it in the alert builder), or add an explicit name=:

Delta DSL
// ✅ Unique titles.
alertcondition(upCross, title="MA cross up")
alertcondition(dnCross, title="MA cross down")

// ✅ OR explicit names with shared title.
alertcondition(upCross, title="MA cross", name="ma_cross_up")
alertcondition(dnCross, title="MA cross", name="ma_cross_dn")

2. Dynamic titles that share a literal prefix

The BE compiler runs at save time, before any of the script's series have evaluated. It can't compute string concatenation involving runtime variables (like an @input value), so it slugifies only the literal prefix of the title expression:

Delta DSL
// ❌ Both titles start with the literal "New ".
//    BE sees the slug as "new" for both → 422 duplicate.
alertcondition(triggerA, title="New " + htf + " bar open above prev high")
alertcondition(triggerB, title="New " + htf + " bar open below prev low")

This is the most common cause of the duplicate-name error in scripts that template their titles from inputs. Fix it by providing an explicit name= per call — the title can still be dynamic for the user-facing display:

Delta DSL
// ✅ Explicit name= disambiguates at compile time.
//    title= remains dynamic for the alert builder UI.
alertcondition(triggerA,
               title="New " + htf + " bar open above prev high",
               name="htf_open_above_prev_high")
alertcondition(triggerB,
               title="New " + htf + " bar open below prev low",
               name="htf_open_below_prev_low")
  • For scripts with a single alert, you can skip name= — the slugified title is fine.
  • For scripts with multiple alerts, ALWAYS give each one an explicit name=. This stays stable across title edits, which matters because the user's saved wire-ups in /v1/alerts reference the alert by name — renaming the title (or letting the slug shift) without a stable name= will orphan existing alerts.
  • Use snake_case for name= values (e.g. bull_cross, htf_break_high). The slug rule lowercases and collapses non-alphanumeric runs into _ anyway, so anything else gets normalised.

Anti-patterns

Don't gate alertcondition behind an if block

Delta DSL
// ❌ Don't — alertcondition is registered at compile time.
if isLive
  alertcondition(crossUp, title="Live cross")
end

The BE compiler scans the source statically; an alertcondition call inside a conditional if block STILL ends up in the manifest. The runtime gating doesn't filter what shows up in the alert builder.

If you want a condition to be evaluated only sometimes, gate the condition expression instead:

Delta DSL
// ✅ Right.
liveCondition = isLive and crossover(close, ma)
alertcondition(liveCondition, title="Live cross")

Don't put per-tick state in the message

The BE evaluates the condition (and substitutes placeholders) on the firing bar. Anything else — your indicator's internal state, a counter — won't appear in the message. Stick to the {{…}} placeholders.

Don't expect the FE to fire alerts

Editing a condition in the script editor does NOT fire test notifications, even if the condition would be true on the current chart. Alerts only fire from the BE evaluator, which runs against the user's stored alert wire-ups. To verify a condition is wiring up correctly, save the script, open the alert builder, and confirm the entry shows up.

Next

  • DebugginglogInfo / logWarning / logError for auditing the conditions you wire to alerts before you enable them.
  • Versioning — what happens to existing alerts when you edit the script.
  • Recipes — full alert-driven scripts.
  • Chart Terminal overview — where alert notifications land in the trader's UI.