Docs·Delta DSL Script·Drawing

Drawing reference

Every drawing primitive in Delta DSL — per-bar plots, world-coord shapes, persistent slots, drawing-tool integration, color helpers, and the rules that govern them.

Drawing in Delta DSL is declarative. You don't grab a canvas and stroke; you call a plot* / pane* / draw* / *New function with the data you want drawn, and the engine records it. Records are flushed in batches each frame so a script that emits 50 000 markers is no slower than one that emits 50.

This page covers every drawing primitive. There are five families:

FamilyCoordinate systemPersists across framesUse for
Per-bar plotsBar indexNoLines / bands / markers tied to OHLCV
World-coord shapes(timestamp, price)No (re-emit each frame)One-shot annotations
Pane plots(timestamp, value) inside a sub-paneNoOscillator lines / bands
Persistent slots(timestamp, price)Yes (same name)HUDs, evolving labels
Drawing-tool shapes(timestamp, price)Yes; user-editableTrendlines / hlines / fibs that the trader can grip-drag

The Color helpers section near the bottom covers rgb, rgba, withAlpha, gradient.

A note on color

plot* and pane* arguments named color / bg / border are single static strings — they are read once at draw-call time. Passing a series of color strings (e.g. iff(cond, "#0ECB81", "#F6465D")) silently falls back to a default colour because the bridge's color parser only understands strings.

To paint a multi-tone trace, split the source via mask(series, cond) and emit one plotLine (or plotShape, etc.) per colour:

Delta DSL
ma     = sma(close, 20)
bullMa = mask(ma, close >= ma)
bearMa = mask(ma, close < ma)

plotLine(bullMa, color="#0ECB81", width=2)
plotLine(bearMa, color="#F6465D", width=2)

Per-bar plots

These render one shape (or one line segment) per bar of the visible window, using the bar's index for the X coordinate and the supplied series value for Y. They live on the candle pane (in price space) and respect @pane "overlay" (the default). For sub-pane equivalents see Pane plots.

plotLine(series, color="#…", width=1.5)

Smooth polyline through series. NaN entries become gaps.

Delta DSL
ma = sma(close, 20)
plotLine(ma, color="#F0B90B", width=2)
ParamTypeDefaultDescription
seriesseries<number>Y values per bar
colorstring"#ffffff99"Line colour
widthnumber1.5Stroke width in pixels

plotDashedLine(series, color, width=1, dash=4, gap=4)

Same as plotLine but with a dashed stroke. dash and gap are measured in pixels.

Delta DSL
plotDashedLine(sma(close, 200), color="#ffffff77", width=1, dash=6, gap=6)

plotBand(upper, lower, color="rgba(...)")

Fill the area between two series. NaN gaps in either series leave the corresponding region empty.

Delta DSL
upper = bb_upper(close, 20, 2)
lower = bb_lower(close, 20, 2)

plotBand(upper, lower, color="rgba(240,185,11,0.06)")
plotLine(upper, color="rgba(240,185,11,0.5)")
plotLine(lower, color="rgba(240,185,11,0.5)")

plotCandle(open, high, low, close, bullColor=, bearColor=, wickColor=, width=)

Per-bar OHLC primitive. Each bar draws a wick from high[i] to low[i] plus a body from open[i] to close[i]. Bars where any of open / high / low / close is NaN are skipped — pair with mask(...) to draw on a sparse subset.

ParamDefaultNotes
bullColor"#089981"Used when close ≥ open
bearColor"#F23645"Used when close < open
wickColorbody colourSet to a separate string for two-tone candles
widthengine adaptiveOverride only for unusual layouts
Delta DSL
// Heikin-Ashi candles overlaid on the chart:
ha_close = (open + high + low + close) / 4
ha_open  = sma(ha_close, 1)            // simplified — full HA carries forward
ha_high  = max(high, max(ha_open, ha_close))
ha_low   = min(low,  min(ha_open, ha_close))

plotCandle(ha_open, ha_high, ha_low, ha_close,
           bullColor="rgba(14,203,129,0.7)",
           bearColor="rgba(246,70,93,0.7)")

plotHline(price, color, width=1) / plotDashedHline(price, color, width=1, dash=6, gap=4)

Horizontal line at a fixed price. The line spans the full visible window.

Delta DSL
plotHline(50000, color="rgba(240,185,11,0.4)")
plotDashedHline(45000, color="rgba(240,185,11,0.25)")

plotMarkerUp(boolSeries, priceSeries, color="#…", size=6), plotMarkerDown(...)

Triangle markers. boolSeries[i] truthy → draw a triangle at (i, priceSeries[i]).

Delta DSL
isCrossUp = crossover(close, sma(close, 20))
plotMarkerUp(isCrossUp, low - atr(high,low,close,14)*0.5,
             color="#0ECB81", size=8)

plotShape(boolSeries, priceSeries, shape="circle", color, size=4)

General-purpose shape marker. shape accepts:

ValueGlyph
"circle"filled circle
"triangleUp" / "triangleDown"filled triangle
"square"filled square
"diamond"filled diamond
"cross"+ shape
"xcross"× shape
"arrowUp" / "arrowDown"block arrow
Delta DSL
plotShape(volume > sma(volume, 20) * 2, low, shape="diamond", color="#F0B90B", size=6)

plotText(boolSeries, priceSeries, text, color, size=10)

Text label on every truthy bar. text is a single string applied to all firing bars.

Delta DSL
plotText(rsi(close, 14) > 70, high, "OB", color="#F6465D", size=11)

For per-bar dynamic text, use the world-coord plotLabel inside a for loop.

plotChar(boolSeries, priceSeries, char, color, size=10)

Unicode glyph at each truthy bar. Use raw glyphs (, , ·, ) — emoji render inconsistently across platforms.

Delta DSL
plotChar(crossover(close, sma(close, 50)), low,  "▲", color="#0ECB81", size=14)
plotChar(crossunder(close, sma(close, 50)), high, "▼", color="#F6465D", size=14)

plotBgColor(boolSeries, color="rgba(...)")

Translucent vertical strip behind every truthy bar. Adjacent runs are merged into one rect for performance.

Delta DSL
isWeekend = dayofweek(time) == 0 or dayofweek(time) == 6
plotBgColor(isWeekend, color="rgba(255,255,255,0.04)")

World-coordinate shapes

These take timestamps (epoch ms, the time series or scalars from it) and prices as anchors. Shapes pan and zoom with the chart — anchored to the data, not the screen.

plotLineXY(t1, p1, t2, p2, color=, width=, dash=, gap=, extend=)

Trendline between two anchors.

extend accepts:

ValueEffect
"left"Extend only to the left edge
"right"Extend only to the right edge
"both"Extend in both directions
"up" / "down"Vertical extension (mostly for diagnostics)
numeric bitmaskCombine flags directly
Delta DSL
phPrice = highest(high, 50)[0]
plPrice = lowest(low,   50)[0]
hiTime  = time[highestbars(high, 50)[0]]
loTime  = time[lowestbars(low,   50)[0]]

plotLineXY(loTime, plPrice, hiTime, phPrice,
           color="#F0B90B", width=1.5, extend="right")

plotVLine(t, color=, width=, dash=, gap=)

Vertical line at a timestamp, spanning the full chart height.

Delta DSL
isLondonOpen = hour(time) == 8 and minute(time) == 0
// emit one VLine per London open:
for i = 0 to bars - 1
  if at(isLondonOpen, i)
    plotVLine(at(time, i), color="rgba(240,185,11,0.3)", width=1, dash=4, gap=4)
  end
end

plotBox(t1, p1, t2, p2, bgColor=, borderColor=, width=, dashed=)

Filled + stroked rectangle in (time, price) space. Pass alpha=0 (e.g. "rgba(0,0,0,0)") in either color to skip that side.

Delta DSL
hi = highest(high, 100)
lo = lowest(low,   100)

plotBox(time[99], lo[0], time[0], hi[0],
        bgColor="rgba(240,185,11,0.04)",
        borderColor="rgba(240,185,11,0.5)",
        width=1)

plotLabel(t, p, text, color=, bg=, border=, size=, align=, anchor=)

Single-shot text chip at (t, p).

anchor controls chip placement around the point with an arrow tail:

ValueBehaviour
"none" (default)Chip sits ON the point, aligned via align
"above"Chip BELOW the point, arrow points UP
"below"Chip ABOVE the point, arrow points DOWN
"left"Chip RIGHT of the point, arrow points LEFT
"right"Chip LEFT of the point, arrow points RIGHT

align (when anchor="none") accepts "left", "center", "right".

Delta DSL
plotLabel(time[0], close[0], "Now: " + tostring(close[0], "0.00"),
          color="#F0B90B", anchor="left")

plotPolyline(times, prices, color=, width=, dashed=, dash=, gap=)

Connect arbitrary world-anchored points (parallel arrays). Straight segments — no spline smoothing.

Delta DSL
// Draw a zigzag through the last 5 swing pivots (build the arrays in a for-loop):
ts = nz(0, 0)              // empty number-series placeholder via shift trick
ps = nz(0, 0)              // (in practice you'd build via a fn or
                           //  pre-compute via `valuewhen` + `shift`)
plotPolyline(ts, ps, color="#F0B90B", width=1.5, dashed=true)

plotPolygon(times, prices, fill=, border=, width=)

Filled polygon in world coords. Implicitly closed.

Delta DSL
plotPolygon(timesArr, pricesArr,
            fill="rgba(240,185,11,0.06)",
            border="rgba(240,185,11,0.5)",
            width=1)

plotArrow(t1, p1, t2, p2, color=, width=, head=)

Arrow with a triangular head from (t1, p1) to (t2, p2).

Delta DSL
plotArrow(time[10], low[10], time[0], close[0],
          color="#0ECB81", width=2, head=10)

Pane plots

Available only inside @pane "below" or a custom aux pane. They map to the pane's own value space (e.g. RSI 0-100), not the candle price scale.

Delta DSL
@pane "below"

paneHeight(0.25)
paneRange(0, 100)

r = rsi(close, 14)
paneLine(r, color="#F0B90B", width=1.5)
paneHline(70, color="rgba(246,70,93,0.6)")
paneHline(30, color="rgba(14,203,129,0.6)")
paneBand(70, 30, color="rgba(240,185,11,0.06)")
FunctionEffect
paneLine(series, color, width=1.5)Polyline inside the band
paneDashedLine(series, color, width=1, dash=4, gap=4)Dashed polyline
paneHline(value, color, width=1)Horizontal at a fixed pane value
paneBand(upper, lower, color)Filled OB/OS band
paneFill(series, base=0, color)Histogram fill from base up/down to series[i]
paneCandle(open, high, low, close, …)Per-bar OHLC inside the pane
paneLabel(value, text, color, size=10, align)Annotate a level
paneRange(min, max)Lock the pane Y axis (else auto-fit)
paneHeight(ratio)Pane height, 0.10..0.50 of chart area

paneRange and paneHeight are one-shot — call them once near the top of the script. Multiple calls override.

The world-coordinate primitives have pane equivalents too: panePlotLine, panePlotVLine, panePlotBox, panePlotLabel. Y is in the pane's value space, not price.

See Panes for the full pane workflow.

Persistent slots

The first argument to every *New function is a string slot name. The same name across frames mutates the same primitive in place; omit a previously-emitted name and the primitive is freed at end-of-frame.

This is how you build:

  • Live HUDs that update text/anchors every redraw.
  • Persistent boxes around evolving ranges.
  • Snake-line drawings from a fixed point to "now".
FunctionArgs
labelNew(name, t, p, text, color=, bg=, border=, size=, align=, anchor=, pane=)Slotted version of plotLabel
boxNew(name, t1, p1, t2, p2, bgColor=, borderColor=, width=, dashed=, extend=, pane=)Slotted box
lineNew(name, t1, p1, t2, p2, color=, width=, dash=, gap=, extend=, pane=)Slotted line
polylineNew(name, times, prices, color=, width=, dashed=, fill=, closed=, pane=)Slotted polyline
Delta DSL
@name "Live HUD"

r   = rsi(close, 14)
atr14 = atr(high, low, close, 14)

text = "RSI " + tostring(r[0], "0.0")
     + "\nATR " + tostring(atr14[0], "0.00")

labelNew("hud", time[0], high[0], text,
         anchor="above", align="left",
         color="#F0B90B", bg="rgba(0,0,0,0.6)")

The "hud" slot is mutated in place every redraw — no flicker, no re-create cost.

Slot conventions

  • Use a stable name across frames as long as the primitive should stay visible.
  • Patterns:
    • "hud" / "trend_label" — fixed-purpose primitives.
    • "buy" / "sell" — one slot per side.
    • "alert_" + tostring(i) — short-lived primitives keyed by event bar.
    • "trend_" + tostring(j) — N-th lookback level.

Slot caps

  • 500 labels per script
  • 50 boxes per script
  • 100 lines per script
  • 50 polylines per script

Allocations beyond the cap silently no-op so a misbehaving script can't tank the page. The profiler badge in the script editor shows the live count.

pane= argument

Persistent primitives accept a pane= keyword:

ValueEffect
"overlay" (default)Render on the candle pane
"below"Render in the legacy below pane
<aux-pane-id>Render in the named aux pane (e.g. "rsi", "macd")

A label living in the candle pane and a label living in @pane "rsi" use independent slot tables — same name doesn't conflict.

Drawing-tool shapes

Persistent shapes routed through the engine's regular drawings Vec — the same pool that user-drawn shapes live in. Right-click delete, grip-drag, and snap-to-bar all work on them. Same string-slot semantics as labelNew.

FunctionAnchors
drawingTrendline(name, t1, p1, t2, p2, color=, width=, dashed=, pane=)Two (t, p)
drawingHline(name, price, color=, width=, dashed=, pane=)One price
drawingFibRet(name, t1, p1, t2, p2, color=, width=, pane=)Two (t, p) define 0% / 100%
drawingChannel(name, t1, p1, t2, p2, t3, p3, color=, width=, pane=)Anchors 1+2 set the trend, 3 sets the parallel offset
Delta DSL
// Auto-fib retracement off the last 50-bar range:
hiBars = highestbars(high, 50)[0]
loBars = lowestbars( low, 50)[0]

drawingFibRet("auto_fib",
              time[loBars], at(low,  bars - 1 - loBars),
              time[hiBars], at(high, bars - 1 - hiBars),
              color="#F0B90B", width=1)

When the user drags the script-emitted trendline by hand, the new anchors are visible to the next compute via getUserDrawings — a great way to build "snap-to-trader-line" interactive tools.

getUserDrawings()

Returns a frozen array of { id, owner, kind, pane, anchors, anchorsRaw, style } — every drawing currently on the chart that the user (NOT the script) created. Kinds include "trendline", "hline", "fib_ret", "channel", "arrow", "range", "circle", "fib_ext", "brush", "path", "elliott", "anchored_vwap", "position", "price_label", "arrow_marker_up", "arrow_marker_down", "text_note".

Delta DSL
draws = getUserDrawings()
n = 0                          // count via an `at`-style helper if needed

Important. Delta DSL grammar does NOT support obj.field member access on arbitrary values. To read fields of a getUserDrawings() entry, you need a stdlib helper that returns a primitive (e.g. a future firstUserDrawingPrice("hline") helper). Right now getUserDrawings() is a forward-looking primitive whose raw shape is exposed but only consumable through future helper APIs. Use drawingHline / drawingTrendline etc. for the create / mutate side.

Color helpers

Compose colours dynamically inside the script. All four are scalar (return strings, not series).

rgb(r, g, b)

Compose a #rrggbb hex from 0-255 channels. Channels are clamped to [0, 255].

Delta DSL
plotLine(ma, color=rgb(240, 185, 11), width=2)        // #f0b90b

rgba(r, g, b, a)

Compose an rgba(...) string. a ∈ [0, 1].

Delta DSL
plotBand(upper, lower, color=rgba(240, 185, 11, 0.06))

withAlpha(colorStr, alpha)

Apply an alpha override to any colour string. Recognises #rgb, #rrggbb, #rrggbbaa, rgb(...), rgba(...). Other inputs return unchanged.

Delta DSL
@input lineColor = input.color("#F0B90B", "Line color")

plotLine(ma, color=lineColor)
plotBand(upper, lower, color=withAlpha(lineColor, 0.08))     // same hue, dimmer band

gradient(t, colorA, colorB)

Linearly interpolate between two colours by t ∈ [0, 1]. Returns colorA at 0, colorB at 1; out-of-range clamps. Useful for "value heat" colouring of static thresholds:

Delta DSL
@input ratio = input.float(0.5, "Mix", minval=0, maxval=1, step=0.05)

mixedColor = gradient(ratio, "#0ECB81", "#F6465D")
plotLine(ma, color=mixedColor, width=2)

Note. Per-bar dynamic colours are not supported by plotLine (see the top of this page). gradient / withAlpha / rgb / rgba are useful for ONE-SHOT colour derivation at compute time — their output should be a single string passed once to a draw call.

Performance notes

  • Every per-bar plot is O(n) where n = bars — the engine iterates the visible window once per draw call.
  • World-coord primitives (plotLineXY, plotBox, plotLabel, …) are O(1) per call. Don't worry about emitting a few hundred per script.
  • Persistent slots are O(1) per call too, but count toward the slot caps (500 labels / 50 boxes / 100 lines / 50 polylines per script). A for i = 0 to bars-1 ... labelNew(...) end over a 5 000-bar window WILL cap out — for that, use plotShape (which compresses across bars) instead.
  • Total draw records per script are capped at 50 000. A script that produces more silently truncates and shows a warning chip in the legend.

Next

  • Panes — the sub-pane workflow end-to-end.
  • Alerts — wiring alertcondition into push / Telegram / in-app.
  • Recipes — full scripts that use every drawing family.