Bar states, sessions and time
Lesson 9 · about 11 min
Trading logic is full of clauses about time: only during the regular session, only on the first bar of the day, not in the last ten minutes, only once the bar has closed. Pine exposes all of this through three groups of built-ins. Bar states tell you what kind of bar the script is on; session functions tell you whether a bar falls in a time window; time functions give you the clock.
Bar states
| Variable | True when |
|---|---|
barstate.isfirst |
the first bar of the chart |
barstate.islast |
the last bar on the chart, whether historical or live |
barstate.ishistory |
a historical bar |
barstate.isrealtime |
the live bar (any update of it) |
barstate.isnew |
the first update of a bar (every historical bar, first tick live) |
barstate.isconfirmed |
the final update of a bar (every historical bar, closing tick live) |
barstate.islastconfirmedhistory |
the last historical bar before the live one |
Two are used constantly. barstate.islast is where you draw things that should only exist once, like a table or a label at the current price, because drawing on every bar wastes the object budget. barstate.isconfirmed is the guard that makes a condition fire only when the live bar has actually closed, which is the foundation of the repainting discussion in module 5.
//@version=6
indicator("Bar states", overlay=true)
var label info = na
if barstate.islast
label.delete(info)
txt = barstate.isrealtime ? "live bar, updating" : "last historical bar"
info := label.new(bar_index, high, txt, style=label.style_label_down, color=color.new(color.blue, 20), textcolor=color.white)
On historical bars isconfirmed is always true, because history is made of closed bars. That is why a script can look identical on history and behave differently live if it does not guard with it.
Time variables
time is the bar's open time as a Unix timestamp in milliseconds; time_close is the close time. Calendar pieces are available directly: year, month, dayofmonth, dayofweek, hour, minute, second. These use the exchange's time zone by default. To read them in a specific zone, use the function forms: hour(time, "America/New_York").
timestamp() builds a time from parts: timestamp("America/New_York", 2024, 3, 15, 9, 30), or from a string timestamp("2024-03-15T09:30:00-04:00"). Comparing time against a timestamp is how you restrict a backtest to a date range, and input.time() lets the user pick that date from a calendar.
dayofweek compares against constants: dayofweek == dayofweek.friday.
Sessions
A session string is "HHMM-HHMM", optionally with days: "0930-1600:23456" means weekdays only (1 is Sunday). The time() function with a session returns the bar's time if the bar is inside the session and na otherwise, which makes not na(time(...)) the standard "in session" test.
//@version=6
indicator("Sessions and time", overlay=true)
sess = input.session("0930-1600", "Session")
tz = input.string("America/New_York", "Time zone")
inSession = not na(time(timeframe.period, sess, tz))
newDay = timeframe.change("D")
lastTenMin = timeframe.isintraday and not na(time(timeframe.period, "1550-1600", tz))
bgcolor(inSession ? color.new(color.blue, 93) : na)
bgcolor(lastTenMin ? color.new(color.orange, 85) : na)
plotchar(newDay, "New day", char="|", location=location.top, color=color.gray, size=size.tiny)
timeframe.change("D") is true on the first bar of each new day and works for any period string, so timeframe.change("W") marks Mondays (or Sundays for forex and crypto, which is worth checking on your symbol). It replaces the older idiom of comparing dayofmonth to its previous value.
The session.* group covers the symbol's own hours: session.ismarket is true during regular hours, session.ispremarket and session.ispostmarket for extended hours on symbols that have them, and session.isfirstbar on the first bar of a session.
Timeframe information
timeframe.period is the chart's period as a string ("15", "D"). timeframe.isintraday, timeframe.isdaily, timeframe.isweekly are booleans. timeframe.multiplier is the number part. timeframe.in_seconds() converts a period to seconds, which lets you compare timeframes: timeframe.in_seconds("60") > timeframe.in_seconds(timeframe.period) checks that an hour is higher than the chart.
Scripts that only make sense intraday should say so rather than draw nonsense:
if not timeframe.isintraday
runtime.error("This script needs an intraday chart.")
runtime.error stops the script and shows the message on the chart.
Key idea:
barstate.isconfirmedmeans the bar is finished,not na(time(timeframe.period, session, tz))means the bar is inside a time window, andtimeframe.change("D")means a new day started. Most time logic is a combination of those three.
Try it: Colour the background of the first 30 minutes of the regular session on a 5-minute chart, add a label on
barstate.islastshowing the current hour and minute in New York time, and stop the script withruntime.errorif it is added to a daily chart.
Recap
barstate.islastis for one-off drawings;barstate.isconfirmedis for "bar has closed" logic and is always true on history.timeandtime_closeare millisecond timestamps;hour,dayofweekand friends read the exchange zone unless you pass a time zone.- Sessions are
"HHMM-HHMM:days"strings;not na(time(timeframe.period, sess, tz))tests membership. timeframe.change("D")marks a new period;session.ismarketmarks regular hours.timeframe.isintradayandtimeframe.in_seconds()let a script check it is on a suitable chart;runtime.errorstops it otherwise.
See it drawn
Original diagrams for the ideas on this page. Illustrative, not real market data.