Multi-timeframe (HTF)
Aggregate any chart-TF series into HTF buckets ("1D", "1W", "1M", …) and broadcast back aligned to chart bars. Pine request.security equivalent for same-symbol resampling — no network, no repaint by default.
The HTF helpers let a script READ data as if it were running on a HIGHER timeframe (HTF) than the chart's current TF, then receive the result aligned back to chart bars. Same-symbol only — Phase 3.1 does not cross symbols (see Limitations for the cross-symbol roadmap).
This covers ~80% of what Pine's request.security(syminfo.tickerid, tf, expr) is used for: anchored daily/weekly/monthly levels, HTF indicators overlaid on intraday charts, "new daily bar" gates, "today's high so far" overlays.
At a glance
| Function | Returns | Use case |
|---|---|---|
htf_resample | aggregated series | core primitive — any series → HTF bucket → broadcast |
htf_new_bar | bool series | "fire once per new HTF bar" gate |
htf_bucket_start | ms series | x-coord for "anchor at bucket boundary" drawings |
htf_bars_in_bucket | int series | progress within current HTF bucket |
Timeframe string format
| String | Meaning | Bucket boundary |
|---|---|---|
"1m" / "5m" / "15m" / "30m" | Minute window | seconds-since-epoch grid floor |
"1h" / "2h" / "4h" / "12h" | Hour window | seconds-since-epoch grid floor |
"1D" / "3D" | Day window | UTC midnight, anchored to epoch day 0 |
"1W" / "2W" | Week window | ISO Monday 00:00 UTC, anchored across N-week windows |
"1M" / "3M" | Month window | UTC 1st of month, anchored to year boundary (so "3M" = Q1/Q2/Q3/Q4 starts) |
"1Y" | Year window | UTC Jan 1 |
Lowercase calendar letters (1d / 1w / 1m for month) are rejected — the lowercase m is reserved for minutes. Use the uppercase form for D / W / M / Y.
htf_resample
htf_resample(series, tf, timestamps, op="last", lookahead=false)
The core primitive. Aggregates series into HTF buckets defined by timestamps and tf, then broadcasts the per-bucket value back to chart bars.
Parameters
series— any chart-TF series (close,high, an indicator result, aniff-mask of close, …).tf— HTF string, see format table.timestamps— the built-intimeseries. Auto-handles seconds-or-ms (matches the policy inlib/time.js).op— how to combine chart bars within a bucket:opBucket value at finalize Common use "first"first finite value HTF open "last"last finite value HTF close (default) "min"min finite value HTF low "max"max finite value HTF high "sum"sum of finite values HTF volume / cumulative flow "avg"mean of finite values HTF vwap-ish (use cautiously — vwap needs weighting) lookahead—false(default) returns the LAST COMPLETED bucket's value at each chart bar.truereturns the RUNNING value of the CURRENT bucket (intra-bar). Lookahead-off is the repaint-free default.
Returns
A series of the same length as the shorter of series and timestamps. NaN at chart bars where no completed bucket exists yet (no-lookahead mode) or where the current bucket has only NaN samples (lookahead mode).
Example — daily OHLC anchored on a 1h chart
@version 1
@name "Daily OHLC anchors"
@input show_today = input.bool(true, "Show today's levels")
// Resample chart-TF OHLCV to daily buckets. Lookahead=true so the
// HIGH/LOW/OPEN visible on intraday bars within today track the
// running daily high/low/open (otherwise we'd see yesterday's
// values on every bar of today, which is the repaint-free
// default but not what you want for "today's open" overlays).
d_open = htf_resample(open, "1D", time, "first", true)
d_high = htf_resample(high, "1D", time, "max", true)
d_low = htf_resample(low, "1D", time, "min", true)
d_close = htf_resample(close, "1D", time, "last", true)
d_vol = htf_resample(volume, "1D", time, "sum", true)
plotLine(d_open, color="#888a90", width=1)
plotLine(d_high, color="#0ECB81", width=1)
plotLine(d_low, color="#F6465D", width=1)
// Daily RSI on a 1h chart — compute on the resampled close, plot
// at chart resolution. Same value across all chart bars within
// one daily bucket (Pine semantics).
daily_rsi = rsi(d_close, 14)
plotLine(daily_rsi, pane="below", color="#D4A60A")
Example — weekly support/resistance with completed buckets
@input weeks_back = input.int(4, "Weeks of S/R", minval=1, maxval=12)
// lookahead=false (default) — at each chart bar, see the LAST
// COMPLETED weekly value. Drawing the weekly high/low overlay
// on an intraday chart that NEVER repaints (the level finalises
// when the week closes and stays put on Monday's first bar
// onward).
weekly_high = htf_resample(high, "1W", time, "max")
weekly_low = htf_resample(low, "1W", time, "min")
plotLine(weekly_high, color="#0ECB81", width=2)
plotLine(weekly_low, color="#F6465D", width=2)
htf_new_bar
htf_new_bar(timestamps, tf)
Boolean series — true at every chart bar that's the FIRST bar of a new HTF bucket. Equivalent to Pine's ta.change(time(tf)) != 0. Bar 0 always returns false (no pre-history baseline).
new_day = htf_new_bar(time, "1D")
// Plot one label per daily candle — exactly one fires at the
// first chart bar of each new day.
plotLabel(time, low, iff(new_day, "•", na),
color="#FFFFFF",
size=10)
// Or as an alert gate — fire when a new daily bar opens above
// the weekly high:
weekly_high = htf_resample(high, "1W", time, "max")
alertcondition(new_day and open > weekly_high,
title="Daily open above weekly high",
kind="bullish")
htf_bucket_start
htf_bucket_start(timestamps, tf)
Series carrying the UTC ms timestamp marking the start of each chart bar's HTF bucket. Useful as an x-coordinate for lineNew / boxNew / labelNew when you want to anchor a level to the bucket boundary rather than to a chart bar.
// Today's open line that visually starts at UTC midnight, not at
// whatever 1h bar happens to be the first one inside today:
d_open = htf_resample(open, "1D", time, "first", true)
d_start = htf_bucket_start(time, "1D")
// Draw one persistent line that updates each tick. Slot=1 so the
// line PERSISTS across script runs.
lineNew(1, at(d_start, last_bar_index), at(d_open, last_bar_index),
last_bar_time, at(d_open, last_bar_index),
color="#D4A60A", width=2, dash=2, gap=4)
htf_bars_in_bucket
htf_bars_in_bucket(timestamps, tf)
Series of bar counts within the current HTF bucket — 0 at the first chart bar of a new bucket, 1 at the second, etc. Reset to 0 at every new bucket. Single-pass implementation (cheaper than barssince(htf_new_bar(...))).
// Fade the "today so far" zone — 50% alpha at the start of the
// daily bar, 8% at the end of the typical 24-bar (1h) daily.
in_day = htf_bars_in_bucket(time, "1D")
alpha = 0.50 - in_day * 0.018
alpha = max(alpha, 0.08)
plotBgColor(true, color=withAlpha("#0ECB81", alpha))
// Or wait at least 4 bars into a new daily bar before allowing
// a new entry — prevents reacting to the open spike:
new_day = htf_new_bar(time, "1D")
warmup = in_day < 4
entry_ok = longSignal and not warmup
Repaint policy
Default (lookahead=false) — repaint-free. Chart bar i shows the FINAL value of the LAST COMPLETED HTF bucket. Once a bucket closes, the value never changes — historical bars on the chart show the same value the live bar saw at that point in time. This matches Pine's barmerge.lookahead_off and is the SAFE default for any signal-generation logic.
lookahead=true — live, intra-bar. Chart bar i shows the RUNNING value of the CURRENT HTF bucket. The value at any chart bar can change as more chart bars of the current HTF bucket arrive. Useful for visual overlays ("today's high so far") but DANGEROUS for backtesting: a strategy using htf_resample(high, "1D", time, "max", lookahead=true) would see today's eventual high from the FIRST bar of today (lookahead bias).
| Mode | When to use | Repaint? |
|---|---|---|
lookahead=false | Signals, alerts, anything that drives entries/exits | No |
lookahead=true | Visual overlays for "current bucket so far", live HUDs | Yes (paints differently as bucket evolves) |
Limitations
1. Lower-TF requests give nonsense
Calling htf_resample(close, "1m", time) on a 1h chart asks for finer data than the chart has. Each chart bar maps to a single 1m bucket containing one value — the output is structurally valid but semantically meaningless. The DSL doesn't currently detect this misuse. Stick to TFs at-or-coarser-than the chart TF.
A future hardening pass may sniff the chart TF from median(timestamps[i+1] - timestamps[i]) and throw on lower-TF requests.
2. Cross-symbol (Pine request.security("BTC", ...)) — deferred to Phase 4
The DSL sandbox forbids fetch / WebSocket from script-space, so cross-symbol HTF would require BE-side fetching + symbol whitelisting + multi-tab subscription caching. None of that is done in Phase 3.1. To overlay a different symbol today:
- Add it as a separate indicator on the chart pane (UI-side).
- Wait for Phase 4 which will introduce a
cross_symbol(...)gateway with the necessary BE plumbing.
3. Calendar precision
- Week buckets are ISO Mondays. Sunday-start weeks are not supported (yet).
- Month buckets are anchored to year boundary, so
"3M"emits Q1/Q2/Q3/Q4 starts — not "rolling 3 months from now". - Year buckets are anchored to year-0, so
"5Y"groups by half-decade starting 1970/1975/1980/…
4. Timestamp gaps in the chart history
Bad timestamps (NaN, ≤0) are silently passed through as NaN in the output. The accumulator doesn't reset on a bad bar — it just doesn't add to the running stats. Practical effect: a single bad bar in the middle of a daily bucket doesn't break the daily aggregate, but a long gap can mask a missed bucket boundary if both the gap and the missing boundary share a chart bar.
Performance
| Function | Complexity | Notes |
|---|---|---|
htf_resample | O(N) | Single forward pass over chart bars. Inline Float64Array(7) accumulator — no per-bucket allocations. |
htf_new_bar | O(N) | Single pass, compares each bar's bucket to previous. |
htf_bucket_start | O(N) | Single pass + Date.UTC arithmetic per bar (only for calendar units). |
htf_bars_in_bucket | O(N) | Single pass, integer counter. |
All four are designed to stay under the HEAVY_COMPUTE_BUDGET_MS = 16ms envelope at 5000 bars (the engine's per-eval budget). For an indicator that calls htf_resample 5 times (O, H, L, C, V) on a 5000-bar chart, expect ~1ms wall-clock total.
Reference recipes
Anchored VWAP on a configurable HTF
@input tf = input.string("1D", "Anchor", options=["1D","1W","1M"])
// Anchored VWAP — sum(price * volume) / sum(volume) per bucket,
// with lookahead so the running VWAP is visible intra-bucket.
pv = hlc3 * volume
sum_pv = htf_resample(pv, tf, time, "sum", true)
sum_v = htf_resample(volume, tf, time, "sum", true)
htf_vwap = iff(sum_v > 0, sum_pv / sum_v, na)
plotLine(htf_vwap, color="#D4A60A", width=2)
"First N bars of new daily" exclusion mask
@input cooldown = input.int(4, "Cooldown bars after new day", minval=0, maxval=20)
in_day = htf_bars_in_bucket(time, "1D")
trade_ok = in_day >= cooldown
// Combine with any signal — silently skips the open-spike bars.
entries = longSignal and trade_ok
Higher-TF crossover signal
fast = ema(htf_resample(close, "1D", time, "last"), 9)
slow = ema(htf_resample(close, "1D", time, "last"), 21)
new_day = htf_new_bar(time, "1D")
golden = crossover (fast, slow) and new_day
death = crossunder(fast, slow) and new_day
alertcondition(golden, title="Daily EMA golden cross", kind="bullish")
alertcondition(death, title="Daily EMA death cross", kind="bearish")