Skip to content
ChartDeck ChartScript docs Account Open the terminal

Bars and time

How a script walks the chart's bars, what bar_index, time and the OHLCV values mean, how barstate works, and why every time is UTC.

A ChartScript indicator does not see the whole chart at once. It runs once per bar, starting at the oldest loaded bar and moving right, and every name such as close or time holds the value of the bar it is running on. When it reaches the newest bar it keeps going: each live update of the forming bar runs the script on that bar again.

This page covers what you can read about the current bar: its position, its prices, its times and its state. The execution model explains the run itself in more depth.

#Bar position: bar_index

bar_index counts the bars of the run: 0 on the first (oldest) bar, then 1, 2, 3 … up to last_bar_index on the newest one.

Bar 0 is the first bar of the run, not the first bar the market ever traded. A run covers 5,000 bars, or the number the indicator asks for with indicator(…, { calc_bars_count: N }), up to your plan's bars per run (5,000 on Beginner, 7,500 on Plus, 10,000 on Pro and 20,000 on Elite; at most 2,000 on a phone). The run does not follow the chart: scrolling back past bar 0 shows older bars without the script's values. So never treat bar_index as a fixed date: reload the chart later, change calc_bars_count or switch plan and the numbers move.

bar_index is handy for counting and for placing drawings. This fragment marks every 50th bar:

ChartScriptFragmentOpen in editor
plotchar(bar_index % 50 == 0, "Every 50 bars", "•", location.abovebar, color.muted)

#Prices and volume

NameWhat it holds
open, high, lowthe bar's open, high and low
closethe close; on the live bar, the last price, so it moves until the bar closes
volumethe bar's volume in base units (coins: BTC on BTCUSDT), not in dollars
hl2, hlc3, ohlc4, hlcc4the usual averages: (H+L)/2, (H+L+C)/3, (O+H+L+C)/4 and (H+L+2C)/4

Because volume is in coins, multiply by a price when you want a dollar figure. volume * hlc3 is a good approximation of the quote volume of a bar:

ChartScriptOpen in editor
//@chartscript 1
indicator("Dollar volume", { format: format.volume })
const usd = volume * hlc3
const avg = ta.sma(usd, 20)
plot(usd, "Dollar volume", usd > avg ? color.accent : color.muted, { style: plot.style_columns })
plot(avg, "20-bar average", color.text)

History works the same for every one of these: close[1] is the previous bar's close, high[5] the high five bars ago. See variables and series and qualifiers.

#Times are milliseconds in UTC

Every time value in ChartScript is an int: milliseconds since 1 January 1970, in UTC. Crypto markets have no exchange time zone, so ChartDeck uses UTC everywhere (market.timezone is "UTC"), and there is no daylight saving anywhere in bar times.

NameWhat it holds
timewhen the bar opened
time_closewhen the bar closes: the open of the next bar (a monthly bar closes on the 1st of the next month)
last_bar_timethe open time of the newest bar on the chart
time_tradingday00:00 UTC of the bar's day (a coin market's trading day is the UTC day)
timenowthe current time when the run happens

timenow is different on every run, so anything you compute from it can differ between a reload and the live chart. Use it for "how long ago" displays, not for signals.

#Calendar parts and time zones

The calendar variables split the bar's open time into parts, in UTC: year, month (1-12), dayofmonth, dayofweek, weekofyear (ISO weeks), hour, minute and second.

dayofweek runs from Sunday = 1 to Saturday = 7. Compare it with the named constants rather than numbers: dayofweek == dayofweek.monday reads better than dayofweek == 2 and cannot be off by one.

Each of these also exists as a function that takes any time and an optional time zone, which is how you work in local time. A time zone is either an IANA name ("America/New_York", "Europe/London", "Asia/Tokyo") or a fixed offset ("UTC+2", "GMT-5"). Named zones follow their daylight-saving rules.

ChartScriptFragmentOpen in editor
const nyHour = hour(time, "America/New_York")
plot(nyHour, "Hour in New York")

To turn a date into a time, use timestamp: timestamp(2026, 3, 1) is 1 March 2026 00:00 UTC, timestamp("Europe/London", 2026, 3, 1, 8, 0) is 08:00 London time, and timestamp("2026-03-01") parses a date string. To show a time as text, use str.format_time, which also takes a time zone.

#The bar's state: barstate

The barstate values tell you which kind of bar the script is running on:

NameTrue when
barstate.isfirstthe first bar of the run (bar_index == 0)
barstate.islastthe newest bar of the chart (live or not)
barstate.ishistorya closed bar of the history
barstate.isrealtimethe live bar
barstate.isnewthe first run of a new live bar
barstate.isconfirmedthe bar is closed: every history bar, and the final run of the live bar when it closes
barstate.islastconfirmedhistorythe last closed bar of the history, just before the live bar

Two patterns cover most uses:

  • if (barstate.islast) builds things only once, on the newest bar: a table, a label with the latest value. This keeps a script fast, because the work is not repeated on thousands of history bars.
  • if (barstate.isconfirmed) acts only on closed bars, so a signal cannot appear and then vanish while the live bar is still moving. See repainting.

#Scripts in Bar replay

Bar replay (Shift+R, or the Replay button in the chart toolbar) rewinds every chart of your layout to a moment you pick and plays the bars forward from there. Space plays and pauses, the right arrow steps one bar (Shift+right arrow ten), the left arrow steps back, and Esc returns to live. Beginner replays minute charts and up at 1x and 2x; Plus, Pro and Elite add 5x, 10x and seconds charts.

Only bars that had closed at the replay's time are on the chart, so a script that reads the chart's own bars keeps running and never sees a later bar: each step adds a bar, as a new bar does when the chart is live.

Most of what a script reads follows the replay clock too, so the script keeps running and sees only what was known at the replay's time:

A script that reads data which cannot be shown as it was at that time pauses while the chart replays:

Its legend says "Paused in replay" and why, and it starts again on its own when you go back to live. The chart's other overlays that need live data are hidden the same way.

#A whole example: weekday and day separators

This indicator shades weekends, draws a marker on the first bar of each UTC day, and puts the time of the newest bar in a label. It shows the calendar variables, time_tradingday and barstate.islast working together.

ChartScriptOpen in editor
//@chartscript 1
indicator("Day separators", { overlay: true })

const shadeWeekends = input.bool(true, "Shade weekends")
const tz = input.string(
    "UTC",
    "Time zone for the label",
    { options: ["UTC", "Europe/London", "America/New_York", "Asia/Tokyo"] }
)

// a new UTC day starts when the trading day changes
const newDay = time_tradingday != time_tradingday[1]
plotshape(newDay && timeframe.isintraday, "New day", shape.diamond, location.bottom, color.muted, { size: size.tiny })

const isWeekend = dayofweek == dayofweek.saturday || dayofweek == dayofweek.sunday
bgcolor(shadeWeekends && isWeekend ? color.new(color.muted, 90) : na, { title: "Weekend" })

state info: label = na
if (barstate.islast) {
    label.delete(info)
    const txt = "Last bar opened " + str.format_time(time, "EEE dd MMM HH:mm", tz) + " (" + tz + ")"
    info = label.new(
        bar_index,
        high,
        txt,
        { style: label.style_label_down, textcolor: color.text, color: color.new(color.muted, 70) }
    )
}

The ChartScript documentation is in English for now; translations will follow.