ChartnautDocs

Warmup and memory

Warmup is how far back your script's memory reaches. Declare it and Chartnaut walks that much history before every value it keeps, so each value has converged and does not depend on where your chart started loading. This page covers what warmup guarantees, how to declare it, how it resolves on each timeframe, how much your plan allows and how to tell when you declared too little.

What warmup guarantees

With a warmup of L on a timeframe, every bar's value is computed from a walk that starts at least L buckets before it, from a fixed point on the time grid (Seams and anchors explains the grid). So:

  • A value never depends on how much history your chart loaded, on a reload, or on scrolling.

  • The chart and a run over history start each bar's memory from the same point and compute the same value, whether that history is computed in one pass or in chunks.

  • Every timeframe the script reads resolves its own count from the same declaration. A count such as warmup(1000) is 1,000 buckets on each of them. A length of time such as w.day() is one day on each of them: 288 buckets on 5m, 24 on 1h.

A script that keeps state and declares no warmup is refused by the lint (window/warmup-required), because its values would depend on where the run began. A script that keeps no state, such as a plot of ctx.volume, declares nothing.

Declaring it

Call warmup(...) once, at the top level, after your inputs. It can read input handles, so the depth follows the settings a reader picks.

const length = input.number({ id: "length", label: "Length", default: 14, min: 1 });
warmup((w) => w.wilder(length));   // RSI: 50x the length

It takes a plain number or a function of w, the builder:

Declaration

What it means

warmup(500)

A constant count of bars, taken as written

w.window(length)

A lookback of length bars: SMA, Donchian, a rolling percentile. The same as w.of(length)

w.ema(length)

EMA convergence, 30x the length. The same as w.of(length).times(30)

w.wilder(length)

Wilder smoothing (RSI, ATR, ADX, RMA), 50x the length

w.span("1d")

A length of time. Units are m, h, d and w: "90m", "4h", "3d", "2w"

w.day()

One day, the same as w.span("1d")

w.max(a, b)

The larger of two needs: independent pieces of state

w.sum(a, b)

Stages that feed each other add up: an EMA of an EMA, MACD's signal

w.when(mode).is("daily").then(a).otherwise(b)

A depth that depends on a select or a toggle

w.forever()

State that never forgets: a running sum, a count, an all-time high

w.of(handle).times(k)

A setting scaled by your own factor, such as 0.5 for a script that smooths over half its period

w.range(handle, "hi")

One end of an input.range

w.maxOf(handle), w.sumOf(handle)

The largest or the total of a multi-select, or of one field across an array input's rows: w.maxOf(bands, "period")

Terms combine. A script with a daily reset and an RSI declares warmup((w) => w.max(w.day(), w.wilder(length))). MACD stacks its signal on its slow EMA: warmup((w) => w.sum(w.ema(slow), w.ema(signal))). VWAP picks by its anchor setting:

const calculationMode = input.select({
  id: "calculationMode", label: "Reset Time", default: "daily",
  options: [
    { value: "consistent", label: "Remain Consistent" },
    { value: "daily", label: "Reset Daily" },
    { value: "weekly", label: "Reset Weekly" },
    { value: "monthly", label: "Reset Monthly" },
    { value: "london", label: "Reset at London Session Open" },
  ],
});
warmup((w) => w.when(calculationMode)
  .is("consistent").then(w.forever())
  .is("weekly").then(w.span("7d"))
  .is("monthly").then(w.span("31d"))
  .otherwise(w.day()));

The declaration is the largest need across every piece of state the script keeps. One piece that never forgets makes the whole declaration w.forever().

Choosing the term

Your state

Declare

A lookback of N bars

w.window(length)

An EMA

w.ema(length)

RSI, ATR, ADX or any Wilder average

w.wilder(length)

Resets every session or every day

w.day()

Resets weekly or monthly

The longest gap between resets: w.span("7d"), w.span("31d")

Carries the previous session forward, such as yesterday's value area

The span that covers it: w.span("3d") reaches across a weekend

Never resets: a running total, a count, an all-time high, a trend flag

w.forever()

No state at all

Nothing

State that resets on a boundary wants a length of time. A count of bars is a different stretch of time on every timeframe, so the lint suggests a span when a script resets on ctx.session.isStart and declares a count (window/reset-wants-span).

How it resolves

A declaration resolves separately on each timeframe the script reads, against the settings the reader picked.

  • A plain number and a span are taken as written. A span becomes the number of buckets that covers it, rounded up, at least 1.

  • An expression that reads a number from a setting (w.of, w.window, w.ema, w.wilder, w.range, w.maxOf, w.sumOf) resolves to at least 50 bars, because a setting can carry a number nobody chose, such as 0. A w.when that only chooses between spans and constants is taken as written.

  • Everything is capped at the viewer's indicator memory, covered below.

w.day() on each timeframe:

Timeframe

Bars

1m

1,440

5m

288

15m

96

1h

24

4h

6

1d

1

The warmup also sets how wide each stretch of the grid is, and stretches are aligned to UTC midnight. With w.day(), every intraday timeframe's stretches start at UTC midnight, so the chart and every timeframe handle share the same boundaries. That shared grid is what keeps values identical whether history is computed in one pass or in chunks, on any chart and at any time.

The settings dialog shows what the declaration means on the chart's timeframe, for example Remembers 288 bars on 5m (1 day).

Why smoothed indicators need twice as much

What an average still remembers of its starting point after L bars is (1 − k)^L. An EMA uses k = 2 / (n + 1). Wilder smoothing, used by RSI, ATR and ADX, uses k = 1 / n, half as large, so its memory fades half as fast and it needs twice the bars to forget its start by the same amount. That is why w.ema is 30x and w.wilder is 50x. The shipped indicators follow this: RSI declares w.wilder(length), ADX stacks two Wilder stages as w.sum(w.wilder(period), w.wilder(period)), and Keltner Channels takes w.max of its EMA and its ATR.

Memory limits by plan

Your plan sets indicator memory: the most buckets a script can hold for one timeframe. It bounds warmup, retain, and the history loaded behind every timeframe handle.

Plan

Indicator memory

Free

2,500 buckets

Starter

5,000 buckets

Pro

5,000 buckets

Ultra

10,000 buckets

It counts buckets, so the span it covers depends on the timeframe. For a market that trades around the clock:

Timeframe

2,500 buckets

5,000 buckets

10,000 buckets

1m

1.7 days

3.5 days

6.9 days

5m

8.7 days

17 days

35 days

1h

3.4 months

6.8 months

13.7 months

4h

1.1 years

2.3 years

4.6 years

1d

6.8 years

Every bar since 2015

Every bar since 2015

Markets closed at weekends stretch each span further. Free and Starter also hold three years of history, so a daily handle on those plans reaches three years back whatever the memory limit says. See Plans for the rest of each plan.

The limit is the viewer's. A script you share resolves against the plan of whoever runs it, and a definition run over history resolves against the plan of the account that started it.

What w.forever() means on your plan

w.forever() means as far back as the plan allows, back to the instrument's first bar. Where your memory limit reaches the start of the history you hold, as it does on daily charts on every plan, a never-forgetting value is exact: a running total is the total over all of that history.

Where it does not, on intraday timeframes, the value counts from a fixed point on the grid between one and two memory limits back, and that point moves forward one limit at a time. A cumulative line such as On-Balance Volume on a 5m chart on Pro therefore restarts its origin every 5,000 bars, and its level steps at those instants. Its shape between steps is exact. A running high stays correct as long as the high is inside the window. For an extreme or total over a longer span, use ctx.summary, which has no memory limit: Summaries and long windows.

Because w.forever() follows the plan, the same never-forgetting script can read differently on a Free account and an Ultra account on intraday timeframes. When your state can be reset on a boundary, resetting it and declaring the span between resets removes that dependence.

Asking for more than your plan allows

Over-declaring is never an error. The script runs with your limit and tells you:

  • In the builder, the advisory window/clamped-to-cap names the effective values: the declared warmup/retain for 1h exceeds your plan's 5,000-bucket cap and was clamped. Effective warmup 5000, retain 64.

  • On the chart, the indicator shows "History capped": This script asked for more 1h history than your plan's 5,000-bucket limit allows, so it measures from a date. Below Ultra it offers an upgrade.

  • In the script, history.truncated is true with reason: "cap".

The other ways history comes up short read the same way, with their own reason:

reason

What the chart says

Means

cap

History capped

You declared more than your memory limit

plan

History limited by your plan

Your plan's history window stops there

instrument

History starts at this instrument's first bar

Nothing earlier exists

floor

History truncated

The platform's data starts on 2 February 2015

Read it before stating an extreme:

const daily = timeframe("1d");
export function onBar(ctx) {
  const h = daily.history;
  const label = h.truncated ? `high since ${new Date(h.from * 1000).toISOString().slice(0, 10)}` : "all-time high";
}

How to tell warmup was not enough

Too little warmup does not break a script. It leaves some of the starting point in each value, and that shows in one place: the grid boundaries, where every value's starting point moves forward. Signs:

  • A line takes a small step at regular intervals, one warmup's worth of bars apart, where price did nothing unusual.

  • The same indicator at a higher period steps more than at a lower one.

  • A level or swing that resets daily moves at those instants on a 5m chart, because it declared a count shorter than a day.

To check, double the warmup and compare. If confirmed values move beyond the last digit you care about, the smaller number was short. Declare the named term rather than a number picked by eye: w.ema and w.wilder leave nothing visible.

The lint catches the common mismatches, all on Lint and error codes:

  • state/never-forgets: a running sum, count or high declared with a count. Its value would mean "since the grid point" rather than "since the instrument began". Declare w.forever(), or reset it and declare the span between resets.

  • window/forever-on-decaying: w.forever() on state that only fades fetches your whole memory limit for no gain. Declare w.ema or w.wilder.

  • window/decay-under-factor: an EMA or Wilder average whose length is readable from the script, declared under 30x or 50x.

  • window/reset-wants-span: state that resets on ctx.session.isStart declared as a count. Declare w.day() or a span.

Warmup and retain

retain(...) answers a different question: how many past buckets stay readable through a timeframe handle's bars(n). The maximum of the last 250 daily highs needs retain({ "1d": 250 }) and no warmup, because nothing accumulates. Unset, 64 buckets of each timeframe stay readable.

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 allows

Both are capped by your memory limit. The details are on Timeframe handles and runOn.