Pine Script time and date handling, and firing a command once per bar
Pine Script gives you a bar's timestamp, its calendar breakdown, and a way to check whether that bar falls inside a specific session, none of which the other Pine Script guides here touch. This page covers all three, then the piece that matters most for automation: using barstate.isconfirmed so a time- or date-based condition fires an Autoview command exactly once per bar, not once per realtime tick.
time, time_close, and timenow
Three built-ins hold different timestamps, all in UNIX format:
timeholds the current bar's opening time.time_closeholds the same bar's closing time. It'snaon an open realtime bar of a tick or price-based chart, since that bar hasn't closed yet.timenowholds the timestamp of the script's own current execution. It only updates when the script actually runs, so it isn't a live clock ticking independently of the chart.
barAgeSeconds = timenow - time
The calendar built-ins
Pine Script also breaks the current bar's time down into calendar components, all int values expressed in the exchange's time zone: year, month, weekofyear, dayofmonth, dayofweek, hour, minute, and second. There's no single built-in named day, it's dayofmonth and dayofweek as two separate variables:
isNewMonth = month != month[1]
if isNewMonth
label.new(bar_index, high, "New month", color=color.blue, textcolor=color.white)month != month[1] compares this bar's month to the previous bar's month; the label appears the first bar where they differ. Build a readable date string the same way, concatenating each piece with str.tostring():
dateString = str.tostring(year) + "-" + str.tostring(month) + "-" + str.tostring(dayofmonth)
The same calendar values, for a different timestamp or time zone
year, month, dayofmonth, and the rest above are variables: they always describe the current bar, in the exchange's own time zone. Every one of them also exists as a function taking a timestamp and an optional time zone, which computes the same calendar value for any UNIX timestamp, in any zone:
year(time, timezone)month(time, timezone)dayofmonth(time, timezone)dayofweek(time, timezone)weekofyear(time, timezone)hour(time, timezone)minute(time, timezone)second(time, timezone)
In every case time is a UNIX timestamp in milliseconds and timezone is optional, defaulting to syminfo.timezone if left out, and accepting the same UTC-offset or IANA strings time() and timestamp() do elsewhere on this page. Reach for the function form when the value you want isn't "the current bar in the chart's own zone", for example checking the close time of the previous bar in UTC regardless of what exchange the chart is on:
closeHourUtc = hour(time_close, "UTC")closeHourUtc is the hour, 0-23, that this bar's time_close timestamp falls in, in UTC, whatever time zone the symbol itself trades in. The plain hour variable can't do this: it only ever reports the current bar in the exchange's own zone.
Detecting a session with time()
Checking the hour manually works for a fixed UTC session, but real trading sessions are timezone-specific and don't all start on the hour. time(timeframe, session, timezone) handles both: it returns the current bar's UNIX timestamp if that bar opens or closes inside the session string you give it, in the timezone you give it, or na if it doesn't.
inLondonOpen = not na(time(timeframe.period, "0800-0900", "Europe/London"))inLondonOpen is true on every bar between 08:00 and 09:00 London time, false otherwise, regardless of the exchange time zone the chart itself is set to. timeframe.period passes the chart's own current timeframe through, which is what you want in most cases; time() also accepts a fixed timeframe string like "5" if you need it to check a specific resolution instead.
Timezones: syminfo.timezone and the timezone parameter
time() and timestamp() both take a timezone parameter. Left out, it defaults to the symbol's own exchange time zone; supplied, it accepts either a UTC-offset string like "UTC-5" or an IANA string like "America/New_York". syminfo.timezone holds the current symbol's exchange time zone as an IANA string, in case you need to reference or display it rather than hardcode a session's own zone:
exchangeTz = syminfo.timezone
Converting a timestamp between time zones
str.format_time(time, format, timezone) takes a UNIX timestamp and renders it as a formatted string in whichever zone you pass, converting from the instrument's own zone if the two differ. Passing the same timestamp through twice with two different zone arguments shows the conversion directly:
exchangeTime = str.format_time(time, "yyyy-MM-dd HH:mm", syminfo.timezone)
utcTime = str.format_time(time, "yyyy-MM-dd HH:mm", "UTC-0")
nyTime = str.format_time(time, "yyyy-MM-dd HH:mm", "America/New_York")All three describe the exact same bar-open instant; only the printed clock time changes. This is the tool to reach for whenever a session string, a log line, or an alert message needs to show a human-readable time in a specific zone rather than the chart's own default.
Alert frequency, gathered in one place
alert()'s freq argument controls how often it can fire, and all three date/session patterns above interact with it:
| Constant | Fires |
|---|---|
alert.freq_once_per_bar | Once per bar while the condition holds. The default. |
alert.freq_once_per_bar_close | Only on the bar's close, the confirmed execution -- pairs with a barstate.isconfirmed gate. |
alert.freq_all | On every realtime update, including intrabar ticks. |
For a session-open or date-change condition, alert.freq_once_per_bar_close paired with barstate.isconfirmed is almost always the right combination; alert.freq_all is rarely what you want for anything that places an order.
Seeing confirmed vs. unconfirmed directly
Rather than take barstate.isconfirmed's behavior on faith, log both states and watch the difference happen live:
//@version=6
indicator("Confirmed vs Unconfirmed", overlay=true)
inLondonOpen = not na(time(timeframe.period, "0800-0900", "Europe/London"))
justEnteredSession = inLondonOpen and not inLondonOpen[1]
if justEnteredSession
if barstate.isconfirmed
alert("CONFIRMED: session open, bar closed", alert.freq_all)
else
alert("unconfirmed: session open, bar still forming", alert.freq_all)Watch the log during a real session open. You'll typically see one or more unconfirmed lines first, as the transitioning bar forms and re-forms on each tick, followed by exactly one CONFIRMED line once that bar closes. That's the concrete, visible version of the once-per-bar problem barstate.isconfirmed solves below -- seen in the log, not just asserted in prose.
Duplicate protection
A session or date condition that repaints intrabar (see the confirmed/unconfirmed log above) is exactly the pattern that produces repeated alerts on the same setup. Autoview's own protection for that case is the dedupe= parameter, and it's worth knowing its real scope before relying on it here: dedupe= exists only on the Chrome extension, remembers a single last-seen value per alert (not a history), and is a silent no-op on the cloud webhook platform, where it's accepted but never stored or compared. Duplicate or repeated orders covers the full mechanism and its extension-only limitation. For a time-based condition specifically, dedupe={{time}} (the bar's own timestamp) collapses repeats of the same bar to one execution, while dedupe={{timenow}} looks similar but defeats the purpose, since the current clock time is different on every repaint.
Firing an alert exactly once per bar
A script re-runs on every new tick of a realtime bar, not just once when the bar finally closes. Left ungated, a condition built from any of the above can turn true several times on the same bar before it's done forming, and alert()'s default freq (alert.freq_once_per_bar) only limits repeats within a bar, it doesn't wait for the bar to close. barstate.isconfirmed is true only on a bar's final execution, its close, which makes it the gate for "exactly once, and only once the bar is done":
//@version=5
indicator("Session Open Once", overlay=true)
inLondonOpen = not na(time(timeframe.period, "0800-0900", "Europe/London"))
justEnteredSession = inLondonOpen and not inLondonOpen[1]
if justEnteredSession and barstate.isconfirmed
alert("s=EUR_USD b=buy q=1000 t=market", alert.freq_once_per_bar_close)justEnteredSession catches the bar-to-bar transition into the session window; barstate.isconfirmed holds the alert until that specific bar is confirmed closed, so it fires once, not once per intrabar update while the transition bar is still forming. Pairing it with alert.freq_once_per_bar_close as the freq argument reinforces the same intent on alert()'s own side.
Test before you trust it
Add d=1 to the message before you rely on any of this. Autoview's dry-run gate is exchange-agnostic: every integration's trade path calls a shared withDebugReporter check, and when cmd.d is set, it logs what the command would have done and returns before any live order is sent. Let justEnteredSession trigger across a few real session opens, check the log to confirm it only fired once per session, then remove d=1 once it matches.
One script event should be one webhook delivery -- verify it
barstate.isconfirmed plus alert.freq_once_per_bar_close controls how many times the script fires the alert. It says nothing about whether TradingView's delivery of that alert and Autoview's receipt of it stay one-to-one; a retried delivery on TradingView's side or a network hiccup could in principle produce two webhook posts from one script event. To check the two actually match, count both independently over the same window: add an incrementing counter to the alert message itself, then compare that count to the number of log entries Autoview recorded for the same period.
var int fireCount = 0
if justEnteredSession and barstate.isconfirmed
fireCount := fireCount + 1
alert("s=EUR_USD b=buy q=1000 t=market d=1 // fireCount=" + str.tostring(fireCount), alert.freq_once_per_bar_close)Run this dry (d=1 is already in the message above) across several real session opens. fireCount tells you how many times the script itself fired. Then open the Autoview log for the same stretch of time and count the dry-run entries carrying that command. If the two counts match, one script event produced exactly one webhook delivery; if the log shows more entries than fireCount, something between TradingView and Autoview (a retry, a duplicate webhook, a second sender) is the cause, not the Pine Script logic covered on this page.
A note on what Autoview is. Autoview is an execution tool, not a trading or investment advisor. It places the orders your script and alerts tell it to; it doesn't generate signals or make any claim about results. Trading carries risk of loss, and you're responsible for the strategy you automate. See our disclosures.