Nuances worth knowing
Behaviours that follow from the guarantees and still surprise authors. Each entry says what happens and what to do.
The forming bar runs many times
onBar runs again for the forming bar on every tick, and ctx.accum keeps what the last tick wrote. An EMA stepped in accum steps once per tick; a buffer that appends fills with copies. Store state on confirmed bars and compute the live value from it. Patterns are on Bars and clocks.
ctx.barIndex is not an identity
It counts from the start of the current pass, and a pass restarts at every stretch of the time grid and on every full recalculation. The same bar has different indices on your chart, in replay and on the server. Use ctx.time for ids, dedupe keys and "bars since" arithmetic: (ctx.time - st.lastT) / 300 on a 5m chart.
A daily bucket is a UTC day
timeframe("1d") and ctx.summary("1d", …) measure midnight to midnight UTC. A London or New York session, or a forex trading day that starts at 17:00 New York time, is a session question. Use ctx.session with the tz you need (Sessions).
Weeks start on Monday
The weekly bucket runs Monday 00:00 to Monday 00:00 UTC. Markets that open on Sunday evening put those hours in the previous week.
The forming higher-timeframe bucket is still moving
daily.forming at 14:35 holds today so far. Plotting it is fine; folding it into state from onBar folds a moving value once per chart bar. Fold closed buckets in the handle's handler and read last or state from onBar.
closedThisBar depends on the chart
h.closedThisBar counts buckets that closed on this chart bar: a 5m handle on a 1h chart reports 12 on each confirmed hour, and 1 on a 5m chart. The handle's values are identical on both charts; the count describes the chart bar you are on.
The first forming bucket can miss its start
When your chart's history starts inside the current higher-timeframe bucket, that bucket's forming.high and forming.low cover only the part on the chart until it closes. Its open is correct, and once it closes all its values are. Use last for anything that matters.
A warmup that reads a setting never goes below 50
A warmup(...) expression that reads a number from a setting, such as w.ema(length), resolves to at least 50 buckets, because a setting can hold a number nobody chose. A plain number and a span are used as written: w.day() on 1h is 24. The floor does not affect correctness; it explains a walk longer than you declared.
w.forever() follows the viewer's plan
w.forever() resolves to the memory limit of whoever runs the script. On daily charts that reaches all the history they hold. On intraday charts a running total restarts its origin every memory limit, so two accounts on different plans can see a different level for the same cumulative line. Reset the state on a boundary and declare the span between resets when you can.
Over-declaring costs time, never correctness
A warmup larger than needed is fetched and walked every time the grid moves. It is clamped at your memory limit and reported, never refused. Size it from the table on Warmup and memory rather than rounding up to the limit.
Session resets shorten the warmup a script needs
A value that resets at every session open forgets everything before the reset, so its warmup is the longest gap between resets, whatever its lookback. Declare it as a length of time: warmup((w) => w.day()) for a daily reset, which is 288 bars on 5m and 24 on 1h, and w.span("7d") for a weekly one. A span resolves on every timeframe the script runs on.
oncePer dedupes over the whole run
A run over history and a live chart start in different places. If the oncePer keys can repeat forever, the first match wins wherever the run began, and the two can disagree. Put a time-bound field, such as the session key, in the keys.
A level can only be taken out by a declared rule
Levels, zones and trendlines seal by the rule on their output declaration (invalidateWhen preset or declared function, or invalidateOnEvent), checked across the whole run on the chart and on the server. A function passed with one emission is refused, because once its stretch of the grid is done nothing can re-check it. Every sealing output also needs expires: a duration, so it expires at the same instant on every timeframe.
A throw behaves differently on the chart and the server
On the chart one throwing bar stops the indicator and names the bar. When a definition runs over history, the error is recorded for that bar and the run carries on. Guard every read that can be missing so neither happens.
Math.random and the wall clock
On the server Math.random() is seeded from the bar, so it repeats; in your browser it is not. Date.now() is refused in definitions and on the server. Scripts that need variety should derive it from ctx.time and the bar's prices.
Dependencies run on their own timeframe
A declared dependency read through a timeframe handler's ctx.indicators is computed on that handler's timeframe. The same dependency read from onBar is computed on the chart's timeframe. They are different series with different values, both correct.
A shared script uses the viewer's plan
Indicator memory, the indicator count and the timeframe count are the viewer's, so a script you publish can run with less history, or not run, on a smaller plan. Read history.truncated and say what the script measured.
History arrives before values
An indicator that declared history waits with "loading history" until it lands, so it never draws a provisional value. On a slow connection that wait is visible. "history not loaded" means nothing is fetching it; reload the chart.
