DEV Community

Devil Scrapes
Devil Scrapes

Posted on

Why 43% of Yahoo Finance's 1-Minute Candles Come Back Null

Quick answer

Ask Yahoo Finance's chart endpoint for 1-minute candles on a real, actively-traded stock and a chunk of them come back with open, high, low, close, and volume all null — not zero, not missing from the array, genuinely null, sitting at the correct timestamp. We pulled live 1-minute bars for GSAT (Globalstar, NASDAQ) today and got 170 null closes out of 391 candles — 43.5% — scattered across 168 separate null/non-null transitions through the trading session, not one gap at the open or close. SIRI showed 18 nulls (4.6%) the same day; AAPL showed zero. The daily interval never does this. The 1-minute interval does, and only for names that don't trade every single minute.

The Yahoo Finance Quote Scraper preserves those nulls exactly as the source emits them, rather than zero-filling them into a silently wrong chart.

What actually happens when a minute has no trade 👻

Yahoo's /v8/finance/chart/{symbol} endpoint returns one flat array per OHLCV field, index-aligned to a shared timestamp array. The assumption most people bring to that shape is "every timestamp has a price." It doesn't. If nobody traded the ticker during that specific minute, Yahoo still emits the timestamp — but every field in indicators.quote[0] at that index comes back null.

Here's the live probe, run today against the real endpoint with the same curl-cffi client this Actor ships:

from curl_cffi.requests import AsyncSession
import asyncio, datetime

async def main():
    async with AsyncSession() as s:
        r = await s.get(
            "https://query1.finance.yahoo.com/v8/finance/chart/GSAT",
            params={"range": "1d", "interval": "1m"},
            impersonate="chrome131", timeout=30,
        )
        result = r.json()["chart"]["result"][0]
        ts, q = result["timestamp"], result["indicators"]["quote"][0]
        nulls = [i for i, c in enumerate(q["close"]) if c is None]
        print(f"{len(nulls)}/{len(ts)} null closes")
        i = nulls[0]
        print(datetime.datetime.fromtimestamp(ts[i], tz=datetime.UTC),
              "open=", q["open"][i], "volume=", q["volume"][i])

asyncio.run(main())
Enter fullscreen mode Exit fullscreen mode
170/391 null closes
2026-09-11 13:37:00+00:00 open= None volume= None
Enter fullscreen mode Exit fullscreen mode

That's minute 7 of the session — well into normal trading hours, not a pre-market artifact. regularMarketVolume for the day was 347,041 shares, so GSAT absolutely traded — just not in every 60-second bucket Yahoo reports on. Widen the window and the picture holds: 5 days of 1-minute bars on GSAT came back 38.6% null (754 of 1,951 candles). Zoom out to the 1d/interval=1d combination — this Actor's own default — and the nulls disappear entirely: 22 daily candles, zero nulls, because a full trading day almost never passes with zero prints.

The failure mode this causes downstream is boring and immediate: sum(c["close"] for c in candles) / len(candles) on a null-bearing list doesn't give you a wrong average — it raises TypeError: unsupported operand type(s) for +: 'float' and 'NoneType' and kills your script the first time you point it at anything less liquid than a mega-cap.

Why the default input hides this from you

The reason most people never hit this: the Actor's own range/interval defaults are 1mo / 1d, and daily bars don't have the problem — a session either happened or it's absent from the array entirely, it doesn't get partially reported. The null-candle behavior is opt-in risk: it only shows up once you ask for interval: "1m" (or 5m/15m) on a symbol that isn't AAPL-tier liquid. That's exactly the combination a lot of backtesting and screening use cases reach for — sub-daily bars across a watchlist that includes names outside the S&P mega-caps — so it's worth knowing before your pipeline is the one that finds out.

This Actor's own model layer was built anticipating exactly this: Candle.close, .open, .high, .low, and .volume are all float | None / int | None on purpose, and the test suite ships fixtures with "close": [226.01, null] and asserts the null survives the parse untouched — no silent coercion to 0.0, no dropped row. candle_count on each output row counts every index-aligned candle Yahoo returned, nulls included, so a customer scanning for a suspiciously low candle_count on an interval="1m" run should check for nulls-within-candles, not just missing candles.

What we handle for you 🛡️

Every fetch goes out through curl-cffi with real Chrome/Firefox/Safari TLS impersonation, rotated per retry attempt, and we back off exponentially (2s → 30s cap, 5 attempts) on 408/429/5xx, honouring Retry-After when Yahoo sends it. A genuinely delisted symbol comes back as a clean 404 with chart.error populated — we treat that as terminal and don't burn retries on it, but one bad ticker in a batch of a thousand never takes down the other 999: fault isolation is per-symbol, and the run only fails if every requested symbol comes back empty. Apify Proxy routes every request, with RESIDENTIAL selectable if a run needs it. And as shown above, we never invent a 0.0 where Yahoo said null — your dataset carries the same gaps the source carries, typed and index-aligned, so you can decide how to handle them instead of us deciding for you.

Output

{
  "symbol": "GSAT",
  "currency": "USD",
  "exchange_name": "NMS",
  "instrument_type": "EQUITY",
  "regular_market_price": 82.6,
  "requested_range": "1d",
  "requested_interval": "1m",
  "candle_count": 391,
  "candles": [
    { "timestamp": "2026-09-11T13:30:00Z", "open": 82.6, "high": 82.6, "low": 82.6, "close": 82.6, "volume": 11190 },
    { "timestamp": "2026-09-11T13:37:00Z", "open": null, "high": null, "low": null, "close": null, "volume": null }
  ]
}
Enter fullscreen mode Exit fullscreen mode
from apify_client import ApifyClient

client = ApifyClient("<YOUR_API_TOKEN>")
run = client.actor("DevilScrapes/yahoo-finance-quote-scraper").call(
    run_input={"symbols": ["GSAT", "AAPL"], "range": "1d", "interval": "1m"}
)
for item in client.dataset(run["defaultDatasetId"]).iterate_items():
    real = [c for c in item["candles"] if c["close"] is not None]
    print(item["symbol"], f"{len(real)}/{item['candle_count']} candles with a real print")
Enter fullscreen mode Exit fullscreen mode

Pricing: $0.20 flat per run plus $0.006 per symbol returned — $6.20 per 1,000 symbols. Every new Apify account gets $5 of free credit, no card required to try it.

Yahoo Finance Quote Scraper on Apify

FAQ

Does this happen on daily candles too?

Not in what we measured: range=1mo, interval=1d on the same GSAT ticker returned 22 candles, zero nulls. The null-candle pattern only showed up at 1-minute granularity.

Is this a bug in the Actor?

No — it's Yahoo's own data shape. The Actor's Candle model makes every OHLCV field nullable on purpose and its test suite has fixtures encoding exactly this case; we pass the source's nulls through rather than silently zero-filling them.

Which symbols are affected?

Anything that doesn't print a trade every single minute of the session. Mega-caps like AAPL showed zero nulls in our probe; a mid-liquidity name like SIRI showed 4.6%; a lower-liquidity name like GSAT showed 43.5%.


Built by Devil Scrapes. We rotate real browser TLS fingerprints, retry with backoff, and pass through the source's own nulls instead of quietly inventing zeros for you.

Top comments (0)