Skip to content

[chore·hardening] account_perf's cash_residual gate can KeyError on a currency outside the value-carrying set — latent, currently unreachable from main() #690

Description

@atomchung

Status

Discussion — mechanism-level hardening note from PR #685's investigation into #649, listed in the PR body under "Adjacent findings, deliberately not folded in." No real trade data anywhere below; no maintainer decision yet, and deliberately not framed as a defect with observed user impact — see "Why this is not being filed as a live defect" below.

Owning outcome

account_perf never raises on a data shape it was not specifically built to reject; every failure to compute is a stated {"gate": ...}, matching its own docstring's fail-closed contract ("算不了 → {"note": ...} fail-closed(同 pnl_curve)").

Mechanism

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

  • _fx_getter (:184-242) builds cols, a per-currency daily FX-factor series, only for the currencies passed in as currencies (:217-238). fx_at(c, i) (:240-241) is 1.0 if c == "USD" else cols[c][i] — an unconditional dict index with no .get/default for any currency absent from cols.
  • account_perf (:245-) builds that input set as value_ccys (:262-268): cash_ccys (the union of cash_data["by_currency"] keys and cash-flow-row currencies) unioned with held tickers' currencies. This is deliberately narrower than "every currency that could ever appear anywhere downstream" — by design, per [defect·P1] A currency that appears only in a cash-flow row is added to the USD total at 1.0 — #612's refusal has held currencies as its domain, and no rate is ever requested #649's fix, it is the set that actually carries value.
  • The cash_residual gate (:452-458) evaluates fx_at(r["currency"], n - 1) for every row r in cash_residuals (:454-455). That list is built by cash_reconcile_residuals (perf.py:118-) from ledger snapshot events' own declared cash dicts (_cash_snaps, built at trade_recap.py:2903-2905) — a source independent of cash_flows or cur_map. A ledger snapshot can declare a cash balance in a currency with no matching cash-flow row and no held position in that currency (for example, a residual balance surviving in a third currency the account touched only once, outside the window that still has a flow or holding for it). Such a currency is absent from value_ccys, hence absent from cols, and fx_at would raise KeyError rather than gate cleanly.

Why this is not being filed as a live defect

The investigating PR (#685) characterizes this as pre-existing — not introduced by #649's fix — and unreachable from main() today: no path from the CLI entry point currently constructs a ledger-snapshot-declared cash currency with no corresponding flow or holding that also clears cash_reconcile_residuals's own gates (at least two same-currency snapshots, residual above the abs_floor/pct threshold). This issue records the latent crash mechanism as a hardening note, verified directly by reading the code above, so it is not re-discovered from scratch if a future change — a richer snapshot-refresh flow, for instance — makes that combination reachable.

Evidence

Read against origin/main@e662164, by direct reading of _fx_getter, account_perf, cash_reconcile_residuals, and the call graph from trade_recap.py. No real trade data; no live reproduction attempted, consistent with filing this as a hardening note rather than a reproduced defect.

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