Skip to content

[defect·P2] A permanently-unpriced symbol makes the cache refuse the exact request that produced it — every same-day command re-fetches the whole universe #687

Description

@atomchung

Status

Discussion — mechanism-level finding from PR #684's investigation into #665. No real trade data anywhere below; no maintainer decision yet.

Owning outcome

A same-day prepare bundle should answer a later same-day resume/consider/add-cash on the same book for free — that reuse is the entire point of the same-day market-data cache (#605 §D). It should hold for every book, not only ones where every requested symbol eventually prices.

Mechanism

skills/fomo-kernel/engine/market_data.py (line numbers against origin/main@e662164):

  • MarketDataBundle.covers (:322-366) requires the bundle's priced set to be a superset of the request's instruments/benchmarks (:353-354: if set(request["instruments"]) - priced: return False). priced (:288-295) is the set of columns with at least one non-NaN close. A permanently-unpriced symbol — delisted, or simply misspelled, both ordinary in a real TW/US book — has an all-NaN column and is therefore never in priced, for any bundle fetched at any time.
  • The superset rule is deliberate and documented at :345-350: "a bundle that requested {GOOD, DEAD} and priced only GOOD would otherwise 'cover' a later request for DEAD, hand back a bundle with no DEAD price, and suppress the retry that a transient outage deserves (external review, finding 5)." That rationale is correct for a transient outage — retrying is the whole point.
  • _cache_load (:763-778), the same-day disk cache, gates every lookup on bundle.covers(request) (:776). The sharp edge: once a permanently-unpriced symbol is in the request, covers returns False even against the exact request that produced the cached bundle — a request for {GOOD, DEAD} stores a bundle whose own priced set excludes DEAD, so re-asking that identical request the same day still fails covers and misses the cache it just wrote.
  • _from_yahoo (:630-679) falls through to a fresh fetch of request_universe(request) — the whole universe, not just the missing symbol (:650-652) — whenever _cache_load returns None (:646-648).
  • resolve (:795-844) routes both the in-process memo (:837-840) and the disk cache through the same covers call, so neither reuse path is available once a permanently-unpriced symbol is anywhere in the request.

Net effect: a book with one delisted or misspelled symbol anywhere in its instruments loses same-day caching entirely, for itself. Every prepare, resume, consider, and add-cash call on that book re-fetches every other symbol too, at full provider cost, for no informational gain — the permanently-unpriced symbol was never going to price differently on a second same-day call.

Why this is not simply #665

#665 (closed, fixed by PR #684) was one symptom: add-cash's drift gate mistook the consequence of this re-fetch — a second live resolution disagreeing with the first because the provider moved between calls — for the anchor itself having broken something. PR #684's fix is a frozen-frame carve-out scoped to add-cash only (amending_session=True re-entering _prepare_session, _from_frozen_frame in market_data.py): it makes add-cash reuse the exact frozen frame regardless of what covers says, but does nothing for resume, consider, or a second plain prepare the same day on the same book. The re-fetch-the-whole-universe cost this issue describes is still paid by every one of those routes.

Evidence

Read against origin/main@e662164 (post-#684/#685 merge). No real trade data; this is a code-mechanism finding traced by direct reading of covers/_cache_load/_from_yahoo/resolve, not a live-provider reproduction.

Cross-references

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions