Higher-timeframe kit
Two modules build a higher timeframe without a second data feed.
./sdk/resample folds the chart's own bars: a Resampler for the eight
timeframes the built-in indicators' Timeframe setting offers, chart to
1W, and a Bucket for any span Pine spells, "120", "2D", "2W" or
"3M". ./sdk/candles reads a candles stream instead: Periods folds
its candles into calendar periods, a CandleList keeps the newest of
them. Feed each class every chart bar. Each section writes its rules
out: how buckets are keyed, when one closes, what a read shows. The
clock, session and market-session classes beside them are the
Clock and sessions kit; the fold itself, and
the interval pin for a real coarser feed, are on
Multi-timeframe.
Which one
| Class | Module | Reads | Spans | Reaches back |
|---|---|---|---|---|
Resampler | . | the chart's own bars | the built-ins' eight timeframes: chart, 5m, 15m, 30m, 1h, 4h, 1D, 1W | the loaded history |
Bucket | . | the chart's own bars, on 24/7 markets | any span Pine spells: "120", "2h", "2D", "2W", "3M" | the loaded history |
Periods | . | a candles stream | calendar day, week, month, quarter, year | the stream's bars |
Candle | . | a candles stream | the newest N closed candles of the stream's timeframe | the stream's bars |
From the chart bars: Resampler
The ./sdk/resample module builds a higher timeframe from the chart's own
bars, the way the built-in indicators' Timeframe setting does: a 20-bar
average at 1h on a 5m chart averages twenty closed 1h candles, never twenty
5m bars. Feed every chart bar to a Resampler; when a selected bar closes,
push its value into a ClosedWindow or commit it to a Smoothed; then read
either the closed bars alone (confirmed) or the closed bars plus the forming
one as a trial step that is never stored (developing).
| Export | Signature | Answers |
|---|---|---|
Resampler | new Resampler( | the selected timeframe, numbered like the built-ins' Timeframe setting: tf.CHART, tf.FIVE_MINUTES, tf., tf., tf.HOUR, tf.FOUR_HOURS, tf.DAY, tf.WEEK (0..7); any other number aborts with Resampler: unknown timeframe <x> |
update | r.update(t, tradeDate, open, high, low, close, volume): void | folds one chart bar: t is bar.time(), tradeDate is time.trade_date; call once per bar |
closed | r.closed(): bool | a selected bar closed on this chart bar, and r.last is its candle |
newBucket | r.newBucket(): bool | this chart bar starts a selected bar |
last, forming | r.last.close, r.forming.high | the last closed candle and the forming one: open, high, low, close, volume and startSec, NaN until each exists |
source | r.source(which: i32, forming: bool): f64 | one price of either candle: field.CLOSE, field.OPEN, field.HIGH or field.LOW, numbered like the built-ins' Source setting |
confirmed | r.confirmed(): bool | read the committed state alone: waitForClose is on, or the timeframe is the chart's |
refused | r.refused(): bool | your indicator must write NaN to every output while this is true (nothing is blanked for you): the selected bar is narrower than the chart's bars, or a 1D or 1W bar came without a trade date |
widthSec, prevStartSec | r.widthSec(): f64 | the selected bar's width in seconds; the start of the selected bar before the forming one |
ClosedWindow | new Closed | the last cap closed values: push(x), count(), back(n, x, withX), then mean, wma, stdev, highest, lowest, highestOffset, lowestOffset and sum, each (p, x, withX), and vwma(volumes, p, x, v, withX) |
Smoothed | new Smoothed(kind: string, period: i32) | an "ema" or "rma" over closed values: commit(x), value(), peek(x) (one more step, not stored) and copyFrom(other) for a scratch copy |
The rules, written out:
- Buckets. 5m to 4h are rolling,
floor(t / width)from the epoch, on every market. 1D is the exchange trade date and 1W a Monday week of trade dates, both read fromtime.trade_date: a CME bar from the 17:00 Chicago open already belongs to the next day's candle, and a Sunday-evening bar to Monday's week. - Closing. A selected bar closes on the first chart bar of the next
one, and a gap simply skips the empty buckets. Its candle holds the first
bar's open and the last bar's close as those bars gave them, the highest
high, the lowest low and the summed volume; a
NaNhigh, low or volume on any bar carries into that field. A candle whose last bar closed atNaNnever closes, while aNaNclose on an earlier bar does not matter. - Confirmed and developing. Confirmed: every chart bar inside selected
bar k shows the state after bar k - 1, so a value never changes once it
is drawn. Developing: that state plus one step on the forming candle,
which is
withX = truewith the forming value on a window andpeekon a smoother. - Warm-up counts closed selected bars: a 20-bar average at 1h needs 20 closed hours inside the loaded history (19 and the forming one when developing). The first bucket may be partial; it still closes and counts.
- Refusal.
refused()blanks nothing by itself: while it is true, your indicator must writeNaNto every output, as the sample below does. TheResamplerkeeps folding underneath, so its candles, windows and smoothers still answer and would draw numbers that mean nothing. It turns true when the selected bar is narrower than the chart's bars (known from the second bar on, once the bar spacing is measured), and stays true from a 1D or 1W bar without a trade date on.
A moving average and an EMA of the selected timeframe's closes, with the built-ins' Timeframe and Wait for close settings. The default reads 1h on a 1h chart or a finer one, and refuses on a coarser one:
param("timeframe", 4, { min: 0, max: 7, description: "0 chart, 1 5m, 2 15m, 3 30m, 4 1h, 5 4h, 6 1D, 7 1W" });
param("period", 20, { min: 1, max: 200, description: "Length in bars of the selected timeframe" });
param("wait_for_close", 1, { min: 0, max: 1, description: "1 confirmed values, 0 developing values" });
input("close", ohlcv.close);
input("trade_date", time.trade_date);
output("htf_sma", line, overlay, { color: "#2563eb", width: 2, description: "Average of the selected timeframe's closes" });
output("htf_ema", line, overlay, { color: "#f59e0b", width: 2, description: "EMA of the selected timeframe's closes" });
let r = new Resampler(tf.HOUR, true);
let closes = new ClosedWindow(20);
let ema = new Smoothed("ema", 20);
let period: i32 = 20;
function onStart(): void {
// Constructed here so a bad setting reports in the Console.
period = i32(p_period());
r = new Resampler(i32(p_timeframe()), p_wait_for_close() >= 0.5);
closes = new ClosedWindow(period);
ema = new Smoothed("ema", period);
}
function onBar(): void {
r.update(bar.time(), in_trade_date(), bar.open(), bar.high(), bar.low(), bar.close(), bar.volume());
if (r.closed()) {
// A selected bar just closed: its candle is final, so it joins the state.
closes.push(r.last.close);
ema.commit(r.last.close);
}
// Developing reads add the forming bar as one trial step, never stored.
const live = !r.confirmed();
let sma = closes.mean(period, r.forming.close, live);
let smoothed = live ? ema.peek(r.forming.close) : ema.value();
if (r.refused()) {
sma = NaN;
smoothed = NaN;
}
out_htf_sma(sma);
out_htf_ema(smoothed);
}Confirmed, both lines step once per closed hour and hold between; with Wait for close off they follow the forming hour on every chart bar. A chain reads the same way: a signal line over a closed MACD commits each closed MACD value, and its developing read peeks with the trial MACD.
Pine equivalents
| Pine | Here |
|---|---|
request.: the last closed hour, never repainting | new Resampler( and closes.mean(20) over a ClosedWindow pushed on closed() |
request. on the realtime bar: the forming hour | new Resampler( and ema. |
barstate. before using a higher-timeframe value | confirmed reads (waitForClose on), which only ever show closed selected bars |
timeframe. | r.newBucket() |
request. | r.last.close on tf.DAY: the exchange trade date's candle |
Any span: Bucket
A Bucket folds the chart's own bars into a span the chart does not
serve, keyed the way TradingView aligns a multi-period timeframe, so
request.security(syminfo.tickerid, "120", close) and its "2D", "2W"
and "3M" cousins port without a pinned input. The constructor takes the
span and the chart's bar interval in seconds: declare
chart.interval_sec(); at top level and pass p_chart_interval_sec()
from onStart(), as the sample below does, so the module knows where
each bar closes instead of guessing from the gaps between bars. The span
is Pine's timeframe string plus the kit's own letters: digits alone or
with m are minutes ("120", "45", "5m"), s seconds, h or H
hours ("2h"), d or D days ("2D"; a bare "D" is one), w or
W weeks ("2W") and M months ("3M"): the capital M is months as in
Pine, the small m minutes as in the kit's interval pins. Anything else, a
count of 0 or an intraday span past a day aborts with
Bucket: bad span <x>, so construct in onStart().
| Export | Signature | Answers |
|---|---|---|
Bucket | new Bucket(spec: string, chart | buckets of one span over the chart's bars, whose interval is p_ |
update | b.update(t, open, high, low, close, volume): void | folds one chart bar: t is bar.time(); call once per bar |
closed, newBucket | b.closed(): bool | a bucket closed on this chart bar (b.last is its candle); this chart bar starts a bucket |
isLastBar | b.isLastBar(): bool | this chart bar's close (its open plus the chart interval) reaches the bucket's end: the bucket's last bar, where b.forming already holds its final candle |
complete | b.complete(): Candle | the newest bucket complete at this bar's close: forming on the bucket's last bar, last on every other bar; what Pine's request. shows on historical bars with lookahead off |
last, forming | b.last.close, b.forming.high | the last closed bucket and the forming one: open, high, low, close, volume and startSec, NaN until each exists |
startSec, endSec, widthSec, lastEndSec | b.endSec(): f64 | the forming bucket's bounds in epoch seconds and its width (shorter than the span on the last bucket of a day or year); the last closed bucket's end |
lastWhole, formingWhole | b.lastWhole(): bool | the candle's bucket started at or after the first bar fed, so it holds every bar of the bucket (the first bucket may have started before the history) |
refused | b.refused(): bool | the chart's bars cannot build the span (the rule below); your indicator must write NaN to every output while it is true |
refusedReason | b. | why, as readable text (Bucket: span 45 is not a whole multiple of the chart's 1800s bars); the empty string while the span is served |
reset | b.reset(): void | back to the freshly constructed state; the span, the interval and a refusal decided at construction stay, a straddle refusal clears |
The rules, written out (TradingView's bar alignment on a 24/7 market):
- The chart interval decides what the bars can build. The span is
refused from the first bar, with its reason, when the interval is
unknown (
chart.interval_sec()reads0; every place an indicator runs fills it today), when the span is narrower than the bars, or when it is not a whole multiple of them:"45"on a 30-minute chart would put the 00:30 bar astride 00:45, a candle Pine serves from finer data the chart does not have (a month counts as 28 days for both tests). Where the bars' own grid cannot meet a boundary, the first bar that straddles one (hourly bars opening on the half hour under"2h", a month boundary that is not a Monday on a weekly chart) refuses the span from that bar on, untilreset(). A monthly chart's bars have no fixed width, so aBucketrefuses them. - Intraday spans count from 00:00 UTC of each day, so
"2h","3h","4h","6h","8h"and"12h"are the epoch floor theResampleruses, while a span that does not divide the day ("7h","50") ends its last bucket of the day short, at midnight. - Day spans count calendar days from January 1st and restart every
year: on
"3D"the bucket after 2026-12-30 is two days long, and 2027-01-01 starts a new one. - Week spans count Monday weeks from the year's first Monday, and the
days before that Monday belong to the last bucket of the year before:
2027 starts on a Friday, so January 1st to 3rd 2027 sit in the
"2W"bucket that began on Monday 2026-12-21. - Month spans count from January and restart every year:
"3M"is the quarter, and a"5M"year ends with a two-month bucket. - Closing is the
Resampler's rule: a bucket closes on the first chart bar of a later bucket, when its last bar's close is finite, andb.lastis never revised; gaps skip empty buckets; the first bucket may be partial, andlastWhole()says so. - Sessions are not modelled: Pine anchors its intraday buckets at a
venue's session open and counts trading days only, which differs from
the UTC day on a session venue, and a session-anchored grid (bars
opening on the half hour) refuses as a straddle. Use a
Bucketon 24/7 markets.
The close, high and low of the newest two-hour bucket complete at each
bar, the way request.security(syminfo.tickerid, "120", close) shows
them on historical bars, with the span as a setting and the chart's bar
interval passed in:
param("hours", 2, { min: 1, max: 24, description: "Hours per bucket" });
chart.interval_sec();
output("htf_close", line, overlay, { color: "#2563eb", width: 2, description: "Close of the newest bucket complete at this bar" });
output("htf_high", line, overlay, { color: "#16a34a", description: "Its high" });
output("htf_low", line, overlay, { color: "#dc2626", description: "Its low" });
output("bucket_start", none, overlay, { description: "1 on the bar that starts a bucket" });
let b = new Bucket("2h", 0.0);
function onStart(): void {
// Built here from the setting and the chart's interval: a bad span aborts
// with a readable message, a span the bars cannot build refuses with one.
b = new Bucket(i32(p_hours()).toString() + "h", p_chart_interval_sec());
}
function onBar(): void {
b.update(bar.time(), bar.open(), bar.high(), bar.low(), bar.close(), bar.volume());
// complete() is the forming bucket on its last bar and the last closed one otherwise.
const c = b.complete();
const blank = b.refused() || isNaN(c.close);
out_htf_close(blank ? NaN : c.close);
out_htf_high(blank ? NaN : c.high);
out_htf_low(blank ? NaN : c.low);
out_bucket_start(b.newBucket() ? 1.0 : 0.0);
}b.last alone is Pine's request.security(..., close[1], lookahead = barmerge.lookahead_on), the previous bucket on every bar of the current
one, and b.forming its repainting reading of the realtime bar. A
ClosedWindow or a Smoothed pushed on b.closed() gives an indicator
on the span, exactly as with a Resampler. On a chart whose bars cannot
build the span (a 3-hour setting on a 2-hour chart) every output reads
NaN and b.refusedReason() says why; show it in a label or a HUD when
the port should explain itself.
| Pine | Here |
|---|---|
request. on historical bars | b. with new Bucket("120", p_ |
request. | b.last.high with new Bucket("2D", p_ |
request. on the realtime bar | b.forming.close with new Bucket("2W", p_ |
timeframe. | b.newBucket() |
Calendar periods and candle lists
The ./sdk/candles module reads a candles stream, the closed candles of
one timeframe reaching back bars deep
(Multi-timeframe).
A Periods folds the candles into calendar periods; a CandleList keeps
the newest of them. Feed each one every bar, an empty block included,
with load(in_<name>_view(), in_<name>_cells(), bar.time()): the bar
moves the clock even when no candle arrives. Construct them at module
start when the period is a constant, or in onStart() when a setting
picks it.
| Export | Signature | Answers |
|---|---|---|
period | period.DAY, period.WEEK, period.MONTH, period.QUARTER, period.YEAR | the period a Periods folds, numbered 0..4 so a choice setting's index passes straight through |
Periods | new Periods(kind: i32, keep: i32 = 1, leg: i32 = period.DAY) | calendar periods of one kind, keeping the newest keep closed ones readable; leg is the stream's interval, period.WEEK for a 1w stream and period.DAY for 1d or finer; an unknown kind aborts with Periods: unknown period <x>, any other leg with Periods: a stream's leg is period.DAY (1d or finer) or period.WEEK (1w), not <x> |
load | p.load(cells: Static | moves the clock to the bar's open t, then folds this bar's block; returns the candles taken |
confirmed | p.confirmed(n: i32 = 0): PeriodCandle | the n-th newest closed period, 0 the last one; all NaN for an n past keep - 1 or before n + 1 periods have closed |
developing | p.developing(): PeriodCandle | the bar's own period, folded from the candles closed so far |
isNew, startSec, endSec, complete | p.isNew(): bool | the bar starts a new period; the period's bounds in epoch seconds; whether the stream reaches back to its start |
add, at, reset | p.add(t, open, high, low, close, volume): bool | folds one candle by hand; moves the clock to a bar opening at t (call it before that bar's add calls, as load does); back to the constructed state |
PeriodCandle | open, high, low, close, volume, startSec, endSec, pv, pvVolume, count; vwap() | one period: the first open, the highest high, the lowest low, the last close, the summed volume, and its VWAP sums |
CandleList | new CandleList(cap: i32) | the newest cap closed candles of the stream |
load, count | list., list.count(): i32 | keeps this bar's candles; how many it holds, at most cap |
openSec, open, high, low, close, volume | list.high(i: i32): f64 | candle i, 0 the oldest kept and count() - 1 the newest; NaN outside the list |
The rules, written out:
- Buckets. Every boundary is 00:00 UTC: a week starts on Monday, a month on the 1st, a quarter on January, April, July or October 1st, a year on January 1st. A candle belongs to the period holding its open, and a daily candle of a market with sessions is stamped with its trade date.
- Confirmed. A period is confirmed on every bar that opens at or after
its end. All of its candles have arrived by then, so a confirmed period
never changes.
developing()never holds the forming candle, so it readsNaNuntil the period's first candle closes. - Never partial. A period that began before the stream's first candle
reads
NaNfromconfirmed()anddeveloping(), andcomplete()says whether the bar's own period is whole. Ask for enoughbarsto reach the start of every period you read: 366 daily candles always hold the current year. - Any bar width. On a monthly or yearly chart one bar carries a month
or a year of candles.
Periodskeeps every period a read can still reach, thekeepnewest closed ones and the bar's own, whatever a bar spans, in the memory it allocated at construction. - Weekly streams. Construct a
Periodsfed a1wstream withperiod.WEEKas its third argument. Its clock then runs on the start of the bar's week, so a week that straddles a month, quarter or year boundary counts in the period it opens in, as calendar requests over weekly candles count it. The stream's interval is declared, never read from the candles, so a1dstream with days missing keeps its daily clock. Feed a1dstream for exact periods. - The list. On the newest bar, a stream declared
bars: Nread throughCandleList(N)holds the newest N closed candles: the rowsrequestBars(sym, tf, { bars: N + 1 })returns, without its last, the forming candle. That one is the interval'sformingview, folded from the chart's own bars. On an earlier bar the list holds the candles delivered through that bar, so it serves per-bar math as well as drawing.
Last month's high and low, and the average close of the last 20 closed days, on any chart interval:
input("close", ohlcv.close);
input("d", candles.cells, { interval: "1d", bars: 70, description: "The newest 70 closed days" });
output("pm_high", line, overlay, { color: "#16a34a", description: "Last month's high" });
output("pm_low", line, overlay, { color: "#dc2626", description: "Last month's low" });
output("avg20", line, overlay, { color: "#2563eb", description: "Average close of the last 20 closed days" });
const month = new Periods(period.MONTH);
const days = new CandleList(20);
function onBar(): void {
const block = in_d_view();
const n = in_d_cells();
const t = bar.time();
month.load(block, n, t);
days.load(block, n, t);
const last = month.confirmed(0); // NaN until a whole month has closed inside the stream
out_pm_high(last.high);
out_pm_low(last.low);
let avg: f64 = NaN;
if (days.count() == 20) {
let sum = 0.0;
for (let i = 0; i < 20; i++) sum += days.close(i);
avg = sum / 20.0;
}
out_avg20(avg);
}bars: 70 reaches back past the start of last month on every day of the
year, so even a short chart shows it whole. On a chart that loaded more
than 70 days, the stream starts just before the first loaded bar instead,
and the first month or two read NaN: a month that began before the
stream is never shown partial.