Skip to content

feat(web): the Balances view, from what a cycle recorded (#702) - #720

Merged
eaitbrahim merged 2 commits into
mainfrom
feat-702-balances-view
Sep 4, 2026
Merged

feat(web): the Balances view, from what a cycle recorded (#702)#720
eaitbrahim merged 2 commits into
mainfrom
feat-702-balances-view

Conversation

@eaitbrahim

Copy link
Copy Markdown
Contributor

#702 specified "a live adapter read". That was refused, deliberately and on the record.

keel serve is a loopback reader over SQLite with no credentials, no broker handle and no outbound network -- gather_status says "no broker, no network", and even the Venues screen reads adapter declarations with "no broker handle, no network, no config, no credentials". A balances page is the obvious place to break that line, and breaking it would put venue credentials into the one process a browser can reach and hand an operator's rate limit to every tab left open on a view that re-polls every 15 seconds (main.js's POLL_MS). Hundreds of REST calls an hour, during trading windows, for a number nobody asked to refresh.

What replaced it is better rather than merely safer. Cash comes from equity_points (#698): the figure the agent READ AND SIZED AGAINST when it last evaluated the rails, stamped with when it read it. A fresher number the engine never saw explains nothing about why it did what it did.

READ THROUGH THE MODE, ALWAYS. equity_points holds paper and live rows in one database, so the newest row FOR THE MODE IN FORCE is the answer -- the newest row of any mode would show a $10,000 synthetic balance on a live deployment holding $250. paper_cash_usdc is withheld entirely in live mode even though the key survives a paper->live flip: a synthetic balance beside real money is the most confusing thing this page could show. An unstamped mode, before the first cycle, yields no cash rather than a guess about which account to read.

WHAT IS NOT RECORDED IS NAMED. #702 was written around settled-versus-total cash, and that distinction is not in the database: equity_points.cash comes from _fetch_available_quote, which reads Balance.available and stops, summed across every currency in play -- so the Alpaca adapter's T+1 clamp reaches storage with the distinction already flattened out of it. The tiles render as absent with UNRECORDED IN CYCLE SNAPSHOT beside them, rather than showing the available figure under a "settled" label that would answer the question this page exists to ask honestly. #719 records the pair; the report and payload were shaped so that lands as a value change, not a shape change.

A PARTIAL SUM IS THE DANGEROUS SHAPE. An asset whose tranches are not all priced reports its QUANTITY and no value. Summing over the priced subset would look like a total and be a smaller one -- and a holding rendered as worth less than it is, on the page an operator checks to see what they have, is the failure worth engineering against. What is held is a fact; what it is worth was not observed.

NO ACTION, ANYWHERE. #702's refusal, pinned on the payload AND on the view source: no buying power, no deposit, no withdraw, no transfer, not even an event listener. Cash is a fact, not an affordance, and keel is cash-spot by constitution -- a buying-power tile would advertise leverage the engine refuses to take. It is this page's version of the close button the Positions view also does not have.

The per-asset rows fold gather_positions (#701) rather than re-reading the ledger, so Balances and Positions cannot come to disagree about what is held; every figure carries the instant it was recorded, including each asset's mark.

One test-quality note worth keeping: the mode-partition test first passed against a report that ignored mode entirely, because its fixture made the live row the newest of the two. The fixture now makes the PAPER row newest, which is what makes the filter observable -- found by mutation, not by reading.

What & why

Tests-first evidence

  • Tests written first, seen failing for the right reason

Gates (all must pass)

  • uv run ruff check clean
  • uv run mypy clean
  • uv run pytest -q green

Scope check

  • This PR touches a rail or a default classification — checked means it DOES;
    leave checked only if true, and if so: cite the source and open the discussion
    BEFORE review (CONTRIBUTING.md, "Governance: rulings vs. machinery").
  • New dependency added (needs discussion first)

eaitbrahim and others added 2 commits September 4, 2026 14:20
#702 specified "a live adapter read". That was refused, deliberately and on the record.

`keel serve` is a loopback reader over SQLite with no credentials, no broker handle and no
outbound network -- `gather_status` says "no broker, no network", and even the Venues
screen reads adapter declarations with "no broker handle, no network, no config, no
credentials". A balances page is the obvious place to break that line, and breaking it
would put venue credentials into the one process a browser can reach and hand an
operator's rate limit to every tab left open on a view that re-polls every 15 seconds
(`main.js`'s `POLL_MS`). Hundreds of REST calls an hour, during trading windows, for a
number nobody asked to refresh.

What replaced it is better rather than merely safer. Cash comes from `equity_points`
(#698): the figure the agent READ AND SIZED AGAINST when it last evaluated the rails,
stamped with when it read it. A fresher number the engine never saw explains nothing about
why it did what it did.

READ THROUGH THE MODE, ALWAYS. `equity_points` holds paper and live rows in one database,
so the newest row FOR THE MODE IN FORCE is the answer -- the newest row of any mode would
show a $10,000 synthetic balance on a live deployment holding $250. `paper_cash_usdc` is
withheld entirely in live mode even though the key survives a paper->live flip: a synthetic
balance beside real money is the most confusing thing this page could show. An unstamped
mode, before the first cycle, yields no cash rather than a guess about which account to
read.

WHAT IS NOT RECORDED IS NAMED. #702 was written around settled-versus-total cash, and that
distinction is not in the database: `equity_points.cash` comes from
`_fetch_available_quote`, which reads `Balance.available` and stops, summed across every
currency in play -- so the Alpaca adapter's T+1 clamp reaches storage with the distinction
already flattened out of it. The tiles render as absent with `UNRECORDED IN CYCLE
SNAPSHOT` beside them, rather than showing the available figure under a "settled" label
that would answer the question this page exists to ask honestly. #719 records the pair; the
report and payload were shaped so that lands as a value change, not a shape change.

A PARTIAL SUM IS THE DANGEROUS SHAPE. An asset whose tranches are not all priced reports
its QUANTITY and no value. Summing over the priced subset would look like a total and be a
smaller one -- and a holding rendered as worth less than it is, on the page an operator
checks to see what they have, is the failure worth engineering against. What is held is a
fact; what it is worth was not observed.

NO ACTION, ANYWHERE. #702's refusal, pinned on the payload AND on the view source: no
buying power, no deposit, no withdraw, no transfer, not even an event listener. Cash is a
fact, not an affordance, and keel is cash-spot by constitution -- a buying-power tile would
advertise leverage the engine refuses to take. It is this page's version of the close
button the Positions view also does not have.

The per-asset rows fold `gather_positions` (#701) rather than re-reading the ledger, so
Balances and Positions cannot come to disagree about what is held; every figure carries the
instant it was recorded, including each asset's mark.

One test-quality note worth keeping: the mode-partition test first passed against a report
that ignored `mode` entirely, because its fixture made the live row the newest of the two.
The fixture now makes the PAPER row newest, which is what makes the filter observable --
found by mutation, not by reading.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Review findings on this branch. The first two are the ones worth reading; both are tests
that passed for reasons other than the behaviour they name.

THE MIXED-MARK GUARD COULD NOT FIRE, AND ITS TEST DID NOT REACH IT. `AssetBalanceRow`
withholds a product's VALUE when any tranche of it lacks a mark, because a sum over the
priced subset looks like a total and is a smaller one -- a holding rendered as worth less
than it is, on the page an operator opens to see what they have. The test named for that
seeded two different PRODUCTS rather than two tranches of one, so it never reached the
branch: replacing `any(mark is None for mark in marks)` with `held[0].mark is None` left
every test green.

And the rule as written described a state that cannot occur. `gather_positions` reads the
mark ONCE PER PRODUCT and hands every tranche of it the same figure, so `any(...)` is
provably equivalent to the first row's mark -- through that caller. The guard is real but
DEFENSIVE: what it protects is the fold, which a caller assembling rows from more than one
read, or a mark cache that stopped being per-product, would reach immediately. Pinned now
at `_assets_from` with hand-built rows, which is the only way to produce the state, and the
prose says defensive instead of implying observed.

THREE VIEWS HAD NO KEY-PARITY CHECK AT ALL. `balancesView` was missing from
`_VIEW_ENDPOINTS` -- and so were `positionsView` and `ordersView`, from #701 and #700.
Renaming `data.hwm` to `data.high_water_mark` in the balances view left the entire web
suite green while the tile would have rendered blank forever, with nothing in the console
naming the gap. That is precisely the failure `test_every_ported_view_reads_only_keys_its_
endpoint_sends` was written for. All three added; the scan finds 13, 4 and 11 keys
respectively, so none of them is passing vacuously.

THE PAGE PAID FOR A VERDICT IT NEVER SHOWS. Measured: three products cost 12 candle reads
and a rules read per request -- four per product, three of them computing `entry_bar_ready`
across every configured granularity -- on an endpoint the console re-polls every 15
seconds. Balances renders quantity, mark and value and no gate verdict. `gather_positions`
now takes `with_readiness=False`, so the mark logic stays single-sourced rather than being
copied here, and the skipped rows carry `ready=False, ready_reason=None` -- a pair
`entry_bar_ready` never returns, so work that was skipped cannot be mistaken for a gate
that refused.

A DOCSTRING THAT OVER-CLAIMED. `mode` was described as "the partition every figure below is
read through". True of cash, equity, unrealized, hwm and paper_cash; false of `assets`,
because the `positions` table has no `mode` column. On a database that has flipped
paper->live this page shows live cash beside holdings that may predate the flip. The
docstring now says so and names the engine change that would fix it, rather than implying
this report could infer it after the fact.

Smaller: the "marked at" column declared `key: "mark_as_of"`, which `/api/balances` does not
list as sortable -- a header that looks clickable and is not; product ordering and the
per-product mark source are pinned rather than only argued (both mutations passed before);
and the payload case where a cycle DID record a reading whose split was `None` --
`recorded` true and `cash` absent together, the distinction `has_recorded_cash` exists to
draw -- is now covered.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@eaitbrahim
eaitbrahim merged commit 9fc3f5b into main Sep 4, 2026
4 checks passed
@eaitbrahim
eaitbrahim deleted the feat-702-balances-view branch September 4, 2026 18:58
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.

1 participant