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:
plotchar(bar_index % 50 == 0, "Every 50 bars", "•", location.abovebar, color.muted)#Prices and volume
| Name | What it holds |
|---|---|
open, high, low | the bar's open, high and low |
close | the close; on the live bar, the last price, so it moves until the bar closes |
volume | the bar's volume in base units (coins: BTC on BTCUSDT), not in dollars |
hl2, hlc3, ohlc4, hlcc4 | the 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:
//@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.
| Name | What it holds |
|---|---|
time | when the bar opened |
time_close | when the bar closes: the open of the next bar (a monthly bar closes on the 1st of the next month) |
last_bar_time | the open time of the newest bar on the chart |
time_tradingday | 00:00 UTC of the bar's day (a coin market's trading day is the UTC day) |
timenow | the 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.
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:
| Name | True when |
|---|---|
barstate.isfirst | the first bar of the run (bar_index == 0) |
barstate.islast | the newest bar of the chart (live or not) |
barstate.ishistory | a closed bar of the history |
barstate.isrealtime | the live bar |
barstate.isnew | the first run of a new live bar |
barstate.isconfirmed | the bar is closed: every history bar, and the final run of the live bar when it closes |
barstate.islastconfirmedhistory | the 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:
- other timeframes and markets (
request.*), up to the replay time; - the ChartDeck data series (open interest, funding, basis, CVD, liquidations and the rest), as their built-in studies show them in replay;
- footprints (
fp): the bars that had closed at the replay's time only; - the chart's Big trades (
flow.big_*): the prints at or before the replay time, at the Auto size of that time; - the liquidations on each bar (
liq.long_liq_usd,liq.events): a liquidation once its bar, or its hour, had ended; - the recorded order book (
book.*) and the economic calendar.
A script that reads data which cannot be shown as it was at that time pauses while the chart replays:
- trade prints (
flow.prints) and the model's liquidation levels (liq.levels,liq.heat); - volume at price (
vap); - a protected script that runs on ChartDeck's servers.
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.
//@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) }
)
}