Session string syntax, and its two traps
A time-based session string is <time_period>:<days> [1].
The time period is HHmm-HHmm on a 24-hour clock, and several periods can be
comma-separated. The days part is digits, and omitting it means every day.
| String | Means [1] |
|---|---|
"0930-1700" | 09:30 to 17:00, every day |
"0800-0900,1230-1630" | two separate windows in one day |
"0930-1700:146" | Sundays, Wednesdays and Fridays |
"1700-1630:23456" | an overnight session, Monday to Friday |
"24x7" | the same as "0000-0000:1234567" |
Trap one: 1 is Sunday
Days are numbered 1 to 7 for Sunday through Saturday [1]. Not Monday through Sunday.
So the instinctive way to write "weekdays only" — :12345 — actually selects
Sunday to Thursday. It includes a day most markets are shut and excludes
Friday, one of the most active. Monday to Friday is :23456.
Nothing errors. The filter works, on the wrong days, and the backtest measures a strategy you did not design.
Trap two: the timezone is the exchange's, not your chart's
Session times are interpreted in the exchange's timezone unless you pass a
timezone argument, and that is a different thing from the timezone your chart
is displaying [1].
If you set your chart to local time, read 08:00 off the axis and write
"0800-1600", you have filtered 08:00–16:00 exchange time. On a US equity
symbol viewed from Europe that is a window in your afternoon and evening.
This one is worth testing rather than reasoning about: plot the filter and check the bars it actually keeps.
Only three functions take these strings
time(), time_close() and input.session() are the only functions that
accept a time-based session string [1]. request.*() does not — it needs a
named session instead, via ticker.new() or ticker.modify() with
session.regular or session.extended.
// na outside the session, a timestamp inside it
inSession = not na(time(timeframe.period, "0930-1600:23456"))
Align the session to your bars
TradingView states the requirement directly: align the start and end times of a time-based session with the start and end times of chart bars at the timeframe you expect [1]. A session boundary that falls mid-bar produces results that look arbitrary, because they are.
Session state, ATR and liquidity gates
Beyond the string syntax, Pine exposes the session as state you can read directly [1]:
| Variable | True when |
|---|---|
session.ismarket | inside regular hours |
session.ispremarket | inside pre-market extended hours |
session.ispostmarket | inside post-market extended hours |
session.isfirstbar | first bar of the day's session |
session.isfirstbar_regular | first bar of the regular session |
session.islastbar | last bar of the day's session |
session.islastbar_regular | last bar of the regular session |
The last-bar variables can silently never fire
This is documented and it matters: session.islastbar and
session.islastbar_regular might not be true for any bar in a session if
no price or volume updates occur during the time period of that last bar [1].
So a strategy that closes its position on the last bar of the session can, on a quiet day in a thin instrument, simply not close. Nothing errors, the position carries overnight, and the log gives no reason.
If flattening at the end of the session is a requirement rather than a
preference, gate on the session window itself — not na(time(...)) going false
— rather than trusting a last-bar flag to arrive.
Extended hours are not what you assume on futures
For most US futures, Electronic Trading Hours is the default "regular" session,
and session.extended is equivalent to session.regular [1]. A filter written
to exclude "extended hours" on a futures symbol may therefore exclude nothing at
all.
Volatility and liquidity gates
A session filter answers when. A volatility filter answers whether the market is moving enough for the strategy's assumptions to hold — a mean-reversion rule and a breakout rule want opposite answers from it.
The honest way to build one is relative, not absolute. ATR is denominated in the instrument's price, so a fixed threshold is meaningless across instruments and across time on the same instrument. Compare ATR to its own recent average instead:
atrValue = ta.atr(14)
atrBaseline = ta.sma(atrValue, 100)
expanding = atrValue > atrBaseline
That comparison transfers. A published number like "ATR above 1.5" does not, and any article giving you one has not thought about the instrument you are trading.
The same logic applies to volume gates: compare volume to its own average rather than to a constant, because a constant encodes the liquidity of whatever instrument the author happened to be looking at.
Combining gates costs you sample size
Every filter added removes trades. That improves the statistics on paper and reduces the evidence underneath them at the same time — a strategy with 400 trades and a modest edge is stronger evidence than one with 25 trades and a spectacular one. Watch the trade count as you add gates, not only the return.
Tactix AI on this workflow
AlfaTactix includes Tactix AI: use Tactix Studio to describe your idea in plain language and draft timeframes, signals, filters, and risk into the same six-step Strategy Builder form — or open Tactix Guide on any step when you only need a field explained. Review every value, then export MQL5 or Pine Script from Code Generator (form-first — not untested prompt-to-code).

