My phone buzzed at 1am. "FICO $1197.83, crossed above its 5-day average." Then again near 2am. Same price. Then around 3am. Same price, to the penny.
FICO doesn't print the exact same price for five hours straight. Something was feeding my alert bot a lie, and the bot was repeating it with full confidence.
Here's what was actually happening, and why a timestamp you're ignoring is more dangerous than no data at all.
The setup
I run a little bot that watches a handful of stocks and pings me on Telegram when the price crosses a moving average. Nothing exotic. It checks once a minute, around the clock.
"Around the clock" is the interesting part. During regular hours and the after-hours session, the price comes from my broker. But brokers go quiet after 8pm Eastern. The market doesn't, not really. There's an overnight session now, 8pm to 4am, and a bunch of brokers route it through the same place: Blue Ocean ATS.
So for the overnight window I pulled the price from somewhere that covers it. Robinhood exposes a public quote endpoint, no login required:
GET https://api.robinhood.com/quotes/FICO/
It hands back a tidy little object. Last trade price. Last extended-hours trade price. Bid, ask. And, crucially, an updated_at field.
I used the extended-hours field overnight and moved on. That was the mistake.
The freeze
When the 1am alert showed up, I pulled the raw quote by hand. Here's the part that mattered, at roughly 1:48am Eastern:
{
"last_trade_price": "1207.92",
"last_extended_hours_trade_price": "1197.83",
"updated_at": "2026-06-09T00:00:00Z"
}
Look at updated_at. Midnight UTC. That's 8pm Eastern. The quote hadn't moved in almost six hours.
The public endpoint stops updating at 8pm. It pins itself to the 8pm after-hours close and sits there all night. Both price fields, frozen. The updated_at stamp tells you the truth if you bother to read it, but the prices look perfectly alive. Same shape, same types, valid numbers. Nothing errors. Nothing throws. The data just quietly stops being real.
Meanwhile FICO was trading around 1202 overnight on Blue Ocean. The live number exists. It's just not in the endpoint I was reading. To get it you need Robinhood's authenticated 24-hour feed, with a login and an instrument lookup. The free, public quote doesn't carry the overnight session at all. It carries a corpse and labels it a price.
Why this is worse than nothing
A missing value is honest. Your code checks for null, sees nothing, and skips. No harm.
A frozen value is a liar that passes every check. It's a positive number. It's recent-looking. It satisfies "is this a valid price?" with a straight face. So my bot compared 1197.83 against the moving average, found a crossing, and fired. Every cooldown cycle, all night, it found the "same" crossing again and pinged me again.
The alert wasn't just stale. It was confidently, repeatedly wrong, and it had a timestamp sitting right there the whole time that would have caught it.
The fix is four lines
The freshness check should have been there from the start:
export function extractOvernightPrice(quote, nowMs = Date.now()) {
if (!quote) return null;
if (quote.updated_at) {
const updatedMs = Date.parse(quote.updated_at);
if (Number.isFinite(updatedMs) && nowMs - updatedMs > 15 * 60_000) {
return null; // frozen 8pm close, not a live overnight price
}
}
// ... otherwise return the extended-hours price
}
If the quote is older than fifteen minutes, treat it as frozen and return null. The bot skips the symbol instead of alerting on a ghost. Silence beats a wrong number sent to my pocket at 3am.
Now the overnight window is quiet, by design. If Robinhood ever does serve a genuinely fresh overnight print, the same check lets it through. The rule isn't "never alert at night." It's "only alert on a price that's actually moving."
The part I can't fix, and won't pretend to
There's a second problem hiding under the first. Even with a live overnight price, the moving average I'm comparing against is a daytime average. It's built from regular-session daily bars. There are no overnight bars to build an overnight average from, not from any feed I have.
So comparing a fresh 2am print against a daytime moving average is apples to oranges anyway. I could dress it up, but it'd still be a daytime line pretending to mean something at 2am.
The honest answer was the boring one. Don't alert overnight unless the data genuinely supports it. Ship the freshness guard, let the night go quiet, and stop pretending a frozen 8pm tick is breaking news.
The takeaway
A timestamp on a quote is not decoration. If your data source can go stale without erroring, and most of them can, you have to check freshness yourself. Nobody's going to throw an exception on your behalf.
The failure mode that gets you isn't the source going down. That's loud, you notice, you handle it. It's the source going quiet while still answering, handing you yesterday's number with today's confidence. Read the timestamp. Trust it more than the price.