Docs·Delta DSL Script·Library

Series operations

shift, prev, at, change, crossover/crossunder/cross, highest/lowest, sum/cum, valuewhen, barssince, rising/falling. The bar-aware lookback helpers every indicator depends on.

These are the bar-aware helpers — every function on this page reads the history of a series to produce a new series. They're the building blocks of every indicator: cross detection, lookback, rolling extremes, conditional history.

All functions are NaN-tolerant — invalid inputs propagate as NaN at the affected indices instead of throwing.

Shift / lookback

shift(src, n)

Shift src RIGHT by n bars. The first n bars become NaN; bar i of the output is src[i - n]. n=1 is the most common case.

Delta DSL
prev = shift(close, 1)            // previous-bar close at every index
diff = close - shift(close, 5)    // 5-bar momentum

Negative n shifts LEFT (a "look ahead"). Use sparingly — most indicators don't have legitimate forward-look needs.

prev(src, n=1)

Alias for shift(src, n). Reads more naturally in a flow:

Delta DSL
gapPct = (open - prev(close)) / prev(close) * 100

at(series, i)

Return the scalar value of series at absolute bar index i. Distinct from series[k] / shift(series, k), which is a relative HISTORY shift.

Delta DSL
// Inside a for-loop iterating over bar indices:
for j = 0 to bars - 1
  if at(crossUp, j)
    lineNew("xup_" + tostring(j),
            at(time, j), at(low, j),
            at(time, j), at(high, j),
            color="#0ECB81")
  end
end

at(...) is what you reach for when you need scalar (timestamp, price) coordinates per loop iteration. NaN-safe: out-of-range or non-finite indices return NaN.

Difference

change(src)

src[i] - src[i-1]. NaN at index 0 (no previous bar).

Delta DSL
delta = change(close)              // per-bar return in absolute terms
isUpBar = change(close) > 0        // boolean series

Accepts numeric series AND boolean series — booleans are coerced to 0/1 before subtraction, so change(boolSeries) gives +1 / -1 / 0 for toggle / un-toggle / no-change.

Crossings

crossover(a, b)

True on the bar a crosses ABOVE b. False everywhere else. a and/or b may be a scalar or a series.

Delta DSL
buyEvent = crossover(close, sma(close, 50))
plotShape(buyEvent, low, shape="arrowUp", color="#0ECB81", size=8)

Returns a boolean series. NaN inputs at either bar produce false (not NaN).

crossunder(a, b)

True on the bar a crosses BELOW b. Symmetric to crossover.

Delta DSL
sellEvent = crossunder(close, sma(close, 50))
plotShape(sellEvent, high, shape="arrowDown", color="#F6465D", size=8)

cross(a, b)

True on EITHER crossover(a, b) or crossunder(a, b) — i.e. any directional cross.

Delta DSL
flip = cross(close, sma(close, 50))
alertcondition(flip, title="MA cross", message="Cross detected on {{symbol}}")

Rolling extremes

highest(src, period)

Rolling maximum over the last period bars (inclusive of the current bar). NaN until the warmup is satisfied (first period - 1 bars).

Delta DSL
hh = highest(high, 20)              // 20-bar high
plotLine(hh, color="rgba(14,203,129,0.5)", width=1)

lowest(src, period)

Rolling minimum.

Delta DSL
ll = lowest(low, 20)
plotBand(highest(high, 20), lowest(low, 20), color="rgba(240,185,11,0.06)")

highestbars(src, period)

Number of bars BACK to the rolling-max value. 0 means the current bar IS the max; period - 1 means the max is at the oldest bar of the window.

Delta DSL
peakAge = highestbars(high, 50)
freshBreak = peakAge == 0                // true on the bar that sets a new 50-bar high

lowestbars(src, period)

Symmetric to highestbars.

Rolling sum / cumulative

sum(src, period)

Rolling sum over period bars. NaN during warmup. Used to build manual moving averages, accumulation indicators, anything additive.

Delta DSL
volMa20 = sum(volume, 20) / 20      // identical to sma(volume, 20)

NaN-tolerant: bars where src is NaN don't contribute to the running sum, but the bar's output is still NaN until enough finite values have entered the window.

cum(src)

Cumulative sum from the first finite bar. NaN before the first finite bar; finite-and-monotonically-extending after.

Delta DSL
cumDelta = cum(change(close))
plotLine(cumDelta, color="#F0B90B")    // running price-change sum (≈ close - close[0])

Useful for building indicators like OBV (On-Balance Volume), accumulation/distribution lines, and PVT — though the canonical wrappers (obv, ad, pvt) are already in Indicators.

Anchored accumulators

These solve the Pine pattern that cum() alone cannot — a cumulative that restarts on an event (session boundary, ATR threshold, custom signal, …). Pine writes this as a per-bar var float acc := is_anchor ? value : acc + value, which the bulk-vector model can't express in user space. The functions below ship a single-pass O(N) implementation so the recurrence stays inside JS — same shape as holdSign / supertrend, just for anchored cumulation.

cumReset(value, resetEvent, initial=0)

Cumulative sum that resets to zero (and then adds the current bar's value) on every bar where resetEvent is truthy.

Anchored VWAP boilerplate collapses to two lines:

Delta DSL
isWeek = weekofyear(time) != weekofyear(time[1])
flow   = cumReset(hlc3 * volume, isWeek)
vol    = cumReset(volume,        isWeek)
vwap   = flow / vol
plotLine(vwap, color="#F0B90B", width=2)

Semantics: on the reset bar, value[i] IS the new total (matches Pine's acc := is_anchor ? value : acc + value). NaN values are treated as zero so the accumulator doesn't break on missing data. initial sets the seed before the first reset fires.

Use cases beyond VWAP: anchored session volume, session range (maxSinceReset(high) - minSinceReset(low)), running PnL since position open, cumulative imbalance per session.

maxSinceReset(value, resetEvent, initial=-Infinity)

Running maximum of value since the latest resetEvent bar (inclusive). NaN values are ignored. The reset bar wipes history and re-seeds with value[i].

Delta DSL
isSession = dayofmonth(time) != dayofmonth(time[1])
sessHi    = maxSinceReset(high, isSession)
sessLo    = minSinceReset(low,  isSession)

// Mark the session high / low as horizontal lines:
plotLine(sessHi, color="rgba(14,203,129,0.5)", width=1)
plotLine(sessLo, color="rgba(246,70,93,0.5)",  width=1)

minSinceReset(value, resetEvent, initial=+Infinity)

Symmetric mirror — running min since reset.

barssinceReset(resetEvent)

Bar count since the latest resetEvent bar (0 on the reset bar, 1 the next bar, …). Equivalent to barssince(resetEvent) — exposed under the anchored-helpers naming so all four siblings (cumReset, maxSinceReset, minSinceReset, barssinceReset) sit next to each other in autocomplete.

Delta DSL
sinceOpen = barssinceReset(isSession)
isEarly   = nz(sinceOpen, 999) < 5     // first 5 bars of the session
plotBgColor(isEarly, color="rgba(240,185,11,0.04)")

Generic per-bar state machines

The specialised helpers above (cum, cumReset, maxSinceReset, …) are all instances of the same shape — a running fold over the series. scan and accumReset ship that fold as generic primitives with a string op discriminator. They give you the Pine var x; if cond x := combine(x, value) recurrence in one call, with eight combiner choices and no sandbox break (op is a string, not a first-class JS function).

scan(src, op, initial=identity)

Forward fold over src. Op selects the combiner:

OpCombinerDefault initialUse case
"sum"state + v0Running sum (== cum(src))
"product"state * v1Compounded returns: scan(1 + ret, "product")
"max"max(state, v)-InfinityAll-time high (== cumMax)
"min"min(state, v)+InfinityAll-time low (== cumMin)
"last"v (overwrite)NaNLatest finite value carrying forward
"and"state && bool(v)trueSticky AND — stays true until first false
"or"state || bool(v)falseSticky OR — stays false until first true
"xor"state XOR bool(v)falseParity flip — toggles each true event

The op string is case-insensitive ("Sum", "SUM", "sum" all work). An unknown op throws a clear runtime error.

Delta DSL
// All-time high since chart load (equivalent to cumMax(high)):
peak = scan(high, "max")

// Has price EVER closed above 50k? (sticky-OR — stays true once tripped)
hitAth = scan(close > 50000, "or")

// Compounded equity curve from per-bar returns:
ret    = (close - shift(close, 1)) / shift(close, 1)
equity = scan(1 + ret, "product")          // 1.0 at start, grows / shrinks with returns
plotLine(equity, color="#F0B90B")

If initial is passed explicitly the output is non-NaN from bar 0; otherwise the warmup is one bar (or until the first finite value, for numeric ops).

accumReset(value, resetEvent, op, initial=identity)

Anchored fold — same op set as scan, but the state restarts to the op's identity on each bar where resetEvent is truthy. Generalises cumReset / maxSinceReset / minSinceReset and adds boolean-fold session quality filters that don't have specialised siblings:

Delta DSL
isSession = dayofmonth(time) != dayofmonth(time[1])

sessVol         = accumReset(volume, isSession, "sum")        // == cumReset(volume, isSession)
sessHigh        = accumReset(high,   isSession, "max")
sessLow         = accumReset(low,    isSession, "min")
sessCompounded  = accumReset(1 + change(close)/shift(close,1), isSession, "product")

// "Session has been all-green so far":
sessAllGreen    = accumReset(close > open, isSession, "and")
// "Any bear bar in the session yet":
sessAnyRed      = accumReset(close < open, isSession, "or")

plotBgColor(sessAllGreen, color="rgba(14,203,129,0.04)")
plotBgColor(sessAnyRed and not sessAllGreen, color="rgba(246,70,93,0.04)")

The bool variants ("and" / "or" / "xor") are particularly hard to express otherwise — there's no cum_and / cum_or specialised stdlib fn because that would balloon the API. accumReset(boolSeries, anchor, "and") covers the same ground in one call.

runStreak(boolSeries)

Consecutive-true counter. At each bar, the number of bars boolSeries has been continuously true ending at that bar; resets to 0 the moment it goes false. Always finite output (no NaN warmup).

Delta DSL
greenBars    = close > open
bullStreak   = runStreak(greenBars)
fiveInARow   = bullStreak >= 5
plotShape(fiveInARow, low, shape="diamond", color="#0ECB81", size=6)

// First bar of a NEW streak (streak == 1 AND previous streak == 0):
streakStart  = bullStreak == 1 and shift(bullStreak, 1) == 0
alertcondition(streakStart, title="Bull streak begins")

The Pine equivalent is var int streak = 0; streak := cond ? streak + 1 : 0 — common shape that previously needed a valuewhen + barssince dance to fake.

stateMachine(cond, value, initial=NaN)

Sticky value with explicit initial. On bars where cond is truthy, snap the state to value[i]; otherwise carry the previous state forward. Differs from valuewhen(cond, value, 0) only in that the initial state is configurable — valuewhen returns NaN before the first event, stateMachine returns whatever you pass as initial.

Delta DSL
// Track last entry price; seed at chart-load close so the plot
// has a value from bar 0 instead of waiting for the first signal:
longSignal  = crossover(close, ema(close, 20))
entryPrice  = stateMachine(longSignal, close, close[0])
plotLine(entryPrice, color="#0ECB81")

// Track current trend direction with an explicit "starts long" seed:
flipBull = crossover(close, sma(close, 50))
flipBear = crossunder(close, sma(close, 50))
dir      = stateMachine(flipBull or flipBear, iff(flipBull, 1, -1), 1)

Use valuewhen when you want the NaN-warmup semantics (e.g. "no signal yet — don't plot anything"). Use stateMachine when you need a value from bar 0.

Specialised running cumulatives

Thin wrappers over scan(src, op) — shorter to type, easier to read in indicator code:

cumMax(src)

Running maximum from chart start. Identical to scan(src, "max").

Delta DSL
ath        = cumMax(high)                    // all-time high since load
drawdown   = (ath - close) / ath * 100      // % drawdown from peak

cumMin(src)

Running minimum from chart start. Identical to scan(src, "min").

cumProd(src)

Running product from chart start (init=1). Useful for compounded-return curves:

Delta DSL
barRet     = (close - shift(close, 1)) / shift(close, 1)
equity     = cumProd(1 + barRet)
plotLine(equity, color="#F0B90B")

Conditional history

valuewhen(cond, src, occurrence=0)

Value of src on the bar where cond was last true. occurrence=0 is the most recent event; occurrence=1 is the previous event; etc. Carries forward between events.

Delta DSL
// Last swing-high price:
isPivot = pivothigh(high, 5, 5) == pivothigh(high, 5, 5)
lastSwingHigh = valuewhen(isPivot, high, 0)

plotLine(lastSwingHigh, color="rgba(14,203,129,0.6)", width=1)

// Two pivots ago:
prevSwingHigh = valuewhen(isPivot, high, 1)

NaN until the first cond event in the loaded history.

barssince(cond)

Number of bars since cond was last true. 0 on the firing bar, 1 the next bar, etc. NaN until the first event.

Delta DSL
@input maxAge = input.int(20, "Max age", minval=1, maxval=200)

since = barssince(crossover(close, sma(close, 20)))
freshSignal = nz(since, 999) <= maxAge
plotBgColor(freshSignal, color="rgba(14,203,129,0.04)")

nz(since, 999) substitutes a sentinel when no event has fired yet, so the comparison still works at warmup.

Direction tests

rising(src, length)

true on every bar where src[i] > src[i - length]. Use as a momentum filter.

Delta DSL
strongUp = rising(close, 5)
plotShape(strongUp, low, shape="circle", color="#0ECB81", size=4)

falling(src, length)

true when src[i] < src[i - length].

Delta DSL
strongDown = falling(close, 5)

Patterns

Confirmed crossover (lock on bar close)

crossover fires on every bar where the cross is currently visible — including the still-forming live bar, which can flicker in and out as ticks arrive. To gate on a CONFIRMED cross (only after the bar closes), shift by 1 and require the previous bar to have triggered:

Delta DSL
rawCross = crossover(close, sma(close, 50))
confirmed = shift(rawCross, 1)        // true one bar AFTER the cross fired

alertcondition(confirmed, title="Confirmed cross")

Pair this with the frequency="once_per_close" option in alertcondition (see Alerts) so the user can pick whether they want intra-bar firing.

"First N bars after event" highlight

Delta DSL
since = barssince(crossover(close, sma(close, 50)))
inWindow = nz(since, 999) <= 5         // first 5 bars after the cross

plotBgColor(inWindow, color="rgba(14,203,129,0.06)")

The nz(..., 999) keeps the comparison robust when no event has fired yet.

Distance-from-pivot label

Delta DSL
ph = pivothigh(high, 5, 5)
lastPh = fixnan(ph)               // step line that updates on each new pivot
gap = (close - lastPh) / lastPh * 100

labelNew("gap", time[0], close[0],
         "Δ vs swing: " + tostring(gap[0], "0.00") + "%",
         anchor="right", color="#F0B90B")

fixnan carries the latest pivot price forward; the label updates every redraw.

Performance notes

  • shift, change, crossover, crossunder, cross, highest, lowest, sum, cum, rising, falling are all O(n) in vector form. They run in JS-native loops, not script-eval-time iteration.
  • cumReset, maxSinceReset, minSinceReset, barssinceReset, cumMax, cumMin, cumProd are all O(n) — single forward pass each.
  • scan, accumReset, runStreak, stateMachine are all O(n) — the op-string is resolved to a JS function reference ONCE before the hot loop, so generic-fold throughput matches the specialised cumulatives. Roughly 10× faster than expressing the same pattern via repeated shift + valuewhen + nz calls.
  • valuewhen is O(n × k) in the worst case — every bar may scan back through history. For typical occurrence ≤ 5 and a few thousand bars, this is fine. For deep occurrence values on long histories, consider using fixnan(mask(src, cond)) to cache the most-recent value with a single pass.
  • barssince is O(n) — single forward pass with a counter.
  • at(...) is O(1).

Next

  • Indicators — moving averages, oscillators, volatility, structure.
  • Statistics — rolling stdev / variance / correlation / linreg / polyreg2 / zscore.
  • Drawing — turning the series you've computed into chart objects.