Skip to content

Robinhood best_bid_ask is not a coherent book snapshot — the legs cross on the most liquid pairs #413

Description

@eaitbrahim

Observed live on 2026-08-19 against a real read-only credential, while confirming the venue read path for #412. Latent, not active — nothing in adapter.py calls get_best_bid_ask today (transport.py:405 records that it and get_trading_pairs are the only two transport methods the adapter never invokes). Filing it so it is not rediscovered by whoever first reaches for this endpoint.

What the venue returns

GET /api/v2/crypto/trading/market_data/best_bid_ask/ reports bid above ask on the tightest pairs, persistently — three samples ~2s apart:

symbol ask − bid, sample 1 sample 2 sample 3
BTC-USD −5.90 (−0.86 bps) −1.90 (−0.28 bps) −9.42 (−1.38 bps)
ETH-USD +0.02 (+0.10 bps) −0.06 (−0.29 bps) −0.27 (−1.29 bps)
DOGE-USD −0.000001 (−0.11 bps) −0.000001 (−0.11 bps) −0.000004 (−0.59 bps)
XLM-USD +0.000053 (+3.21 bps) +0.000060 (+3.63 bps) +0.000084 (+5.09 bps)
ADA-USD +0.000043 (+2.41 bps) +0.000043 (+2.41 bps) +0.000045 (+2.49 bps)

One row, verbatim:

{"symbol": "BTC-USD", "timestamp": "2026-08-19T19:18:23.411286272Z",
 "bid": "68305.19", "ask": "68299.03"}

What it is not

Not a label swap. XLM-USD and ADA-USD never cross and carry sensible positive spreads; BTC/ETH/DOGE cross. A swapped pair of names would invert every symbol equally.

The pattern is that the two legs are sampled independently and then stamped with one timestamp. Where the true spread is wider than the sampling jitter (XLM, ADA at 2–5 bps) the ordering survives; where it is narrower (BTC, ETH, DOGE at well under 1 bp) the legs cross. The magnitude is consistent with that reading — the crossings are all ≲1.4 bps, never large.

So the row asserts a simultaneity it does not have. It is two near-simultaneous prices, not a book snapshot.

Why it matters later

translate.to_price_side maps BUY→ask and SELL→bid, and its docstring is explicit about the hazard:

Pricing a BUY preview off the bid, or a SELL preview off the ask, would show the wrong side of the spread — optimistic in exactly the direction that makes a synthesized preview look better than the fill it is meant to estimate.

On a crossed row that protection inverts: ask is the lower number and bid the higher, so a BUY priced off ask and a SELL priced off bid are both optimistic. Small in absolute terms, but it is a systematic bias in the one direction the mapping exists to prevent, on the live-money path.

Anything deriving a spread from this endpoint gets a negative number on precisely the assets keel trades most.

preview_order is unaffected: it uses estimated_price, which is self-consistent (side=ask → 68130.0 matching its own est_total_cost net of est_fee).

The fixture encodes the assumption the venue contradicts

tests/fixtures/rh_best_bid_ask.json has bid 65380.00 / ask 65480.00 — a tidy 15 bps spread that BTC-USD does not actually produce. Same class of problem as #230: a plausible-looking fixture standing in for an unobserved shape.

It also carries "next": null, "previous": null, which the live response for this endpoint does not include — the live body is {"results": [...]} only.

Scope

  • Correct rh_best_bid_ask.json to an observed row, crossing included, so any future consumer meets the real shape in tests.
  • Document on transport.get_best_bid_ask that the legs are not simultaneous and may cross, with the measured magnitude.
  • Decide the contract before anything consumes it: either normalise (lo = min(bid, ask), hi = max(...), price the conservative side per direction) or declare the endpoint unusable for pricing and keep estimated_price as the only quote source. Do not let a first consumer pick silently.
  • If a spread check is ever added, it must tolerate a non-positive spread rather than treat it as a venue error.

Metadata

Metadata

Assignees

No one assigned

    Labels

    phase-3Execution/rails/agentresearchMeasurement, validation or KB work (Research & validation)

    Type

    No type

    Projects

    No projects

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions