How do I build a premarket gap scanner in thinkorswim?
Updated August 27, 2026
Short answer
Set a custom study filter to a 1 minute or 5 minute aggregation with the EXT box checked, then compare the current price to the last close printed during regular hours. Daily aggregation will not work, because the daily bar has no concept of premarket. Expect a shorter list than a dedicated premarket scanner gives you: the thinkorswim scanner only re-runs when it gets round to it, and thinly traded names print no premarket bars at all, so the scan sees stale data for exactly the stocks you were hoping to catch.
What you’re actually up against
thinkorswim doesn’t have a real premarket scanner. It has a general scanner that can be pointed at extended-hours data if you set it up correctly, which is a different thing. The scan runs when you click Scan and then re-runs on the server’s schedule, not yours. Community consensus on useThinkScript is roughly every three to seven minutes, and slower when their servers are busy. At 7:40am that’s often fine. At 9:25am it is not.
Worth knowing before you build it, so you’re not confused later when your list is four names old.
Step 1: the aggregation period, and EXT
Scan tab > Stock Hacker > Add filter > Study. Pick Custom from the study dropdown to open the Scanner Custom Filter window, then click the Aggregation Period button.
Choose 1 min (or 5 min if you want it a bit calmer) and tick EXT.
That checkbox only exists for intraday periods. This is the single reason a premarket scan fails: people build the whole thing on Day aggregation, and the daily bar doesn’t start until 9:30. There’s nothing there to find.
While you’re here, note that 1 to 30 minute aggregations only carry 15 calendar days of history in scans. Plenty for this, but it rules out anything needing a long lookback.
Step 2: the gap condition
You can’t just use close(period = AggregationPeriod.DAY) to get yesterday’s close. Scan filters reject secondary aggregation outright. So you carry the value forward yourself.
# Premarket gap % vs the last regular-hours close
# Run on 1 min or 5 min aggregation with EXT checked
def rthStart = RegularTradingStart(GetYYYYMMDD());
def rthEnd = RegularTradingEnd(GetYYYYMMDD());
def isRTH = GetTime() >= rthStart and GetTime() < rthEnd;
# hold the last close printed during regular hours, carry it through the night
def prevClose = if isRTH then close else prevClose[1];
def gapPct = if !IsNaN(prevClose) and prevClose > 0
then (close / prevClose - 1) * 100
else 0;
plot scan = !isRTH and gapPct >= 4; # 4% or more, outside regular hours only
RegularTradingStart and RegularTradingEnd take a date as YYYYMMDD and give back the session boundaries in milliseconds, for that symbol. Using them instead of hardcoding 9:30 and 16:00 means the script still behaves on half days like the Friday after Thanksgiving, when the close is 1pm. That matters more than you’d think, because the day after a half day is exactly when gaps are worth looking at.
For gap downs, flip the last line to gapPct <= -4. You can’t have both in one filter, one plot only, so make it two filters in the Any of the following group if you want either direction.
The !isRTH on the plot keeps the scan quiet once the bell rings. Drop it if you want the scan to keep firing on the opening print.
Step 3: don’t add a volume filter yet
This is where premarket scans go to die. A Volume stock filter set to 1,000,000 is asking for a million shares in whatever bar the scan is looking at. On a 1 minute premarket bar, nothing on earth does that. You’ll get zero results and blame the gap logic.
If you want a liquidity floor, put it in the same custom filter as a cumulative count instead, or just add a plain price minimum (Last greater than 1) and eyeball the list. Premarket lists are short enough to read.
Step 4: universe and saving it
Set Scan in to All Stocks. Not a watchlist, not All Listed Stocks if you care about the small stuff, though be aware All Stocks pulls in a lot of OTC noise that will never be tradeable in any size.
Show actions > Save scan query names it. Then Show actions > Send to > New watchlist gives you a dynamic watchlist version, which in practice refreshes more usefully than staring at the Stock Hacker results panel.
Why your list is short
A few reasons, and they’re all real rather than fixable.
Thin names print no premarket bars at all. If a stock’s last trade was at 6:02am and it’s now 8:30, the most recent bar in the scan’s series is from 6:02, so close is that stale price. If it hasn’t traded since yesterday evening, the scan is comparing yesterday’s after-hours print to yesterday’s close and finding no gap. The stock might be quoted up 30% with no trades behind it. Dedicated premarket scanners often work off quotes and news, which is why their lists are longer.
The scanner’s refresh is slow. A stock that gapped at 7:15 and faded by 7:35 may never appear.
And OTC and some exchange data depends on what your account is entitled to. If you’re on paperMoney or a delayed-data setup, the scan is working from delayed prices, which during premarket is close to useless. Check a known mover’s quote against a free real-time source before you conclude the scan is broken.
The last thing to check
Put the exact same script on a chart as a study, on a 1 minute chart with extended hours turned on, and look at a stock you know gapped this morning. If scan paints during premarket on the chart but the scanner returns nothing, the problem is a scan setting (aggregation, EXT, universe), and the empty scan triage covers the order to check them in. If it doesn’t paint on the chart either, the logic is wrong and you can debug it somewhere you can actually see the values.
Chart and scan disagreeing on the same script is common enough during extended hours that it has its own page. Read that one before you assume the scan is at fault.