Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
46 changes: 38 additions & 8 deletions skills/fomo-kernel/engine/price_feed.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,11 @@
``prices`` itself is optional (#642) when the envelope carries ``fx``: a host
that can read a public FX rate but not every instrument's close can still
repair a book refused only for its missing rate. An envelope must still
declare at least one of the two — see :func:`parse`.
declare at least one of the two — see :func:`parse`. Both tiers above describe
``prices`` specifically: an envelope with none supplied has neither, and
:func:`parse` omits ``coverage`` entirely rather than reporting one it never
earned (#652 — the old ternary read an empty ``prices`` dict as the smaller
tier, ``single_close``, instead of no tier at all).

Validation is fail-closed: a malformed envelope raises :class:`PriceFeedError`
instead of silently pricing part of a portfolio. Prices are money, and a price
Expand Down Expand Up @@ -186,9 +190,10 @@ def _pairs(rows, where, as_of, value_name, value_check):
def parse(payload):
"""Validate a decoded envelope into the engine's normalized feed structure.

Returns ``{"as_of", "source", "prices", "fx", "coverage"}`` where ``prices``
maps engine symbol to ``{close, date, currency, source, history, splits}``.
Raises :class:`PriceFeedError` on anything the engine should not price from.
Returns ``{"as_of", "source", "prices", "fx"}``, plus ``"coverage"`` only
when ``prices`` is non-empty, where ``prices`` maps engine symbol to
``{close, date, currency, source, history, splits}``. Raises
:class:`PriceFeedError` on anything the engine should not price from.

``prices`` is optional (#642): a host whose own price retrieval is blocked
can usually still read one FX rate off a public source even when
Expand All @@ -199,6 +204,12 @@ def parse(payload):
neither ``prices`` nor ``fx`` has nothing for the engine to apply and is
refused below, once both are parsed, so the message can name whichever of
the two is genuinely empty.

``coverage`` states which of the two tiers above ``prices`` reached —
never a claim about ``fx``. An fx-only envelope's ``prices`` is ``{}``, so
there is no tier to report at all: ``coverage`` is omitted rather than
defaulting to ``single_close``, the smaller tier, which would claim one
close per instrument that was never supplied (#652).
"""
if not isinstance(payload, dict):
raise PriceFeedError("price feed must be a JSON object")
Expand Down Expand Up @@ -308,15 +319,28 @@ def parse(payload):
"price feed must declare at least one of prices or fx; an envelope "
"with neither has nothing for the engine to apply")

return {
feed = {
"as_of": as_of.isoformat(),
"source": source,
"prices": prices,
"fx": fx,
"coverage": ("daily_series"
if any(len(row["history"]) > 1 for row in prices.values())
else "single_close"),
}
# #652: `any()` over an empty `prices` is `False`, so this used to fall
# through to the *smaller* tier, `single_close` — a claim of one close per
# instrument for an envelope that supplied zero. `coverage` is a statement
# about `prices` alone (never about `fx`), so when `prices` is empty there
# is no tier to report and the key is omitted entirely, matching this
# module's own "absent, never a placeholder" rule for a fact nothing
# observed (`basis.price_observations`, #618) rather than inventing a new
# sentinel value nothing else in this file uses. Every reader already
# reaches this through `.get("coverage")` (`provenance` below), which
# returns `None` for a missing key exactly as it would for one explicitly
# set to `None` — so nothing downstream needed to change to stay honest.
if prices:
feed["coverage"] = ("daily_series"
if any(len(row["history"]) > 1 for row in prices.values())
else "single_close")
return feed


def load(path):
Expand Down Expand Up @@ -624,6 +648,12 @@ def provenance(*, mode, feed=None, error=None, requested=(), priced=(),
}
if mode == "agent_feed" and feed:
record["source"] = feed.get("source")
# `feed["coverage"]` (the tier: single_close/daily_series) is a
# different fact from `record["coverage"]` above (requested/priced/
# missing counts) despite the shared name. `.get` already degrades to
# `None` for a feed that priced nothing, since #652 made `parse` omit
# the key rather than defaulting it to `single_close` — a claim of one
# close per instrument an fx-only feed never supplied.
record["series"] = feed.get("coverage")
record["sources_by_ticker"] = {ticker: row["source"]
for ticker, row in sorted(feed.get("prices", {}).items())
Expand Down
2 changes: 1 addition & 1 deletion skills/fomo-kernel/references/price-feed.md
Original file line number Diff line number Diff line change
Expand Up @@ -130,7 +130,7 @@ This is why an omitted split is not merely a missing nicety:

## Coverage tiers

Both are accepted; supply what the source actually gives you.
Both are accepted; supply what the source actually gives you. Both describe `prices` specifically — the rate-only shape above supplies neither, and is never reported as if it had (#652): omitting `prices` means there is no tier, not the smaller one.

| Supplied | Restores |
|---|---|
Expand Down
48 changes: 46 additions & 2 deletions tests/test_price_feed.py
Original file line number Diff line number Diff line change
Expand Up @@ -218,13 +218,18 @@ def test_an_fx_only_envelope_is_accepted_with_prices_empty():
ok(omitted["fx"] == {"TWD": {"usd_per_unit": 0.0307, "date": AS_OF,
"source": "Central bank spot rate"}},
"fx 正常解析,缺 per-row source 沿用 feed 級", repr(omitted["fx"]))
ok(omitted["coverage"] == "single_close", "沒有任何 price row → coverage 層級不因此報錯",
omitted["coverage"])
# #652: 沒有任何 price row 就沒有 coverage 層級可言——不是拒收的理由,但也絕不能
# 冒充回報 single_close(那宣稱「每檔一筆收盤」,這份 envelope 一筆都沒供)。見
# test_fx_only_parse_never_reports_a_single_close_coverage_tier 的完整釘樁。
ok("coverage" not in omitted, "沒有任何 price row → coverage 整鍵省略,不冒充任何層級",
omitted.get("coverage", "<absent>"))

explicit_empty = pf.parse({"as_of": AS_OF, "source": "Central bank spot rate", "prices": [],
"fx": [{"currency": "TWD", "usd_per_unit": 0.0307, "date": AS_OF}]})
ok(explicit_empty["prices"] == {} and explicit_empty["fx"] == omitted["fx"],
"prices 顯式空陣列與省略同義", repr(explicit_empty))
ok("coverage" not in explicit_empty,
"顯式空陣列與省略同義,coverage 同樣整鍵省略", explicit_empty.get("coverage", "<absent>"))

# 反向:兩邊都沒有才真正拒收 —— 單獨缺 prices 不再是拒收的理由。
raises(lambda: pf.parse({"as_of": AS_OF, "source": "x"}),
Expand All @@ -248,6 +253,42 @@ def test_fx_only_still_enforces_every_fx_row_rule():
"after as_of", "fx-only 仍拒收晚於 as_of 的匯率日期")


def test_fx_only_parse_never_reports_a_single_close_coverage_tier():
"""#652: #642 made `prices` optional so a host could repair a
missing-FX refusal with a rate-only envelope, but `any()` over an empty
`prices` dict is `False` — the old ternary fell through to the *smaller*
tier, `single_close`, which claims one close per instrument. An envelope
that supplied zero closes must never make that claim, at the parser
(`parse`) and at the propagation point named in the issue's own repro
(`provenance`, which copies `feed["coverage"]` into `record["series"]`).

Byte-stability is the other half: a price-only and a price+history
envelope must keep reporting exactly what they did before this fix."""
fx_only = pf.parse({"as_of": AS_OF, "source": "Bank of Taiwan",
"fx": [{"currency": "TWD", "usd_per_unit": 0.0305, "date": AS_OF}]})
ok(fx_only["prices"] == {}, "fx-only → prices 解析成空 dict", repr(fx_only["prices"]))
ok("coverage" not in fx_only,
"fx-only → parse() 的回傳整個省略 coverage 鍵(不是 None,更不是 single_close)",
fx_only.get("coverage", "<absent>"))

# 對齊 issue 本身重現的呼叫路徑:provenance() 原封不動把 feed.get("coverage")
# 搬進 record["series"]。省略鍵 → .get() 回 None,誠實地說「沒有層級可回報」。
record = pf.provenance(mode="agent_feed", feed=fx_only,
requested=["2330.TW"], priced=[])
ok(record["series"] is None,
"series 隨 coverage 一起誠實消失,不再繼承 single_close 的假話",
repr(record["series"]))
ok(record["coverage"] == {"requested_n": 1, "priced_n": 0, "missing": ["2330.TW"]},
"另一個同名但不同義的 coverage(requested/priced/missing 計數)不受影響",
repr(record["coverage"]))

# 反向釘住:既有兩層 tier 一個位元都不能動——這次修復只補「沒有價」這一格。
ok(pf.parse(envelope())["coverage"] == "single_close",
"只供收盤價的既有 envelope → single_close 不因這次修復而改變")
ok(pf.parse(series_envelope())["coverage"] == "daily_series",
"供 history 的既有 envelope → daily_series 不因這次修復而改變")


def test_engine_accepts_an_fx_only_envelope_and_still_reports_prices_unavailable():
"""The full engine subprocess, not just the pure parser: an fx-only envelope
must load without crashing, and because it prices nothing, `price_provenance`
Expand All @@ -263,6 +304,8 @@ def test_engine_accepts_an_fx_only_envelope_and_still_reports_prices_unavailable
"fx-only envelope 沒有任何收盤價 → mode 仍是 unavailable", repr(prov)[:160])
ok(prov["coverage"] == {"requested_n": 2, "priced_n": 0, "missing": ["AMD", "NVDA"]},
"覆蓋率誠實:0/2,fx-only 不冒充定價成功", repr(prov["coverage"]))
ok("series" not in prov, "mode 不是 agent_feed → series 這個鍵從未被加上",
prov.get("series", "<absent>"))
ok(card["overview"]["unrealized_coverage"]["priced_n"] == 0,
"未實現覆蓋率仍是 0——這份 envelope 從未宣稱能定價,只帶了匯率")

Expand Down Expand Up @@ -738,6 +781,7 @@ def main():
for fn in (test_parse_contract, test_parse_fails_closed,
test_an_fx_only_envelope_is_accepted_with_prices_empty,
test_fx_only_still_enforces_every_fx_row_rule,
test_fx_only_parse_never_reports_a_single_close_coverage_tier,
test_engine_accepts_an_fx_only_envelope_and_still_reports_prices_unavailable,
test_a_supplied_close_is_rebased_onto_the_share_counts_basis,
test_a_close_already_on_the_split_is_not_adjusted_twice,
Expand Down
Loading