Steven Imrich

HomeTrading platforms → Why does my thinkorswim scan return signals from hours ago? (within vs offset)

Why does my thinkorswim scan return signals from hours ago? (within vs offset)

Updated August 27, 2026

Short answer

Almost always it's because the filter is a state ("price is above the 20 EMA") rather than an event ("price crossed the 20 EMA on this bar"). A state stays true for as long as the condition holds, so a stock that crossed at 9:45 keeps passing all afternoon. Use Crosses, or compare the current bar to the previous one with an offset like close[1], to fire only on the bar where it happened. Add within N bars only when you deliberately want anything from the last N bars.

The thing nobody in the forum threads names

There are two kinds of condition and thinkScript doesn’t make you say which one you meant.

A state describes how the world is right now. Price is above the 20 EMA. RSI is under 30. MACD is positive. A state stays true for as long as it’s true, which can be forty bars.

An event describes something that happened on this bar and not the one before it. Price crossed the 20 EMA. RSI just dropped under 30. MACD flipped positive.

When someone posts “my scan keeps showing stocks that already ran”, they have written a state and they wanted an event. That’s it. That’s the whole bug, ninety percent of the time. close > Average(close, 20) was true at 9:45 when the stock crossed, and it’s still true at 3:30 because the stock never came back down. The scanner is answering the question you asked.

There’s a second, smaller version of the same confusion. If your scan aggregation is set to D, “this bar” means “today”, so a cross that happened at 10am is on the current bar all day and always will be. Nothing you write in the script fixes that. You change the aggregation dropdown on the filter row instead.

Where the settings live

Scan tab > Stock Hacker > Add filter > Study. Click the pencil to open the filter, then the thinkScript Editor tab if you want to write it out rather than use the wizard. The aggregation for that filter is the dropdown on the right of the filter row, and it defaults to D.

An event scan: fires on the bar the cross happened

# fires ONLY on the bar where the cross occurred
def ma = Average(close, 20);
plot scan = close crosses above ma;

Crosses is already an event. It’s true when the current bar is above and the previous bar wasn’t, false otherwise. You can write the same thing by hand and it’s worth doing once so you can see there’s no magic in it:

def ma = Average(close, 20);
plot scan = close > ma and close[1] <= ma;   # [1] means "one bar back"

That [1] is the offset syntax. close[1] is the previous bar’s close, close[5] is five bars back. thinkScript also accepts the wordier close from 1 bar ago, which is the same thing and reads better inside a long condition.

The important detail: the offset is what turns a state into an event. close > ma is a state. close > ma and close[1] <= ma is an event, because it’s asking about a change between two bars rather than about a level. Once you see that, you can convert any state into its event by hand, including ones with no built-in Crosses equivalent.

A “within” scan: fires any time in the last N bars

# fires if the cross happened on ANY of the last 5 bars, including this one
def ma = Average(close, 20);
def crossUp = close crosses above ma;
plot scan = Sum(crossUp, 5) > 0;

Sum over a boolean counts how many of the last five bars it was true on, so > 0 means at least one. This is the form I’d use, because it’s plain arithmetic and it can’t surprise you.

thinkScript also has readable within syntax, and the official tutorial shows it as close is greater than close from 1 bar ago within 3 bars, meaning the condition was true on at least one of the last three bars. That works. Where I’d be careful is chaining within directly onto a crosses above expression, which I’ve seen posted plenty of times but haven’t verified parses cleanly in every build. If you use it, sanity check it on a chart first as described below.

Which one you actually want

An event scan if you’re going to look at the results right now and act on them. It’s a smaller list, it turns over, and an empty result at 11am is meaningful information rather than a broken scan.

A within scan if you’re running it once a day after the close and building a list to review, or if your setup takes a few bars to develop and the cross is only the beginning of it. Three to five bars is the usual range. Push it to twenty and you’re back to a state condition with extra steps.

How to check any of this without guessing

Take the same expression, put it on a chart as a lower study, and plot it:

declare lower;
def ma = Average(close, 20);
plot state = close > ma;
plot event = close crosses above ma;

You’ll see state sitting at 1 for long stretches and event spiking to 1 on single bars. Scroll back a week. That picture explains the whole problem faster than any amount of reading, and it’s also how you confirm the scan aggregation is doing what you think.

One more thing worth knowing on intraday scans. The current bar is still forming while you’re looking at it, so a cross can appear in the results and then disappear when the bar closes below the average after all. If that flicker bothers you, test the last completed bar instead by shifting everything one back:

def ma = Average(close, 20);
plot scan = close[1] crosses above ma[1];

Slower by one bar, and it stays put once it’s there.

If your scan now returns nothing at all rather than too much, that’s usually a different set of causes and they’re covered in why thinkorswim scans return no results. And if the scan and the chart disagree about the same symbol, start with the scan vs chart mismatch page, because that’s normally an aggregation or extended hours difference rather than anything to do with within.

Related questions