ChartnautDocs

Price and bar data

Every call to onBar(ctx) hands you the current bar and the context around it: prices, volume, time, whether the bar has closed, the instrument and the chart. Indicators and definitions get the same fields.

Studies do not receive a bar. They read candles around each event instead: see Results and stats.

The bar

Field

Type

Meaning

open / high / low / close

number

OHLC of the current bar

volume

number

Volume of the current bar

volumeBid / volumeAsk

number

Bid and ask volume. 0 when the feed has no split

time

number

Bar open time, unix seconds, UTC

barIndex

number

0-based index within this calculation pass

barConfirmed

boolean

false only on the last bar of a run whose newest bar may still be printing

isLastBar

boolean

true on the newest bar of the live chart and of a one-off run. Always false in definitions and stored runs

volumePriceLevels

PriceLevel[]

Volume traded at each price on this bar. Needs the volume-candles data source

priceLevels

PriceLevel[]

Deprecated name for volumePriceLevels. Same rows

params

object

Resolved settings, keyed by input id. See Settings inputs and dialog

CFD volume

FX, metals and index CFDs have no exchange volume, so Chartnaut measures quote activity instead. Each time the bid and the ask both move up, the bar gains 10,000 units of aggressive buying. Each time both move down, it gains 10,000 units of aggressive selling. A quote that moves only one side adds nothing.

Field

On a CFD bar

volumeAsk

Aggressive buying: units that lifted the offer

volumeBid

Aggressive selling: units that hit the bid

volume

volumeBid + volumeAsk

Delta is volumeAsk - volumeBid, positive when buyers were aggressive. History, the forming bar and the server all use this convention, so a delta indicator keeps its sign at the live edge.

The level depends on the feed. A busier quote feed moves more often, so the same market reads higher on one provider than another, and the level steps when Chartnaut changes provider. Compare CFD volume within one stretch of history, and prefer ratios (VWAP, relative volume) over absolute thresholds.

Some 1m bars are estimated: they were built from a provider's minute candles, which carry a tick count and no direction. Their volume is on another scale and their bid/ask split says nothing about who was aggressive. Scripts receive their stored values unchanged.

barIndex restarts at 0 on every full recalculation and carries on from the last tick on an incremental update. For a counter that survives both, derive it from ctx.time or keep it in ctx.accum.

The forming bar

On a live chart the runtime calls onBar again for the forming bar on every tick. Before each re-run the chart undoes what the previous run wrote to ctx.accum: plain values, objects and arrays go back to how the last closed bar left them, so a buffer that appends once per call still holds the forming bar once. Plain data is all a slot may hold, so this covers every slot: see State with ctx.accum.

Guard on barConfirmed when something must happen only on a close, such as a signal that must not appear and then vanish:

if (ctx.barConfirmed === false) return; // act on closed bars only

barConfirmed is false only on the last bar of a run whose newest bar may still be printing: the forming bar of the live chart, and the last bar of a server run that ends at the current time (a canvas attaching an indicator, a chart preparing a dependency, a study's visuals). It is true on every bar when a definition runs over history, in the agent's dry run and when a script runs over history. Test it with === false or !== false.

The newest bar

ctx.isLastBar is true on the newest bar of the live chart, on every tick, and on the last bar of a one-off run: an indicator run with chartnaut run or run_script, the agent's dry run, and the last bar of a mode: "once" window. It is always false in definitions and in runs that are stored and stitched together: a definition's events, studies and canvases.

There is no lastBarIndex or lastBarTime. Results are cached in pieces and reused by later runs, and which bar is newest depends on when you run, so a value that depended on it would differ between a cached piece and a fresh one. Use isLastBar for things drawn once at the right edge, with an output that keeps only its latest emit:

output.priceLine({ id: "last" });

export function onBar(ctx) {
  if (ctx.isLastBar) ctx.hline("last", ctx.close); // one level at the newest close
}

A mode: "replace" hline or a layer row works the same way.

Time comes from the bar

Scripts run over bars, not the wall clock, so the same bar gives the same answer on every chart and on the server. Date.now() throws. new Date() with no argument, performance.now() and crypto.randomUUID() fail lint with caps/non-deterministic. new Date(ctx.time * 1000) is fine. Math.random() works and is seeded per bar, so a bar draws the same numbers whichever window it is computed in.

Instrument, chart and history

Field

Contents

ctx.instrument

symbol, displaySymbol, tickSize, precision, multiplier, assetClass, category, sessions ({ start, end, type } in exchange time)

ctx.chart

timeframe (e.g. "5m"), timeframeSeconds

ctx.history

{ from, buckets, truncated, reason? }: where this run's chart-timeframe history starts

ctx.log

info / warn / error(message, data?), shown in the builder's Logs tab

ctx.instrument has the same values on the chart and on the server, and never throws: an unknown instrument reads as tickSize 0.01, precision 2, multiplier 1. Size price rows and round levels with tickSize, never a literal.

Check ctx.history.truncated before you call a value "all-time". reason is "cap", "plan", "instrument" or "floor"; see Warmup and memory.

ctx.log writes only in the indicator and definition builders, and a flood is truncated per calculation.

ctx.session and ctx.summary have their own pages: Sessions and Summaries and long windows.

dataSource

Set the feed on meta. The default is ordinary candles.

meta({
  shortName: "Footprint",
  kind: "overlay",
  dataSource: { type: "volume-candles", settings: { purgeStrategy: "floor-aware" } },
});

type: "volume-candles" loads the volume-at-price tape and fills volumePriceLevels. Without it the array is []. The agent's dry run always leaves the tape empty, so check tape scripts on the chart preview.

In the object form, settings.purgeStrategy is required: leave it out and lint fails with meta/volume-candles-purge-required. Write "floor-aware". The string form dataSource: "volume_candles" needs no settings and runs as "floor-aware".

purgeStrategy

What it does today

"floor-aware"

The chart keeps the tape for the bars in view plus 50 bars before them. Older rows past 4,000 bars move out of memory and load again when you scroll back.

"max-bars"

Accepted by lint. Behaves the same as "floor-aware".

"session-daily"

Accepted by lint. The chart treats it as "floor-aware".

The setting only concerns how the chart holds tape in memory. It never changes the rows a bar receives, and a definition run over history on the server ignores it.

Each PriceLevel row has price, totalVolume, totalBuyVolume, totalSellVolume, volumeDelta and batchCount.

volumePriceLevels is a volume profile: how much traded at each price. A TPO or market profile counts time instead. Build that by bucketing bars, for example in a timeframe("30m") handler, not by reading the tape.