Skip to content

feat(web): the Positions view -- marked at the rails' own price (#701… - #717

Merged
eaitbrahim merged 2 commits into
mainfrom
feat-701-positions-view
Sep 4, 2026
Merged

feat(web): the Positions view -- marked at the rails' own price (#701…#717
eaitbrahim merged 2 commits into
mainfrom
feat-701-positions-view

Conversation

@eaitbrahim

Copy link
Copy Markdown
Contributor

…, items 1-4)

keel status showed a tranche's id, product, rule, qty, entry price and bracket, and web/payload.py::_position_payload explained in its own docstring why it showed no P&L: "OpenPositionStatus carries neither, so emitting one would mean this layer multiplied qty by entry_price... the fix, if it is wanted, is upstream." This is that fix, upstream, plus the endpoint and view that read it.

THE MARK IS THE RAILS' MARK, AND THAT IS THE WHOLE POINT. agent._mark_to_market_parts values a holding at repo.get_candles(product, finest)[-1].close; gather_positions makes the same read through the same _finest_granularity. A positions page quoting a different current price would be a second answer to "what is this worth", and since the first answer moved rail 11's drawdown scalars, the page would be the wrong one.

That is asserted across the module boundary rather than claimed. The two readers do NOT share a source for QUANTITY: the agent counts inventory from the filled-orders log -- never from positions, deliberately, after the phantom-drawdown bug that taught it -- and this report reads the tranche ledger. They share only the mark. So the reconciliation test is what says the two ledgers agree about what is held, and it fails the day they drift.

WHAT IS ABSENT STAYS ABSENT. No cached candle means no mark, and no mark means no market value, no unrealized and no stop distance -- never zero. A zero market value renders a held position as a total loss, which is the most alarming thing this page could say and it would be saying it about missing data. initial_stop NULL is "not on this row" (a DCA leg, a pre-v12 tranche), so its distance is absent too: measured against a substituted zero it would read as a position comfortably clear of a stop it does not have.

Signed where the sign means something, and nowhere else. unrealized and stop_distance carry verdicts; a market value does not, because an account is not good for being worth something and a glyph on every balance hides the one figure that matters. A negative stop distance means the tranche is trading THROUGH its protection -- keel is cash-spot and long-only, so a stop always sits below the mark and the sign has one meaning. stop_distance_pct crosses as a raw fraction with no %, the posture ratio already documents for the drawdown scalars.

THE FRESHNESS CHIP IS THE ENTRY GATE'S VERDICT, not a data age. missing/behind/ unconfirmed are entry_bar_ready's own words for why the agent would refuse to open here. freshness.assess tolerates the normal forming-bar lag; entry_bar_ready refuses a one-bar-late finer series because that lag is exactly what produces a duplicate real-money order. Showing the softer number would tell a reader the feed is fine while the engine's own gate is refusing it -- and this chip is the most common answer to "why has nothing happened", which is why it sits beside the money rather than under a disclosure.

NO CLOSE ACTION, EVER. #701's own refusal, pinned on the source rather than trusted: an exit goes through the typed-phrase friction of the terminal path, because a panic tap on a table row must not be the last line of defence. The test fails the build the day the affordance arrives looking like an obvious convenience.

A GUARD FOR A BUG THAT SHIPPED SILENTLY IN THE WRITING OF THIS. The view was first written calling sorting(sort, onSort) -- sorting is a TYPEDEF and a parameter name in render.js, never a function -- and every gate stayed green, because mypy does not read JavaScript, ruff does not either, and the view tests assert over source text rather than executing it. The page would have thrown ReferenceError on first render. test_every_call_resolves_to_something_the_module_has now scans both derivation-free modules for calls that are not in scope where they are made.

Scoped PER FUNCTION, which is the part that took two attempts: the first version pooled every function's parameters into one module-wide set, so sorting -- a parameter of table and headerCell -- counted as defined inside positionsView, and the scan passed when the original bug was reinstated. It only came out because the guard was itself mutation-tested against the bug it was written for.

Not in this commit, and split rather than faked: #701's attestation chip wants a quarterly expiry that neither asset_attestations nor instrument_attestations records (every other attestation table in the schema carries attest_due_ts; these two do not), and a "Purif: 0.38%" ratio whose denominator nothing holds -- owed_by_asset gives dollars, and what they are a percentage OF is a judgement.

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 12:34
… items 1-4)

`keel status` showed a tranche's id, product, rule, qty, entry price and bracket, and
`web/payload.py::_position_payload` explained in its own docstring why it showed no P&L:
"`OpenPositionStatus` carries neither, so emitting one would mean this layer multiplied
`qty` by `entry_price`... the fix, if it is wanted, is upstream." This is that fix,
upstream, plus the endpoint and view that read it.

THE MARK IS THE RAILS' MARK, AND THAT IS THE WHOLE POINT. `agent._mark_to_market_parts`
values a holding at `repo.get_candles(product, finest)[-1].close`; `gather_positions`
makes the same read through the same `_finest_granularity`. A positions page quoting a
different current price would be a second answer to "what is this worth", and since the
first answer moved rail 11's drawdown scalars, the page would be the wrong one.

That is asserted across the module boundary rather than claimed. The two readers do NOT
share a source for QUANTITY: the agent counts inventory from the filled-orders log --
never from `positions`, deliberately, after the phantom-drawdown bug that taught it -- and
this report reads the tranche ledger. They share only the mark. So the reconciliation test
is what says the two ledgers agree about what is held, and it fails the day they drift.

WHAT IS ABSENT STAYS ABSENT. No cached candle means no mark, and no mark means no market
value, no unrealized and no stop distance -- never zero. A zero market value renders a held
position as a total loss, which is the most alarming thing this page could say and it
would be saying it about missing data. `initial_stop` NULL is "not on this row" (a DCA leg,
a pre-v12 tranche), so its distance is absent too: measured against a substituted zero it
would read as a position comfortably clear of a stop it does not have.

Signed where the sign means something, and nowhere else. `unrealized` and `stop_distance`
carry verdicts; a market value does not, because an account is not good for being worth
something and a glyph on every balance hides the one figure that matters. A negative stop
distance means the tranche is trading THROUGH its protection -- keel is cash-spot and
long-only, so a stop always sits below the mark and the sign has one meaning.
`stop_distance_pct` crosses as a raw fraction with no `%`, the posture `ratio` already
documents for the drawdown scalars.

THE FRESHNESS CHIP IS THE ENTRY GATE'S VERDICT, not a data age. `missing`/`behind`/
`unconfirmed` are `entry_bar_ready`'s own words for why the agent would refuse to open
here. `freshness.assess` tolerates the normal forming-bar lag; `entry_bar_ready` refuses a
one-bar-late finer series because that lag is exactly what produces a duplicate real-money
order. Showing the softer number would tell a reader the feed is fine while the engine's
own gate is refusing it -- and this chip is the most common answer to "why has nothing
happened", which is why it sits beside the money rather than under a disclosure.

NO CLOSE ACTION, EVER. #701's own refusal, pinned on the source rather than trusted: an
exit goes through the typed-phrase friction of the terminal path, because a panic tap on a
table row must not be the last line of defence. The test fails the build the day the
affordance arrives looking like an obvious convenience.

A GUARD FOR A BUG THAT SHIPPED SILENTLY IN THE WRITING OF THIS. The view was first written
calling `sorting(sort, onSort)` -- `sorting` is a TYPEDEF and a parameter name in
`render.js`, never a function -- and every gate stayed green, because mypy does not read
JavaScript, ruff does not either, and the view tests assert over source text rather than
executing it. The page would have thrown `ReferenceError` on first render.
`test_every_call_resolves_to_something_the_module_has` now scans both derivation-free
modules for calls that are not in scope where they are made.

Scoped PER FUNCTION, which is the part that took two attempts: the first version pooled
every function's parameters into one module-wide set, so `sorting` -- a parameter of
`table` and `headerCell` -- counted as defined inside `positionsView`, and the scan passed
when the original bug was reinstated. It only came out because the guard was itself
mutation-tested against the bug it was written for.

Not in this commit, and split rather than faked: #701's attestation chip wants a quarterly
expiry that neither `asset_attestations` nor `instrument_attestations` records (every other
attestation table in the schema carries `attest_due_ts`; these two do not), and a "Purif:
0.38%" ratio whose denominator nothing holds -- `owed_by_asset` gives dollars, and what
they are a percentage OF is a judgement.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…t had no tests (#701)

Review findings on this branch, and the two bugs in the fix for the first one.

THE CHIP ASKED THE WRONG QUESTION. `_readiness_for` passed the COARSEST configured
granularity to `entry_bar_ready` for every row. But `agent._entry_gate_granularity` gates a
rule on the timeframe the RULE DECLARES, and falls back to the coarsest only for a rule
that declares none -- and `granularity` is a constructor parameter on `TurtleBreakout`,
`PullbackContinuation`, `CusumEvent` and `TripleBarrier`. So a tranche opened by a rule on
ONE_HOUR was judged on the daily series: with an hourly feed three bars late and the daily
series current, the page rendered a green "entry gate ready" over precisely the state the
chip exists to expose. The reverse also held -- a product traded only by hourly rules, with
no daily bars cached, read "keel would not open here" while the agent traded it fine.

Now resolved through the agent's own function, per row. Two degradations, both deliberate:
a `rule_name` nothing matches, and a rules row whose params no longer build, both fall back
to the coarsest rather than raising. A chip is not worth a 500, and the fallback is what the
agent itself uses for a rule that declares nothing.

AND TWO BUGS IN THAT FIX, found by review before it landed. The lookup was keyed on
`rules.kind` while `positions.rule_name` holds the rule's `name` -- a separate constructor
argument that defaults to the kind. Two `turtle_breakout` rows on different timeframes, a
configuration this codebase supports, collapsed to whichever row was read last, and one
tranche silently inherited the other's granularity; the test written alongside used two
different KINDS and so never covered it. Keyed on the name now, and a name that answers to
two different granularities maps to the fallback rather than picking one -- which row opened
a given tranche is genuinely unknowable from `rule_name` alone, and a chip that guessed
would state it with the same confidence as a resolved one.

The second: `_build_rule` ran once per TRANCHE rather than once per rule. It runs the
rule's real constructor with its validation, and a DCA book is one rule with many tranches,
so per-row building was the common case and not the edge one. Once per rule, pinned by a
count.

Both consequences of the per-row granularity are fixed too: the readiness cache was keyed
on product alone, which handed the second tranche of a product the first one's verdict, and
the view rendered one chip per product off `held[0]` -- the same error in the UI. The
verdict is a per-tranche column now.

THE ENDPOINT WAS EXECUTED BY NO TEST. `/api/positions` never reached `test_api.py`'s
`API_ROUTES`, the hand-written tuple every generic endpoint pin is parametrised over -- the
envelope, the no-JSON-number walk over the real bytes, the cache headers, the nosniff
header, the POST refusal. The payload builder was well covered, and that module's own
header says those are not the same statement. Added, with a test asserting the tuple equals
`web_api.API_ROUTES` so the next route cannot slip out the same way.

THE REFUSAL WAS PARTLY ASSERTING ABOUT ANOTHER VIEW. The view tests sliced a fixed `[:4000]`
from `positionsView`, which ran 1362 characters past its end into `ordersView`. #701's
central refusal -- no close action, ever -- therefore passed partly because `ordersView`'s
first 1362 characters happen to contain none of those words, and would have started failing
on an edit to code it does not describe. Sliced to the next `export function`.

THE CALL SCANNER HAD BLIND SPOTS, including one that hid functions silently: a default
parameter value closed the `([^)]*)` character class early, and the function stopped being
scanned at all rather than reporting anything. Module-level arrow functions were never
walked. Both fixed, with a floor assertion so a scan that walks nothing cannot pass, a
positive control per shape, and the remaining limits -- object and class methods, arrow
parameters admitted function-wide -- written down rather than implied.

Smaller, all confirmed: `mark_at` returned the bar's timestamp beside a `None` mark, putting
a valuation time on a valuation the row denies having; `_READINESS_STATES` sat one character
from the pre-existing `_READINESS_STATE` for `VenueReadiness`, an unrelated vocabulary, and
is now `_ENTRY_GATE_*`; `stop_distance_pct` rendered at `ratio`'s default two places, so a
tranche 0.2% through its stop and one 0.2% above it both showed as "0.00" in exactly the
range the column exists to make findable; the `or Decimal("0")` fallbacks on NOT NULL
columns are direct reads, so the day one becomes nullable is a loud failure rather than a
silent zero -- the substitution `_position_row_to_dict` refuses for `initial_stop`, for the
same reason; and `ordersView`'s JSDoc, which the new function had been inserted underneath,
is back above `ordersView`. That last one is the identical mistake made and fixed in #700.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@eaitbrahim
eaitbrahim merged commit 8380620 into main Sep 4, 2026
4 checks passed
@eaitbrahim
eaitbrahim deleted the feat-701-positions-view branch September 4, 2026 17:50
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