Financebotresearch desk研究台

Playbook › Playbook

yfinance fast_info.previous_close is unreliable — it corrupts every computed day change

static 2026-09-26

Claim

.mcp/fin.py --px computed its day-change column as last_price / previous_close - 1 using yf.Ticker(t).fast_info. For AKAM on 2026-09-26 this printed −14.47%. The truth, from get_stock_info on the same ticker the same minute:

field value
previousClose 110.41
currentPrice 113.94
regularMarketChangePercent +3.20

And directly from fast_info on the same call:

accessor value
previous_close 133.21 ← wrong
regular_market_previous_close 110.41 ← correct
last_price 113.94

fast_info exposes both fields, and only the longer-named one is the prior regular-session close. previous_close appears to be sourced from chart metadata and can drift to an unrelated session, most visibly on a name that gapped.

Why it is worth a note

The failure is silent and directional. It does not raise, does not return None, and produces a plausible-looking percentage, so nothing downstream can detect it. On 2026-09-26 it did two distinct kinds of damage in one scan:

  1. It invented an event. AKAM appeared as the worst name in the universe at −14.5%, which read as a CDN/security rout. AKAM had in fact just signed a 7-year, $11.6B agreement with Anthropic and closed up 3.2%.
  2. It hid a real one. The same sweep reported ZS at −6.4% when the true figure was −10.1% — the largest single-name move on the board, and the trigger of a genuine sector de-rate.

So the error is not noise that averages out. It manufactures false signal and suppresses true signal at the same time, which is the worst possible shape for a number that feeds a movers list.

Blast radius

Anything consuming fin.py --px: /watchlist-scan, /daily-brief, .mcp/alerts.py, .mcp/news.py. Prices themselves were always correct — last_price is fine. Only the percent column was affected, so no zone check, fair-value comparison or trim level was ever wrong because of this. The damage was confined to "what moved today," which is exactly what a morning brief is for.

The rule

Never compute a day change from fast_info.previous_close. Use fast_info.regular_market_previous_close, or take regularMarketChangePercent from get_stock_info when a single name has to be right. Fixed in .mcp/fin.py on 2026-09-26 with the fallback fi.regular_market_previous_close or fi.previous_close.

The generalisation is the standing rule, not the specific field: a vendor accessor whose name sounds like the number wanted is not the same as the number wanted, and a computed percentage is the easiest place for that substitution to hide. Cross-check one instance against a second endpoint before trusting a whole column. ⧉ [[principle-primary-source-beats-vendor]]

Related

[[pitfall-stale-entry-zone-suppresses-a-name]] — same week, same scan, same shape: a mechanism that quietly removes a name from view rather than reporting something false about it.