fix(price-feed): an envelope that supplied no close does not report a close-per-instrument coverage tier (closes #652) - #655
Merged
Conversation
… 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>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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 tosingle_closewheneverpricesis empty, becauseany()over an empty dict isFalse. #642 (merged as 878c333) madepricesoptional for an fx-only repair of the #612 refusal, but left this untouched, so a rate-only envelope that priced zero instruments reportedsingle_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
coverageis omitted fromparse's returned dict entirely whenpricesis empty — notNone, not a new sentinel string.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 tonullor 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 constrainscoverageto a closed enum in the first place)..get("coverage")(see next section), which returnsNonefor a missing key exactly as it would for an explicitNone— so omitting costs nothing at any call site and required no code change downstream ofparse.references/price-feed.md's "## Coverage tiers" section andprice_feed.py's module/function docstrings are updated to saycoveragedescribespricesalone (neverfx), and that an fx-only envelope reports no tier.Every reader of
coverage/seriesI found (grepped the whole repo,.py/.md/.json)The field's blast radius is small —
coverage/seriesare pureparse()-output facts, never part of the input JSON schema:skills/fomo-kernel/engine/price_feed.pyparse()itself — the fix.provenance(),record["series"] = feed.get("coverage")— already defensive via.get(); added a clarifying comment only (no logic change). This is the propagation point [defect·P2] #642's fx-only envelope reportssingle_closeprice coverage while supplying no close #652 named: a run withmode="agent_feed"copiesfeed["coverage"]verbatim intorecord["series"].parse()'s own docstring — updated to state the omission.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 assertedomitted["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);coverageis aparse()-computed output field that never appears in the schema at all (confirmed:grep -n coverageon this file returns nothing; the one"series"hit is unrelated prose inhistory's description). Nothing here constrainscoverageto an enum, so omitting the key is schema-compatible with zero edits."coverage"appears in this repo (portfolio_basis.py,trade_recap.py's sector/valuation coverage,card_renderer.py:1453'sprovenance.get("coverage"),review.py's manyprojected["coverage"]sites, severalschemas/*.jsonfiles) is a different, unrelated concept that happens to share the word — sector-attribution coverage, portfolio valuation coverage, orprice_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 thanfeed["coverage"]/record["series"]; none of them go nearparse()'s output. The literal strings"single_close"/"daily_series"occur nowhere else in the repo (.py/.md/.json, includingdocs/,evals/) exceptprice_feed.pyandtests/test_price_feed.py.Price-only and price+history envelopes are byte-stable:
coverageis computed identically to before wheneverpricesis non-empty, pinned by an explicit assertion in the new test.agent_feedreachability question — answered with evidenceThe 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 itsfeedvariable:_resolve_consider_pricesnever touches a--prices-supplied envelope, andmarket_data.resolve()documents the same precedence on thepreparelane: "a supplied envelope answers for everything it declares and no Yahoo request is made at all." So the two origins never combine into onefeedin 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:— it checks
supplied's truthiness before checking whether anything was priced, unliketrade_recap.py's equivalent selector (main(), ~line 2677), which checkspx is Nonefirst. Becausecmd_considerreusesfeedfor both origins above, an ordinaryconsidercall with no--pricesflag at all, whose own live resolution succeeds, is reported asmode: "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--pricespassed):No agent envelope was ever involved. I also checked the narrower fx-only-via-
--pricescase directly: it does yieldmode: "agent_feed"withpriced_n: 0(arguably correct — an envelope really was supplied), but it's invisible in practice becausecmd_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 (viacpfrom a saved copy — nevergit checkout, so no uncommitted work was at risk), cleared__pycache__, and re-rantests/test_price_feed.py:Exactly the 4 assertions guarding this fix went red, each showing the reintroduced
single_closevalue. Restored the fix viacpfrom the saved copy, cleared__pycache__again, re-ran: 160/160 green, andgit diffagainst the saved fixed copy confirmed byte-identical restoration.Test suite
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.pystandalone: 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). Theagent_feedmode-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