Timeframe handles and runOn
To read another timeframe, such as a daily EMA on a 5-minute chart, declare a timeframe(...) handle at module scope. To limit which chart timeframes a script runs on, set runOn in meta. How far back each timeframe reaches is set by warmup and retain, covered at the end of this page.
runOn
meta({
shortName: "OR",
kind: "overlay",
runOn: ["1m", "5m"],
runOnReason: "The opening range is built from intraday bars",
});runOn is an allow-list of chart timeframes. On any other timeframe the script does not run, and Charts shows runOnReason. Leave it out to allow every timeframe. Definitions and studies take the same two fields. runOn does not load any extra timeframe: that is what handles are for.
Timeframe handles
timeframe(source, options?): TimeframeHandle
handle.onBar((ctx: TimeframeCtx) => void): handle // optional, at most once per handlesource is a timeframe literal ("1m", "5m", "15m", "30m", "1h", "2h", "4h", "1d", "1w"), "chart" for the chart's own timeframe, or an input.timeframe handle. ?:, ??, || and && over those are accepted. Anything else, such as ctx.params.htf or a concatenated string, fails lint with tf/untyped-source or tf/no-ctx-at-module-scope.
meta({ shortName: "D-EMA", kind: "overlay" });
const daily = timeframe("1d");
daily.onBar((ctx) => {
const st = ctx.accum("ema", () => ({ e800: NaN }));
st.e800 = Number.isFinite(st.e800) ? st.e800 + (2 / 801) * (ctx.close - st.e800) : ctx.close;
});
warmup(24000); // about 30x the EMA length, in daily buckets; clamped to your plan's memory
function onBar(ctx) {
const e = daily.state.ema && daily.state.ema.e800;
if (Number.isFinite(e)) ctx.plot("ema800", e);
}The handler runs on the timeframe's own bars: once per closed bucket, oldest first, starting with the history loaded before the chart begins, so its state has converged by the first visible bar. What it keeps in ctx.accum is the handle's state: ctx.accum("ema", …) in the handler is daily.state.ema in onBar.
A handler produces no output. plot, emit, hline, range, mark and layer writes throw inside it (timeframe/no-output-in-handler). The chart's onBar is the only place that draws or emits, and it reads the handle.
The handler ctx
The handler receives the closed bucket, not a chart bar:
Field | Meaning |
|---|---|
| The bucket |
| Bucket index within the run; negative for warmup buckets |
| The timeframe this handle resolved to |
| Settings and this handle's state |
| The last |
| A handle listed in |
| Declared dependencies, computed on this timeframe's bars |
| As in |
What onBar reads
Read | Meaning |
|---|---|
| The handler's state. Shared and mutable: |
| The newest closed bucket. |
| The bucket containing the current chart bar. Higher timeframes only |
| How many buckets closed on this chart bar: 0 or 1 for a higher timeframe, 0 to n for a lower one, 1 for the chart's own on a confirmed bar |
| The last |
| Where this timeframe's history starts, and whether it was cut |
| The resolved timeframe string |
Every read looks back only: last and bars(n) never include the forming bucket. Each bucket has open, high, low, close, volume, volumeBid, volumeAsk, time (bucket start) and barIndex.
Acting once per close
if (daily.closedThisBar && daily.state.signal) {
ctx.emit("sig", { dir: daily.state.signal }, { label: "Daily signal" });
}The handler ran exactly once for the bucket that just closed, and closedThisBar is non-zero on that chart bar only. Do not remember a bucket time to detect the close yourself.
When a bucket closes
A bucket closes on the chart bar whose end reaches the bucket's end: the daily closes on the 23:55 bar of a 5-minute chart, not on the 00:00 bar after it, once a later bar confirms the 23:55 bar. On a live chart that is a tick after midnight. Everything ending at one instant closes in one step: at Monday 00:00 the 1m bucket, the 4h, the day, the week and the chart bar itself. Within that step every bucket is sealed, then handlers run in declaration order, then onBar.
4h buckets start at 00:00, 04:00, 08:00, 12:00, 16:00 and 20:00 UTC. Weeks start Monday 00:00 UTC.
Chart-timeframe and lower-timeframe handles
timeframe("chart"), or a literal equal to the chart's timeframe, runs its handler once per confirmed chart bar. last is the newest confirmed bar, closedThisBar is 1 on a confirmed bar and 0 on the forming one, and bars(n) is empty.
A lower timeframe, such as timeframe("1m") on a 5-minute chart, reads a separate, finer feed. Its handler runs once for each 1m bucket that closed inside the chart bar, oldest first, and m1.bars(m1.closedThisBar) is exactly those buckets. A lower timeframe has no forming bucket.
Two handles on one timeframe share the data and keep separate state.
Reading another handle from a handler
A handler sees only its own timeframe unless it declares what else it reads:
const weekly = timeframe("1w");
weekly.onBar((ctx) => { /* ctx.state.ema, ctx.state.bias */ });
const daily = timeframe("1d", { reads: [weekly] });
daily.onBar((ctx) => {
const w = ctx.tf(weekly); // weekly, already advanced through this instant
ctx.state.streak = ctx.close > w.state.ema ? (ctx.state.streak || 0) + 1 : 0;
});readsmay name only handles declared above. Naming one declared below fails withtf/reads-forward-reference, and naming something that is not a handle fails withtf/reads-not-a-handle. Two handles can never read each other.ctx.tf(other)returns{ last, forming, state, closedThisBar }, advanced through the current instant, live and during the warmup walk alike.ctx.tf(x)for anything not inreadsis refused withtimeframe/undeclared-read, and that handler stops for the run. To reach the chart's bars from a handler, declareconst chart = timeframe("chart")above it and list it inreads.A handle listed in
readsthat its handler never reads warns withtimeframe/reads-unused.
If two handles need each other, split the state by direction: keep the weekly EMA in weekly, let daily read it, and add const weekView = timeframe("1w", { reads: [daily] }) below it for the week's view of its days. Anything stateless belongs in onBar, where every handle has already advanced.
Rules
Declare handles and register handlers at module scope, next to the inputs. A handle inside a function fails with
tf/handle-not-module-scope; a secondonBaron one handle fails withtf/duplicate-handler.Never build a timeframe string by concatenation.
Guard
undefined:lastis undefined until the first close.ctx.security, andctx.tfinsideonBar, have been removed. Linttf/security-removednames the rewrite.meta.window.timeframesis deprecated. Handles declare what the script reads.In a
mode: "once"indicator, read a handle positioned at bariwithhandle.at(i):last,forming,closedThisBarandbars(n).handle.stateis the state at the end of the window.
Studies
Studies have no timeframe handles. Measure forward and backward from the event with the candle helpers: see Results and stats. The timeframe a study runs on is the one its definitions' events were recorded on, so match runOn to theirs.
History depth: warmup and retain
Two top-level calls set how much history a script gets. Both go after the inputs, because they can read input handles.
Call | Meaning | Declare it when |
|---|---|---|
| How far back the script's state remembers: buckets walked before the range, so state that accumulates has converged | The script keeps state: an EMA, a running extreme, a zone list |
| Buckets readable through | The script reads a span rather than accumulating over it |
Both count buckets of the timeframe being read, and each timeframe the script reads resolves its own count. A constant is the same count everywhere: warmup(1000) means 1,000 5-minute bars for onBar and 1,000 daily buckets for a daily handle. A span is the same length of time everywhere: w.day() is 288 buckets for a 5m onBar and 24 for an hourly handle.
A script that keeps state must declare warmup, or lint fails with window/warmup-required. A script that keeps none declares nothing. A warmup on a script that declares handles but registers no handler has nothing to walk, and fails to compile with window/warmup-inert.
Declaring warmup
const period = input.number({ id: "period", label: "Period", default: 20, min: 1 });
warmup((w) => w.ema(period)); // follows the setting the reader picksCall it once. The builder w:
warmup(1000) // a constant count of bars
warmup((w) => w.window(period)) // a lookback of `period` bars
warmup((w) => w.ema(period)) // EMA convergence: 30x the period
warmup((w) => w.wilder(period)) // Wilder smoothing (RSI, ATR, ADX): 50x the period
warmup((w) => w.span("1d")) // a length of time: 1,440 bars on 1m, 288 on 5m, 24 on 1h
warmup((w) => w.day()) // the same as span("1d")
warmup((w) => w.of(period).times(0.5)) // a setting scaled by your own factor
warmup((w) => w.range(band, "hi").times(30)) // one end of an input.range
warmup((w) => w.maxOf(lengths).times(30)) // the largest value of a numeric multiSelect
warmup((w) => w.maxOf(rows, "length").times(30)) // a field across input.array rows
warmup((w) => w.sumOf(rows, "length").times(30)) // summed across rows
warmup((w) => w.sum(w.ema(slow), w.ema(signal))) // stages that feed each other add up (MACD)
warmup((w) => w.max(w.day(), w.wilder(period))) // the larger of two needs
warmup((w) => w.when(maType) // branch on a select
.is(["SMA", "WMA"]).then(w.window(period))
.is("EMA").then(w.ema(period))
.otherwise(w.wilder(period)))
warmup((w) => w.when(useSlow).then(w.ema(slow)).otherwise(w.ema(fast))) // a boolean
warmup((w) => w.forever()) // never forgets: as far back as the plan allowsSpan units are m, h, d and w: "90m", "4h", "3d", "2w". Pass input handles, never id strings. A colour, line style or line width input cannot drive warmup. Disabled input.array rows still count, so toggling a row does not change the depth.
How deep
State | Warmup |
|---|---|
A lookback of N bars: SMA, Donchian, a rolling percentile |
|
Exponential: EMA |
|
Wilder / RMA: RSI, ATR, ADX |
|
Resets every session or day: a session VWAP, anything gated on |
|
Resets weekly or monthly | The longest gap between resets: |
Carries the previous session forward | The span that covers it: |
Never forgets: a running total, a count, a latched flag |
|
Warmup covers every piece of state in the script, so one never-forgetting accumulator makes the whole declaration w.forever(). w.forever() on state that only decays warns with window/forever-on-decaying.
A plain number and a span are taken as written. An expression that reads a number from a setting resolves to at least 50 bars, because a setting can hold a number nobody chose, such as 0. The full resolution rules are on Warmup and memory.
Declaring retain
retain(...) changes what can be read back, never what is walked. The maximum of the last 250 daily highs needs retain({ "1d": 250 }) and no warmup, because nothing accumulates.
meta({ shortName: "ATH-250", kind: "overlay" });
const daily = timeframe("1d");
retain({ "1d": 250 });
function onBar(ctx) {
const highs = daily.bars(250).map((b) => b.high);
if (highs.length > 0) ctx.plot("hi250", Math.max(...highs));
}The forms:
retain(64) // every timeframe
retain({ "1d": 250, "1h": 500 }) // per timeframe
retain((r) => r.span("20d")) // 20 days of whichever timeframe is read
retain((r) => r.max(r.of(lookback), 64)) // follows a setting
retain((r) => r.forever()) // as far back as the plan allowsr has the shared terms: r.of, r.range, r.sum, r.max, r.maxOf, r.sumOf, r.when, r.span, r.day and r.forever. A span resolves per timeframe, so r.span("20d") keeps 20 daily buckets on a daily handle and 480 on an hourly one.
The memory cap
Both depths are capped per timeframe at your plan's indicator memory: Free 2,500, Starter 5,000, Pro 5,000 and Ultra 10,000 buckets. w.forever() and r.forever() resolve to that number. Over-declaring is clamped and warns with window/clamped-to-cap, never an error. See Budgets and limits and Indicator memory.
Say what you can support
ctx.history, handle.history and a handler's ctx.history report where history actually starts:
{ from: number; buckets: number; truncated: boolean; reason?: "cap" | "plan" | "instrument" | "floor" }A script that labels a value "all-time high" while truncated is true is wrong in a way nobody checks. Read it before stating an extreme.
To find an extreme, a total or a period's open over a long span, use ctx.summary instead of a bigger warmup: see Summaries and long windows. Why the numbers match on every chart and on the server, and how the warmup walk is anchored, is on Warmup and memory and Seams and anchors.
