Debugging
Inspect your DeltaDSL script with logInfo, logWarning and logError. Per-true-bar enumeration, value placeholders, the dedicated Logs panel, click-to-jump navigation and emit caps.
When a strategy doesn't fire when you expect, or fires on bars you don't want, the answer is rarely "stare at the chart harder". DeltaDSL gives you three logging functions — logInfo, logWarning, logError — and a dedicated Logs panel docked next to the editor that mirrors what your script saw at every interesting bar.
Treat them the way you'd treat console.log in a normal program: drop a call at the bar where you expect something to happen, save, then read the panel to see what your script actually did. The functions are bulk-vector aware — they understand series and conditions natively, so a single call can enumerate every event in the chart's history.
Where logs appear
The Logs panel opens on the right of the script editor whenever the editor dialog is open. Every logInfo / logWarning / logError call your script makes streams into this panel in real time.
Each entry shows:
- Level icon — info, warning, or error.
- Bar timestamp — when on the chart the entry was emitted.
- Source line — the line in your script that ran the call. Click it to jump the editor caret there.
- Bar marker — click the small "scroll to bar" icon next to the timestamp to pan the chart to the exact candle the log fired on. A short-lived bubble pops up on that candle showing the log text.
- Message — the formatted string.
Panel controls (top bar):
| Control | Effect |
|---|---|
| Pause / Resume | Freezes the buffer. Existing entries stay visible; new emits are dropped until you resume. Use during live re-compiles so the panel doesn't scroll out from under you. |
| Clear | Empties the buffer. Useful when you want to see only logs from the next save / next bar close. |
Level chips (info, warning, error) | Toggle individual levels on / off in the visible list. The chips dim when off. |
| Search box | Substring filter against the message text. |
| Count | visible / total — when filters or level toggles hide entries, the dim number shows the underlying total. |
The Logs panel is a script-editor feature. Logs are intended for debugging while you write a script. They don't appear in the Chart Terminal's main UI and they don't survive a page reload — open the editor dialog whenever you want to see what your script is logging.
The three functions
logInfo("text or fmt {0}", arg1, arg2, …)
logWarning("text or fmt {0}", arg1, arg2, …)
logError("text or fmt {0}", arg1, arg2, …)
Identical signatures — the only difference is the severity level shown in the panel. Pick by intent:
| Function | Use when |
|---|---|
logInfo | Routine diagnostics. "Trigger fired at X", "RSI snapshot Y", "entry conditions met". |
logWarning | Something looks off but the script will continue. "Volume is zero on this bar", "input outside expected range". |
logError | A real failure path. "Required input missing", "computation produced NaN where it shouldn't". The panel highlights error entries in red so they stand out in a long list. |
There is no log.info / dotted form — the function names are flat (no member call).
Call shapes
logInfo (and the warning / error siblings) accepts four shapes. Reach for the shape that matches the question you're asking.
1. Plain text
logInfo("Indicator loaded")
One entry, message verbatim. Fastest, useful for sanity checks ("did this line even run?"). Emitted once at the end of the evaluation.
2. Snapshot of the final bar
r = rsi(close, 14)
logInfo("Final RSI = {0}", r)
One entry, placeholders resolved at the last bar of the series. This is the "what did the indicator end the chart with?" debug. Numeric series (rsi, sma, close, …) fall into this shape automatically because they are not boolean.
3. Per-true-bar enumeration (implicit gate)
trigger = crossover(close, sma(close, 50))
logInfo("Cross at bar {0}, price {1}", trigger, close)
When any placeholder argument is a boolean series, that series is treated as the gate. The log emits one entry per true bar, with {N} placeholders resolved at each matching bar.
The example above produces one entry for every bar where crossover fired, each with the bar index and the close price at that bar — exactly the per-event list you want when you ask "did this trigger on the bars I expected?".
Detection is automatic: the runtime peeks at the right edge of each argument, and the first boolean series it finds is the gate. Numeric series (RSI, close, anything that holds numbers) are never treated as a gate, so the snapshot shape in §2 continues to work unchanged.
4. Explicit gate
volSpike = volume > sma(volume, 20) * 2
logInfo(volSpike, "Vol spike: vol={0}, price={1}", volume, close)
The first argument is the gate, the second is the format string, the rest are placeholder values. Use this shape when:
- The gate is a different series from the values you want to print.
- The gate is a numeric expression treated as truthy (
close > 0rather than a pre-cast boolean). - You want the gate to be obvious from the call site instead of relying on type inference (§3).
Same per-true-bar behaviour as the implicit form: one entry per bar where the gate is truthy.
{N} placeholders
{0}, {1}, {2}, … are positional substitutions. The N-th placeholder is replaced with the N-th value argument (after the format string), rendered as a short string at the relevant bar.
| Value | Rendered as |
|---|---|
42 | 42 |
3.14159265358979 | 3.141593 (trimmed to a sensible decimal) |
na / NaN | na |
true / false | true / false |
| A series argument | The value of the series at the bar being logged (last bar for snapshot, the matching bar for per-event). |
| A string | The string itself. |
Unknown placeholders ({99} when there are only two args) are left in the message literally — they don't crash the call.
For complex formatting (fixed decimals, padding, custom date strings) prefer tostring() (see Math, format & time helpers) and concatenate the result:
logInfo("RSI = " + tostring(r, "0.00") + " on " + tostring(time, "yyyy-MM-dd HH:mm"))
Worked example: trigger audit
You added a divergence detector that fires "way too often" on EURUSD. Before changing thresholds, log the actual events:
@name "RSI Bull Div Audit"
@pane "below"
@input rsiPeriod = input.int(14, "RSI period", minval=2, maxval=200)
@input pivotL = input.int(5, "Pivot left", minval=2, maxval=20)
@input pivotR = input.int(5, "Pivot right", minval=2, maxval=20)
r = rsi(close, rsiPeriod)
priceLow = pivotlow(low, pivotL, pivotR)
rsiLow = pivotlow(r, pivotL, pivotR)
bullDiv = (low < shift(low, pivotL + pivotR))
and (r > shift(r, pivotL + pivotR))
paneRange(0, 100)
paneLine(r, color="#F0B90B", width=1.5)
plotShape(bullDiv, low, shape="arrowUp", color="#0ECB81", size=10)
// Snapshot: what did the run end with?
logInfo("Run finished. RSI={0}, lastClose={1}", r, close)
// Per-event enumeration: every bar where the detector fired.
logInfo("Bull div fired: low={0}, RSI={1}, prevLow={2}, prevRSI={3}",
bullDiv, low, r, shift(low, pivotL + pivotR), shift(r, pivotL + pivotR))
// Audit a suspicious sub-condition separately.
flatRsi = (r - shift(r, 5)) < 0.3
logWarning("Flat-RSI region (may dilute signal): RSI={0}", flatRsi, r)
Open the editor with this script. The Logs panel shows:
- One
INFO — Run finished. …entry at the top of the buffer (the snapshot). - One
INFO — Bull div fired: …entry per bar where the detector triggered. - One
WARN — Flat-RSI region …entry per bar in the flat zone.
Click any per-event entry's bar marker to scroll the chart to the candle that fired. The script tells you when, where, and with what values — without you having to read the source by eye.
Caps and limits
DeltaDSL logging has hard caps so a debug session can't accidentally fill the buffer with millions of entries and lock up the panel.
| Cap | Value | What it means |
|---|---|---|
| Per call | 500 entries | A single logInfo(true_series, …) over a long history emits at most 500 entries, even if the gate is true on thousands of bars. The 500 are the first matching bars in chart order, so you see the start of the pattern instead of just the tail. |
| Per evaluation | 2000 entries | The sum across every logInfo / logWarning / logError call in one script run. Beyond 2000, further emits are dropped. |
| Per entry | 2000 characters | Long messages (e.g. accidentally formatting an entire series) are truncated with a … (truncated) suffix. |
| Buffer size | 10000 entries | The panel keeps the most recent 10000 entries across all evaluations of this script. Older entries scroll off the bottom of the ring buffer. |
| Iteration budget | Shared | Each emit counts against the same per-evaluation iteration budget that loops use. Logging cannot be used to side-step the runtime's safety net. |
If the panel header shows visible / total with total >> visible, you've hit a cap or a level filter; clear the filter to confirm.
Anti-patterns
Don't ship logInfo in a published script
Logs are a debugging affordance for whoever is writing the script. Once the script is doing what you want and ready to share, remove (or comment out) the log calls. Leaving them in doesn't break anything — every call counts against the per-evaluation budget and clutters the panel for anyone else who opens the editor on the same script.
Don't use logs for live notifications
// ❌ Logs do not send push / Telegram / in-app notifications.
logError("BUY signal on {{symbol}}", buySignal)
Logs are a chart-editor diagnostic surface only. For real-time delivery (push, Telegram, in-app), use alertcondition — it goes through the backend evaluator and reaches the user regardless of whether the editor is open. {{symbol}} placeholders also only work in alert messages, not in log strings.
Don't log inside a tight loop without a guard
// ❌ Emits one entry per iteration — hits the per-call cap immediately.
for k = 0 to bars - 1
logInfo("k = {0}, val = {1}", k, at(close, k))
end
The 500-per-call cap protects you from a runaway buffer, but the first 500 iterations still produce 500 entries you have to scroll through. Add a real condition:
// ✅ Only log iterations that matter.
for k = 0 to bars - 1
val = at(close, k)
if val > extremeLevel
logInfo("Extreme close at k={0}, val={1}", k, val)
end
end
Or use the per-true-bar shape (§3) outside the loop, which is usually more idiomatic:
extreme = close > extremeLevel
logInfo("Extreme close: val={0}", extreme, close)
Don't pass a series where you mean its final value
// ⚠️ Logs "[series len=…]" if the placeholder is interpreted as a
// non-boolean series and the implicit-snapshot path doesn't trigger.
// Use the snapshot shape (§2) or pre-extract with at(series, lastBar).
logInfo("RSI is " + r) // string concat with series → falls back to "[series len=…]"
logInfo("RSI is {0}", r) // ✅ snapshot, resolves at the last bar
Use the {N} placeholder form when interpolating a series — it knows how to pick the right bar. String concatenation with + is for scalar values (numbers from at() / valuewhen(), strings, etc.).
Don't expect logs after a refresh
Logs live in a per-script in-memory ring buffer that's owned by the editor dialog. Closing the dialog, reloading the page, or switching to a different script clears the buffer. If you need a permanent record of a condition firing, that's an alert, not a log.