Skip to content

fix(price-feed): an envelope that supplied no close does not report a close-per-instrument coverage tier (closes #652) - #655

Merged
atomchung merged 1 commit into
mainfrom
claude/issue-652-fx-only-coverage
Jul 31, 2026
Merged

fix(price-feed): an envelope that supplied no close does not report a close-per-instrument coverage tier (closes #652)#655
atomchung merged 1 commit into
mainfrom
claude/issue-652-fx-only-coverage

Conversation

@atomchung

Copy link
Copy Markdown
Owner

What

price_feed.parse's coverage ternary — "daily_series" if any(len(row["history"]) > 1 for row in prices.values()) else "single_close" — falls through to single_close whenever prices is empty, because any() over an empty dict is False. #642 (merged as 878c333) made prices optional for an fx-only repair of the #612 refusal, but left this untouched, so a rate-only envelope that priced zero instruments reported single_close — a claim of one close per instrument it never supplied. This is #642's own acceptance item 4, unimplemented. Closes #652.

Coverage value chosen, and why

coverage is omitted from parse's returned dict entirely when prices is empty — not None, not a new sentinel string.

  • The owner's own acceptance wording offered two options: "an honest non-price coverage state (or omit price coverage)". Omission is named explicitly.
  • This repository already has a stated idiom for exactly this shape of fact — "absent, never a placeholder" — for basis.price_observations ([implementation·M1] A consider answer states which day's prices it was computed from #618, same price/valuation domain): a fact nothing observed is left out rather than defaulted to null or a stand-in value. Reusing that idiom instead of inventing a new enum value (e.g. "no_prices") is the smaller change: no new vocabulary for every reader to learn, and no schema/whitelist anywhere needs to grow a value (verified below — nothing constrains coverage to a closed enum in the first place).
  • Every actual reader of the field already uses .get("coverage") (see next section), which returns None for a missing key exactly as it would for an explicit None — so omitting costs nothing at any call site and required no code change downstream of parse.

references/price-feed.md's "## Coverage tiers" section and price_feed.py's module/function docstrings are updated to say coverage describes prices alone (never fx), and that an fx-only envelope reports no tier.

Every reader of coverage / series I found (grepped the whole repo, .py/.md/.json)

The field's blast radius is small — coverage/series are pure parse()-output facts, never part of the input JSON schema:

  • skills/fomo-kernel/engine/price_feed.py
  • tests/test_price_feed.py — fixed the one existing assertion that pinned the wrong value (test_an_fx_only_envelope_is_accepted_with_prices_empty, previously asserted omitted["coverage"] == "single_close"), and added a new focused test (below).
  • skills/fomo-kernel/references/price-feed.md — "## Coverage tiers" section, one added sentence.
  • skills/fomo-kernel/schemas/price-feed.schema.json — checked, no change needed. This schema validates only the input envelope (as_of/source/prices/fx); coverage is a parse()-computed output field that never appears in the schema at all (confirmed: grep -n coverage on this file returns nothing; the one "series" hit is unrelated prose in history's description). Nothing here constrains coverage to an enum, so omitting the key is schema-compatible with zero edits.
  • Everywhere else "coverage" appears in this repo (portfolio_basis.py, trade_recap.py's sector/valuation coverage, card_renderer.py:1453's provenance.get("coverage"), review.py's many projected["coverage"] sites, several schemas/*.json files) is a different, unrelated concept that happens to share the word — sector-attribution coverage, portfolio valuation coverage, or price_feed.provenance's own {requested_n, priced_n, missing} sub-object (also confusingly named "coverage", but a pre-existing, separate field this PR does not touch). Verified each of these reads from a different dict than feed["coverage"]/record["series"]; none of them go near parse()'s output. The literal strings "single_close" / "daily_series" occur nowhere else in the repo (.py/.md/.json, including docs/, evals/) except price_feed.py and tests/test_price_feed.py.

Price-only and price+history envelopes are byte-stable: coverage is computed identically to before whenever prices is non-empty, pinned by an explicit assertion in the new test.

agent_feed reachability question — answered with evidence

The exact posture #652 asked about — the engine's own retrieval succeeding and the agent separately supplying an fx-only envelope, blended into one run — is not reachable. cmd_consider (review.py ~6661-6678) takes exactly one of two mutually exclusive branches for its feed variable:

if args.prices:
    feed = price_feed.load(...)                          # a real agent envelope
else:
    feed, market_bundle = _resolve_consider_prices(...)   # #605 §E: the engine's OWN live resolution

_resolve_consider_prices never touches a --prices-supplied envelope, and market_data.resolve() documents the same precedence on the prepare lane: "a supplied envelope answers for everything it declares and no Yahoo request is made at all." So the two origins never combine into one feed in a single call, on either lane.

However, I found and reproduced a broader, definitely-reachable sibling defect one level up, and opened it separately: #654. _consider_price_feed_status's mode selector (review.py:3679) is:

mode=("agent_feed" if supplied else ("engine_fetch" if priced else "unavailable")),

— it checks supplied's truthiness before checking whether anything was priced, unlike trade_recap.py's equivalent selector (main(), ~line 2677), which checks px is None first. Because cmd_consider reuses feed for both origins above, an ordinary consider call with no --prices flag at all, whose own live resolution succeeds, is reported as mode: "agent_feed".

Reproduced against this repo's own existing fake-provider harness (tests/test_consider.py's _FAKE_PROVIDER/_with_fake_provider, driven unmodified from a standalone script — no --prices passed):

provider.log (proves live resolution ran): AMAT,AMD,ARM,AVGO,MRVL,MU,NVDA,PLTR,TSM
payload["price_feed"]["provenance"]:
{
  "mode": "agent_feed",
  "source": "yahoo (engine resolver)",
  "sources_by_ticker": {"AMD": "yahoo (engine resolver)", "MRVL": "...", "NVDA": "..."},
  "coverage": {"requested_n": 3, "priced_n": 3, "missing": []}
}

No agent envelope was ever involved. I also checked the narrower fx-only-via---prices case directly: it does yield mode: "agent_feed" with priced_n: 0 (arguably correct — an envelope really was supplied), but it's invisible in practice because cmd_consider's existing #629 refusal (valuation_basis == "unpriced" and recovery.attempted, ~line 6803) fires before that payload is ever emitted — matching #652's own verified claim that this path returns only {"error", "status"}.

Filed as #654 rather than folded in here: fixing it needs an explicit call-origin signal threaded through cmd_consider_resolve_consider_prices_consider_price_feed_status, which is a provenance-mode redesign, not a one-line change, and the task instructions for this PR were explicit that such a redesign must not be silently absorbed into this fix.

Mutation verification

Reverted parse()'s return statement to the exact pre-fix ternary (via cp from a saved copy — never git checkout, so no uncommitted work was at risk), cleared __pycache__, and re-ran tests/test_price_feed.py:

❌ 失敗(156/160 項)
❌ FAIL: 沒有任何 price row → coverage 整鍵省略,不冒充任何層級  single_close
❌ FAIL: 顯式空陣列與省略同義,coverage 同樣整鍵省略  single_close
❌ FAIL: fx-only → parse() 的回傳整個省略 coverage 鍵(不是 None,更不是 single_close)  single_close
❌ FAIL: series 隨 coverage 一起誠實消失,不再繼承 single_close 的假話  'single_close'

Exactly the 4 assertions guarding this fix went red, each showing the reintroduced single_close value. Restored the fix via cp from the saved copy, cleared __pycache__ again, re-ran: 160/160 green, and git diff against the saved fixed copy confirmed byte-identical restoration.

Test suite

python3 tests/run_all.py
...
PASS: all 49 suites passed.

Exit code 0. Confirmed optional deps are actually installed in this environment (review.py doctor: yfinance/pandas/rich all [ok]), so the suite is exercising real code paths rather than skip-and-still-print-passed. tests/test_price_feed.py standalone: 160/160 (up from 154 before this change — the 6 new assertions in the added test, all executed, none skipped).

Scope discipline

No new architecture: no second provider, no alternate normalization path, no market-data abstraction. provenance() required no logic change — only a comment. trade_recap.py's own mode selector is untouched (already correct; confirmed by trace). The agent_feed mode-selector defect is filed separately (#654) rather than folded in, per the task's explicit instruction not to silently widen this into a provenance-mode redesign.

Co-Authored-By: Claude Opus 5 noreply@anthropic.com

… close-per-instrument coverage tier (closes #652)

#642 (closes-#642, merged as 878c333) made `prices` optional so a host could
repair a #612 missing-FX refusal with a rate-only envelope. Its own acceptance
item 4 required that an fx-only parse "give an honest non-price coverage
state (or omit price coverage); never label it single_close" -- but the
coverage ternary was left untouched: `any()` over an empty `prices` dict is
`False`, so it fell through to the *smaller* tier, `single_close`, which
claims one close per instrument that was never supplied. The same value
propagates into `price_feed.provenance`'s `record["series"]`, so a run priced
0/1 could report `series: "single_close"` in the same breath as
`priced_n: 0`.

`parse` now omits `coverage` entirely when `prices` is empty, rather than
defaulting it to either tier -- matching this repository's existing "absent,
never a placeholder" idiom for a fact nothing observed (`basis.price_observations`,
#618), instead of inventing a new sentinel value. `coverage` is a statement
about `prices` alone and never about `fx`; the two-tier table in
`references/price-feed.md` and the parser's module/function docstrings are
updated to say so. `provenance`'s `record["series"] = feed.get("coverage")`
needed no code change -- `.get` already returns `None` for a missing key -- so
only a clarifying comment was added there.

Price-only and price+history envelopes are untouched: `coverage` is computed
identically to before whenever `prices` is non-empty, pinned by an explicit
byte-stability assertion alongside the new fx-only test.

Also investigated and answered with evidence: whether `mode="agent_feed"`
with zero feed-supplied closes is reachable when the engine's own retrieval
also succeeds. It is not, in the form #652 asked about -- `cmd_consider`'s
`if args.prices: / else: _resolve_consider_prices(...)` branches are mutually
exclusive, so an agent-supplied envelope and the engine's own live
resolution never combine into one `feed` in the same call, mirroring
`market_data.resolve`'s "a supplied envelope answers for everything it
declares" precedence on the prepare lane. Reproduced instead a broader,
definitely-reachable sibling defect one level up: `consider` with **no**
`--prices` at all, whose own live resolution succeeds, is unconditionally
reported as `mode: "agent_feed"` (`source: "yahoo (engine resolver)"`)
because `_consider_price_feed_status`'s selector checks `supplied`'s
truthiness before checking whether anything was priced, unlike
`trade_recap.py`'s equivalent selector. Filed as #654 rather than folded into
this fix, since resolving it requires threading an explicit call-origin
signal through `cmd_consider` -- a provenance-mode redesign, not a one-line
change here.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@atomchung
atomchung merged commit 9d85633 into main Jul 31, 2026
3 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[defect·P2] #642's fx-only envelope reports single_close price coverage while supplying no close

1 participant