---
title: "Multi-timeframe"
description: "Read higher timeframes from any Indicator: on the chart, a bucket fold on time.bar_open_sec that confirms a candle only after it closes; on your machine, an…"
order: 20
section: "core-concepts"
---

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

# Multi-timeframe

Read higher timeframes from any Indicator: on the chart, a bucket fold on
`time.bar_open_sec` that confirms a candle only after it closes; on your
machine, an `interval` pin that reads a real coarser feed as of its close.
Rolling buckets, calendar buckets (day, Monday week, month, quarter, year),
confirmed and developing values, and the offset-N form all fall out of the
same integer math. kScript (legacy) covered this with `htf()`, `ltf()`, and
`request()`; the first two Indicator forms replace `htf()` and the coarser
`request()`, and `ltf()` has no counterpart yet.

## Introduction

An Indicator is not locked to its chart's timeframe. From a 1h chart you
can read the 4h trend, pull a daily level, or reset a sum on the Monday
open. Two forms cover it:

- **The bucket fold** (any host): bucket bars by their open time, keep the
  running bucket's values, and fold a bucket into the higher-timeframe
  statistic only when the next bucket's first bar arrives. This is
  `htf(source, timeframe)` written out, and it is what every input on the
  chart uses, since the chart reads its own interval for every input.
- **The `interval` pin** (your machine): `input("h4", ohlcv.close, {
  interval: "4h" })` reads a real 4h feed, and the host contributes each
  4h candle only as of its close. This is `request(symbol, "4h")` with the
  no-repaint discipline built into the data path.

The headline property is what neither form does: it never repaints.

## The bucket fold is confirmed by default

In most charting languages a higher-timeframe lookup is a repaint trap: you
ask for the 4h close on a 1h chart and, while the 4h candle is still
forming, get its live, not-yet-final value. Your signal looks perfect in
backtest and fires a bar early in production, because history got a value
the live chart never had.

The fold closes that trap. The 4h reading on any 1h bar uses only the 4h
candles that had already closed by that bar: the module remembers the last
close it saw in the current bucket, and when a bar from the NEXT bucket
arrives it folds that remembered close into the 4h statistic. The value you
see in history is the value the module saw live. The 4h regime filter from
the kScript page, on any chart interval:

```typescript
import { input, line, none, ohlcv, output, overlay, param, shape, time } from "./sdk/declare";
import { in_bar_t, in_close, in_high, in_low } from "./gen/inputs";
import { emitRow, out_bull_dot, out_h4_close, out_h4_ema, out_is_bearish, out_is_bullish, out_regime, out_bear_dot } from "./gen/outputs";
import { p_ema_len } from "./gen/params";
import { Ema } from "./sdk/ta";

param("ema_len", 20, { min: 2, max: 200, description: "EMA length in 4h candles" });
input("close", ohlcv.close);
input("high", ohlcv.high);
input("low", ohlcv.low);
input("bar_t", time.bar_open_sec);
output("h4_close", line, overlay, { color: "#7c3aed", width: 1, description: "Confirmed 4h close" });
output("h4_ema", line, overlay, { color: "#2563eb", width: 2, description: "EMA of confirmed 4h closes: the trend baseline" });
output("regime", none, overlay, { description: "1 when the confirmed 4h close is above its 4h EMA, 0 otherwise" });
output("bull_dot", shape, overlay, { color: "#16a34a", shape_where: "is_bullish", description: "Bullish 4h regime" });
output("bear_dot", shape, overlay, { color: "#dc2626", shape_where: "is_bearish", description: "Bearish 4h regime" });
output("is_bullish", none);
output("is_bearish", none);

const H4: f64 = 14400.0; // 4h in seconds; buckets are floored from the epoch, like a rolling "4h" token
let ema = new Ema(20);
let bucket: f64 = NaN;
let bucketClose: f64 = NaN; // the running bucket's latest close
let h4Close: f64 = NaN; // the last CLOSED bucket's close
let h4Ema: f64 = NaN;
let high: f64 = NaN;
let low: f64 = NaN;
let regime: f64 = 0.0;

export function init(): void {
  ema = new Ema(i32(p_ema_len()));
}

export function state(): i32 {
  const close = in_close();
  high = in_high();
  low = in_low();
  const b = Math.floor(in_bar_t() / H4);
  if (b != bucket) {
    // The bucket changed: what we remembered is a CLOSED 4h candle. Fold it now, never earlier.
    if (!isNaN(bucketClose)) {
      h4Close = bucketClose;
      h4Ema = ema.update(bucketClose);
    }
    bucket = b;
  }
  bucketClose = close;
  regime = isNaN(h4Ema) ? 0.0 : h4Close > h4Ema ? 1.0 : 0.0;
  return isNaN(h4Ema) ? 0 : 1;
}

export function finalize(): void {
  out_h4_close(h4Close);
  out_h4_ema(h4Ema);
  out_regime(regime);
  out_bull_dot(low);
  out_bear_dot(high);
  out_is_bullish(regime == 1.0 ? 1.0 : 0.0);
  out_is_bearish(regime == 0.0 ? 1.0 : 0.0);
  emitRow();
}

export function reset(): void {
  ema.reset();
  bucket = NaN;
  bucketClose = NaN;
  h4Close = NaN;
  h4Ema = NaN;
  high = NaN;
  low = NaN;
  regime = 0.0;
}
```

The 4h EMA only steps when a 4h candle closes, so it draws as a staircase
across four 1h bars. That flat-then-step shape is the visual signature of a
correct, confirmed higher timeframe (`repainting.md`). The fold works on
any chart interval that divides the bucket: a 15m chart contributes sixteen
bars per bucket, a 1h chart four, and a 4h chart one (the fold then
confirms each bar on the next).

### When you do want the live value

Sometimes you genuinely want the forming bucket, for example a live 4h
close ticking inside the current period. In a fold that value is already in
your hands: it is `bucketClose`, the running variable. Write it to an
output of its own, label it as developing, and never build a signal on it:
developing values change as the bucket fills, so a cross built on them will
not reproduce. Confirmed is what you get by reading the folded variable
instead.

### Offset N: the bucket before that

kScript's `offset: 1` read the completed period before the most recent
one. In a fold that is a small ring of closed-bucket values: keep the last
few `h4Close` values in a `StaticArray<f64>` you rotate at each bucket
change, and read entry `1` for one bucket back. The ring is the history
kScript kept for you (`core-variables.md`).

## Timeframe tokens: rolling versus calendar

kScript's timeframe strings came in two flavours, and the difference was
where the bucket boundaries fell. Both are one line of integer math over
the bar's open time in epoch seconds:

**Rolling buckets** are `N` seconds wide, floored from the Unix epoch:
`Math.floor(bar_t / N)`. `"4h"` is `N = 14400`, `"1d"` is `N = 86400`
(and its buckets start at 00:00 UTC purely because that is where epoch days
fall), `"7d"` is `N = 604800`, whose week boundaries land on Thursdays.

**Calendar buckets** anchor to real UTC calendar boundaries:

| Token | Bucket index | UTC anchor |
| --- | --- | --- |
| `"1D"` | `dayIndex = floor(bar_t / 86400)` | `00:00` |
| `"1W"` | `floor((dayIndex + 3) / 7)` | **Monday** `00:00` (epoch day 0 was a Thursday; `+ 3` shifts the week start) |
| `"1M"` | `civilYear * 12 + civilMonth` | the first of the month |
| `"1Q"` | `civilYear * 4 + (civilMonth - 1) / 3` | Jan / Apr / Jul / Oct 1 |
| `"1Y"` | `civilYear` | Jan 1 |

`civilYear` and `civilMonth` come from the days-to-civil function in
`time-and-sessions.md`. A Monday-anchored `"1W"` and a rolling `"7d"`
produce **different** closes over the same chart, because their week
boundaries fall on different days; write the one you mean. Multi-count
calendar tokens (`"2W"`, `"3M"`) are just a different index (`weekIndex /
2`), so there is nothing to reject.

The cookbook's anchored VWAP and key levels are day and Monday-week folds;
the regime filter above is a rolling 4h fold.

## The `interval` pin: a real coarser feed, on your machine

On your machine the second form is available, and it is the one that also
reaches **another symbol**: an input pinned to a market and a coarser
interval reads that feed natively, and the host step-projects it onto the
primary grid as of each candle's close.

```typescript
import { input, line, lower, ohlcv, output, overlay } from "./sdk/declare";
import { in_close, in_daily, in_eth_4h } from "./gen/inputs";
import { emitRow, out_daily, out_eth_4h, out_ratio_to_daily } from "./gen/outputs";

// The primary: the selector's own market and interval.
input("close", ohlcv.close);
// A coarser interval of the same market: each daily candle arrives as of its close.
input("daily", ohlcv.close, { interval: "1d", description: "Prior daily close, as of close" });
// Another symbol at a coarser interval: the request() form, pinned by symbol and exchange together.
input("eth_4h", ohlcv.close, { symbol: "ETHUSDT", exchange: "BINANCE_FUTURES", interval: "4h", description: "ETH 4h close, as of close" });
output("daily", line, overlay, { color: "#f59e0b", description: "Daily close, stepping once per day" });
output("eth_4h", line, lower, { color: "#7c3aed", description: "ETH 4h close" });
output("ratio_to_daily", line, lower, { color: "#2563eb", unit: "%", description: "This bar's close relative to the last daily close" });

let close: f64 = NaN;
let daily: f64 = NaN;
let eth: f64 = NaN;

export function init(): void {}

export function state(): i32 {
  close = in_close();
  daily = in_daily();
  eth = in_eth_4h();
  return isNaN(daily) || isNaN(eth) ? 0 : 1;
}

export function finalize(): void {
  out_daily(daily);
  out_eth_4h(eth);
  out_ratio_to_daily(daily == 0.0 ? NaN : ((close - daily) / daily) * 100.0);
  emitRow();
}

export function reset(): void {
  close = NaN;
  daily = NaN;
  eth = NaN;
}
```

Install it and read it on any market; the pinned inputs keep reading ETH
and the daily feed while the primary follows `--symbol`:

```bash
om wrun install ./mtf-pinned --replace
om metric series --metric wrun/@you/mtf-pinned/ratio_to_daily --symbol BTCUSDT --exchange BINANCE_FUTURES --interval 1h --bars 72
```

What a coarse pin costs: the fetch widens by two source intervals, and the
value steps once per source candle. A sparse coarse observation carries the
latest CLOSED observation forward; equal-or-finer sources align by bar
open, row for row. The interval vocabulary is `1m`, `5m`, `15m`, `30m`,
`1h`, `4h`, `1d`, `1w` (`FOUR_HOURS` and the other long names are accepted
too).

**Not in Indicators yet.** The chart does not honor `symbol`, `exchange`,
or `interval` pins: on the chart every input reads the chart's own market
and interval, so the file above computes the daily and ETH inputs from the
chart's own candles. A package that pins is a package for your machine
(`om wrun install`, then `om metric series` or a watch condition); on the
chart, use the bucket fold.

## Lower timeframes

**Not in Indicators yet.** kScript's `ltf(interval)` attached the finer bars
inside each chart bar as cells. There is no lower-timeframe celled class:
the celled classes slice the same bar by price (`volume_profile`), level
(`book`), or live print (`tape`, daemon only; `data-sources.md`). Intrabar detail that a
footprint expresses (buy versus sell volume at each price, the point of
control) is available through `volume_profile`; a finer-bar OHLC breakdown
of a chart bar is not.

## What the fold can see

A fold is computed from the bars the host loaded: a bucket already running
when the loaded history starts is incomplete, so keep its statistic `NaN`
until the first boundary passes (the cookbook's anchored VWAP does this).
On your machine an `interval` pin fetches its own window at the source's
interval, so a daily level resolves on a 1m grid without loading a day of
minutes.

## Boundaries

An `interval` finer than the primary is accepted by the schema and resolved
at evaluation; a coarser pin on a non-primary `metric` composition source is
refused by name at the sheet, since the inner value would be read before its
bar closed. On the chart a pin is not an error, it is ignored, and a hosted
alert on a pinned package says so (`This Indicator is pinned to a different
interval than the chart.`). Test a new fold by writing its bucket index to a
data-only output and reading it back with `om metric series`.
