feat(tooling): read-only live probe for the robinhood adapter's fixture shapes - #216
Merged
Merged
Conversation
…re shapes Every test under `tests/broker_robinhood/` runs against a canned transport, so the suite proves the adapter is internally consistent with fixtures we wrote ourselves. It cannot prove that Robinhood accepts our Ed25519 signature, that the endpoint paths are real, or that the response shapes are real -- and `rh_accounts.json`'s nested `fee_tier_status` drives every number in `get_fee_summary` and every `Preview.est_fee`. `scripts/robinhood_smoke.py` closes that gap with read-only GETs: no order, no money at risk. It reports structural diffs against each committed fixture in both directions, since a key the venue sends and the fixture omits is how a capability gets missed. Read-only by construction: the guard is installed ONTO the transport's `_request` rather than wrapping it. A `__getattr__` wrapper looks correct and is not -- the transport's methods call their own `self._request` and sail straight past it, so the guarantee would hold for the tests and not for the probes. A test pins that. Prints shapes, not values, so balances never reach a terminal or a pasted log. Refs #215, #197, #198. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
eaitbrahim
added a commit
that referenced
this pull request
Aug 9, 2026
…ery fixture (#218) * fix(brokers): read robinhood's real estimated_price shape, correct every fixture First live run against a real Robinhood credential (#216) proved the response shapes wrong in four places and the probe script wrong in a fifth (#217). F1 (blocker). `_estimated_price` read `price`, which this venue does not send; the unit price is in the column named after the side asked for (`ask`/`bid`). Every market preview against the live venue came back `est_quote_size = 0.000` with `errors` populated. There is deliberately no fallback to the other side's column -- pricing a sell off an ask overstates the proceeds of an exit -- so a row missing the requested side stays unpriced. `preview_order` now also reads the venue's own `est_fee` instead of deriving one from the account tier, and reconciles `est_total_cost` against `price x quantity` and `est_fee` per response rather than assuming either reading. All three self-consistent readings recover the same fee-exclusive notional, which is what `Preview.est_quote_size` is defined to carry; a total matching none of them is priced as sent AND reported through `Preview.errors`. `synthetic=True` and `supports_native_preview=False` are unchanged and now argued for explicitly: `/estimated_price/` prices a quantity, it does not validate an order. F2. Every money value arrives unquoted, so every fixture is now an unquoted JSON number and both fixture loaders decode with `parse_float=Decimal`, the way the transport does. This makes #194's parser change exercised rather than merely present. F3. `trading_pairs` publishes no `min_order_amount` and no `min_order_size`. Removed from the fixture; the transport docstring and README now record that a pre-flight minimum-size check (#198) has no source on this endpoint. F4. `best_bid_ask` rewritten to the observed `symbol`/`bid`/`ask`; the five invented keys are gone. F5. The probe compared a post-pagination aggregate against a raw single page, so it reported `next`/`previous` MISSING AT VENUE on all five probes and buried the real findings. `fixture_shape` normalizes the fixture to what a probe can observe -- envelope stripped, numbers parsed as `Decimal` -- and a test runs the real comparison for every probe. The three order fixtures remain unverified against the venue: observing an order object requires placing a real one. Closes #217 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * fix(brokers): match robinhood's mixed quoting, ask-only total, and quote timestamp Second live probe run against the branch (#217 F6-F8). F6. The venue is NOT uniformly unquoted -- it mixes, and `accounts` mixes within one object: `buying_power` is a quoted string beside an unquoted `fee_tier_status.fee_ratio`. `trading_pairs` and `best_bid_ask` quote all their money; `estimated_price` and `holdings` quote none. Reverted the six over-converted fields to strings so every fixture matches the venue field for field, and recorded the mixed quoting where it can be acted on: the transport's parser comment, `_decimal_or_none`, the README, and an executable test. The `parse_float=Decimal` + `Decimal(str(x))` pairing is what makes both forms land on the same exact number and neither half is optional. F7. `est_total_cost` is sent on the ask side only; a bid row carries `bid`, `quantity`, `fee_ratio` and `est_fee` and no total. The code already priced this correctly by falling through the reconciliation -- now it is intentional: a new `rh_estimated_price_bid.json` (a verbatim live bid row) and a test pinning that a sell prices from `bid x quantity` with the venue's `est_fee`, empty `errors`, and no `/accounts/` round trip. An error on every sell preview is an error nobody reads, so this must stay a clean answer rather than a degraded one. F8. `best_bid_ask` rows carry `timestamp`; added. Also: `rh_estimated_price.json` is now the verbatim live ask row, which settles the reconciliation empirically -- `64975.78 * 0.001 + 0.61726991 == 65.59304991`, i.e. fee-INCLUSIVE, exactly the reading that would have double-counted the fee. The relation is still derived per response, since that is one symbol, one side, one moment, and the bid side omits the field entirely. Preview tests size their specs from the fixture's echoed quantity rather than a literal, so the two cannot drift apart. Since the three order fixtures remain unverified on a venue proven to mix quoting, `get_order` is now tested against both the quoted and unquoted form of the same order and required to produce identical `Decimal`s. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
eaitbrahim
added a commit
that referenced
this pull request
Aug 9, 2026
#219) `scripts/robinhood_smoke.py` (#216) has required `ROBINHOOD_API_KEY` and `ROBINHOOD_PRIVATE_KEY` since it landed, and #218 exercised it against the live venue twice, but neither PR added the names to `.env.example` -- so the only record of what to put in `.env` was the script's own failure message, which you see after you already went looking. Both are commented as probe-only on purpose. `load_secrets` does not read them, so a reader who fills them in and expects the adapter to come alive in a run has misread the state of this package; the README says the same thing at length and this is the one-line version at the point of use. The seed encoding is called out because a PEM is the likelier guess and fails with a length error rather than anything that names the cause. Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
eaitbrahim
added a commit
that referenced
this pull request
Aug 11, 2026
…the CTS scoring fix (#241) A minor bump, not a patch, for three reasons that each require operator action or change behaviour the deployment is currently relying on. SCHEMA. `SCHEMA_VERSION` goes 9 -> 10 (#223). Both deployed databases are at 9 and must be migrated before this build can use them. BEHAVIOUR REQUIRING OPERATOR ACTION. #223 adds a second attested claim -- what CONTRACT a venue listing is, not only what the underlying asset is. It fails closed with no backfill, deliberately, so after this lands `keel assets screen` REJECTS every product with `instrument_wrapper: UNATTESTED` until `keel assets attest-instrument` is run once per product. Live trading is unaffected: rail 1 gates buys on `config.allowlist`, not on the screen. LIVE SCORING CHANGED. #227 fixed `is_round_number`, which returned True for every 2dp-quoted price and so handed BTC/ETH/PAXG a free CTS point on every bar. Scores on those three assets are genuinely lower under this build than under 0.5.7. Also ships: the Robinhood crypto adapter behind the broker port (#216/#218/#222/#229, not wired to the live path), the TUI activity feed (#235/#237), the CTS factor collinearity study (#224), `Preview.synthetic` at the confirm gate (#221), rail 9 seeing a bracket's own stop (#212), and CI gating merges on the `test` check (#234/#238). Co-authored-by: Claude Opus 5 (1M context) <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.
Adds
scripts/robinhood_smoke.py— an operator tool that validates the Robinhood adapter's assumptions against the live venue without placing an order.Why
tests/broker_robinhood/runs entirely against a canned transport, so it proves the adapter is internally consistent with fixtures we wrote ourselves. It cannot prove:tests/fixtures/rh_accounts.json's nestedfee_tier_statusdrives every number inget_fee_summaryand everyPreview.est_fee, and nothing outside this repo corroborates it.What it does
Five read-only GETs —
accounts,trading_pairs,best_bid_ask,estimated_price,holdings— and a structural diff of each against its committed fixture, reported in both directions. A key the fixture invented is the dangerous one; a key the venue sends and the fixture omits is how a capability gets missed (fees_usd, #197, is exactly that shape).Two safety properties, both tested
Read-only by construction. The guard is installed onto the transport's
_request, not wrapped around it. A__getattr__wrapper looks correct and is not — the transport's own methods callself._requestand sail straight past it, so the guarantee would hold for the tests and not for the probes. That bug was written, caught bytest_running_every_probe_issues_only_gets, and fixed; the test now pins it.Shapes, not values. Every leaf is replaced by its type, so balances and holdings never reach a terminal, a CI log, or a pasted bug report.
It also pre-checks the private key's length, because pasting the base64 public key Robinhood's credential page asked for produces a 401 indistinguishable from a signing bug.
Not covered
The order lifecycle —
statevalues,fee_charged, whether a cancel 200 confirms. No sandbox exists, so that needs a real order; tracked under #198.Gates
Refs #215. Not shipped in the wheel — operator tooling, like
migration_smoke.py.