---
title: "Higher-timeframe kit"
description: "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…"
order: 54
section: "functions"
---

<!-- source: docs/indicators/functions/higher-timeframe-kit.md; generated by packages/cli/scripts/gen-indicator-docs.ts, do not edit -->

# 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](time-and-sessions-kit.md); the fold itself, and
the `interval` pin for a real coarser feed, are on
[Multi-timeframe](../core-concepts/multi-timeframe.md).

## Which one

| Class | Module | Reads | Spans | Reaches back |
| --- | --- | --- | --- | --- |
| `Resampler` | `./sdk/resample` | the chart's own bars | the built-ins' eight timeframes: chart, 5m, 15m, 30m, 1h, 4h, 1D, 1W | the loaded history |
| `Bucket` | `./sdk/resample` | the chart's own bars, on 24/7 markets | any span Pine spells: `"120"`, `"2h"`, `"2D"`, `"2W"`, `"3M"` | the loaded history |
| `Periods` | `./sdk/candles` | a `candles` stream | calendar day, week, month, quarter, year | the stream's `bars` |
| `CandleList` | `./sdk/candles` | 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(option: i32, waitForClose: bool = true)` | the selected timeframe, numbered like the built-ins' Timeframe setting: `tf.CHART`, `tf.FIVE_MINUTES`, `tf.FIFTEEN_MINUTES`, `tf.THIRTY_MINUTES`, `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 ClosedWindow(cap: i32, keepNa: bool = false)` | 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 from `time.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 `NaN` high, low or volume on
  any bar carries into that field. A candle whose last bar closed at `NaN`
  never closes, while a `NaN` close 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 = true` with the forming value on a window and `peek` on
  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 write `NaN` to every output, as the sample below does. The
  `Resampler` keeps 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:

```typescript sample=fn-clock-kit-higher-timeframe
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.security(syminfo.tickerid, "60", ta.sma(close, 20)[1], lookahead = barmerge.lookahead_on)`: the last closed hour, never repainting | `new Resampler(tf.HOUR, true)` and `closes.mean(20)` over a `ClosedWindow` pushed on `closed()` |
| `request.security(syminfo.tickerid, "60", ta.ema(close, 20))` on the realtime bar: the forming hour | `new Resampler(tf.HOUR, false)` and `ema.peek(r.forming.close)` |
| `barstate.isconfirmed` before using a higher-timeframe value | confirmed reads (`waitForClose` on), which only ever show closed selected bars |
| `timeframe.change("60")` | `r.newBucket()` |
| `request.security(syminfo.tickerid, "D", close[1], lookahead = barmerge.lookahead_on)` | `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, chartIntervalSec: f64)` | buckets of one span over the chart's bars, whose interval is `p_chart_interval_sec()` |
| `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.security` 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.refusedReason(): string` | 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()` reads `0`; 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, until `reset()`. A
  monthly chart's bars have no fixed width, so a `Bucket` refuses them.
- **Intraday spans** count from 00:00 UTC of each day, so `"2h"`, `"3h"`,
  `"4h"`, `"6h"`, `"8h"` and `"12h"` are the epoch floor the `Resampler`
  uses, 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, and `b.last`
  is never revised; gaps skip empty buckets; the first bucket may be
  partial, and `lastWhole()` 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 `Bucket` on 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:

```typescript sample=fn-clock-kit-bucket
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.security(syminfo.tickerid, "120", close)` on historical bars | `b.complete().close` with `new Bucket("120", p_chart_interval_sec())` |
| `request.security(syminfo.tickerid, "2D", high[1], lookahead = barmerge.lookahead_on)` | `b.last.high` with `new Bucket("2D", p_chart_interval_sec())` |
| `request.security(syminfo.tickerid, "2W", close)` on the realtime bar | `b.forming.close` with `new Bucket("2W", p_chart_interval_sec())` |
| `timeframe.change("120")` | `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](../core-concepts/multi-timeframe.md#history-the-candles-stream)).
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: StaticArray<f64>, count: i32, t: f64): i32` | 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.load(cells, count, t): i32`, `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
  reads `NaN` until the period's first candle closes.
- **Never partial.** A period that began before the stream's first candle
  reads `NaN` from `confirmed()` and `developing()`, and `complete()` says
  whether the bar's own period is whole. Ask for enough `bars` to 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. `Periods` keeps every period a read can still
  reach, the `keep` newest closed ones and the bar's own, whatever a bar
  spans, in the memory it allocated at construction.
- **Weekly streams.** Construct a `Periods` fed a `1w` stream with
  `period.WEEK` as 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 a `1d` stream with days missing keeps its daily
  clock. Feed a `1d` stream for exact periods.
- **The list.** On the newest bar, a stream declared `bars: N` read
  through `CandleList(N)` holds the newest N closed candles: the rows
  `requestBars(sym, tf, { bars: N + 1 })` returns, without its last, the
  forming candle. That one is the interval's `forming` view, 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:

```typescript sample=fn-candles-kit-prior-month
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.
