ChartnautDocs

Seams and anchors

A seam is a place where one stretch of computed history meets the next. Every chart has them: where the history loaded before your chart meets the chart's own bars, and at fixed points on a time grid where a script's memory restarts. An anchor is the fixed point a stretch starts its memory from. This page says what Chartnaut guarantees at seams and what your script must do to keep it.

The problem anchors solve

A script that remembers has to start remembering somewhere. The obvious place is the first bar available, and that is where most engines start. The first bar available is an accident: it depends on how far you scrolled, when you opened the chart, and whether the value is being computed in your browser or on a server running a definition over a date range. Two runs over the same bars that start in different places compute different values for the same bar, and no amount of extra history fixes it, because the gap moves with the start.

Chartnaut starts memory from anchors instead. An anchor is a point on a grid laid over time itself, and the anchor for a bar is decided by the bar's own time and your declared warmup, nothing else.

How the grid is laid

Time on each timeframe is cut into stretches, each one warmup long, starting from the same UTC grid every bucket uses. With warmup(1000) on a 5m chart, each stretch is 1,000 five-minute bars. With warmup((w) => w.day()), each stretch is one UTC day on every timeframe: 288 bars on 5m, 24 on 1h, each starting at UTC midnight, so the chart and every timeframe handle share the same boundaries. Every bar inside a stretch is computed from a walk that starts at the beginning of the previous stretch. So each value has between one and two warmups of history behind it, and the start point is the same whoever asks.

When the anchor would fall before the first bar of data, it is the first bar of data. On a coarse timeframe that makes the grid disappear: a daily EMA with a warmup longer than the daily history is computed over all of it, the full-history value.

What anchors guarantee

  • A value on a confirmed bar is the same however much history your chart loaded, after a reload and after scrolling.

  • The chart and a run over history start each bar's memory from the same anchor, so they compute the same value.

  • Live values continue to match history. When a live chart crosses into a new stretch, Chartnaut recomputes from the new anchor, so the live value at that bar is the one a fresh load would give.

  • A replay computes each revealed bar exactly as a live chart would have.

  • A timeframe handle reads the same on every chart timeframe. A higher timeframe's anchor only moves when its own stretch does, and every grid tiles with every other.

  • Levels, zones and trendlines that seal when price takes them out seal on the same bar on the chart and on the server, across stretch boundaries. Their expires is a duration, so a level expires at the same instant on a 5m chart, a 4h chart and the server.

The loading seam and the straddling bucket

The other seam is where history loaded behind your chart meets the chart's bars. A timeframe handle reads two sources split at that instant: buckets that ended before it come from stored history at the handle's own timeframe, and buckets after it are built from the chart's bars.

One higher-timeframe bucket usually straddles the seam: a day that started before your chart's first bar and ends after it. The chart's bars cover only its tail. Chartnaut guarantees:

  • When the straddling bucket closes, last and bars(n) hold its true open, high, low, close and volume for the whole bucket, the same bucket a chart that loaded all of it sees.

  • While it is still forming, forming.open is the true open. forming.high, forming.low and the running volume cover only the part on your chart until it closes.

This matters only when your chart's history starts inside the current higher-timeframe bucket, such as a daily handle on a chart whose first bar is today. Read forming with that in mind, and use last for anything you fold into state.

What a script must do to keep the guarantee

Anchoring fixes where memory starts. It cannot fix state that ignores it.

  • Declare warmup honestly. A warmup shorter than your state's memory leaves part of the starting point in each value, visible as a step at each stretch boundary. Size it with the table on Warmup and memory.

  • Size self-resetting state by its longest gap. A value that resets each session declares warmup((w) => w.day()), one day on every timeframe (288 bars on 5m), so each stretch starts at UTC midnight and never cuts a session short.

  • Keep memory in ctx.accum or a handle's state. Each stretch starts fresh and is rebuilt from its anchor, so nothing survives from the chart's first bar. State is correct when the walk from the anchor can rebuild it.

  • Declare w.forever() for state that never forgets, and know what it means on your plan: on intraday timeframes the origin moves one memory limit at a time.

  • Fold state once per bar. State that changes on each tick of the forming bar drifts from what history computes (Bars and clocks).

  • Key identity on ctx.time. ctx.barIndex restarts with each pass and each stretch.

  • Declare level lifecycle on the output: output.hline({ id, mode: "invalidateOnCustomFunction", invalidateWhen: "wickAbove", expires: "5d" }). A function passed per emission cannot be re-checked once its stretch is done, so the lint refuses it (output/invalidate-per-emit-fn). Pass per-level numbers through meta and read them in the declared rule.

  • Scope oncePer keys to time. Dedupe runs over the whole run, and a chart and a run over history start in different places. Include a time-bound payload field in keys, such as the session key s.key, and the result is the same in both. Keys whose values recur forever, such as a side field alone, fire once per run, wherever that run started.

  • Keep randomness and the wall clock out. Math.random() and new Date() do not give the same value in your browser and on the server.

Seams you can see

Where

What you see

Is it expected?

A stretch boundary, one warmup of bars apart

A small step in a line

No: warmup is too short for the state

A stretch boundary on a w.forever() intraday script

A cumulative line changes level

Yes: the origin moved one memory limit

The loading seam

Nothing

Values are identical on either side

The straddling bucket while forming

A forming high or low that misses the part before your chart

Yes, until the bucket closes

The start of your plan's history window

"History limited by your plan" beside the indicator

Yes: values there have less history behind them