Skip to content

Robinhood adapter: close the independent review findings #196

Description

@eaitbrahim

Follow-up to #192, which merged before the independent review's fixes had landed. All findings below are present on main as of 9422c25.

None of this is reachable today. keel/execution/executor.py:486 still calls broker.place_order(product_id, side, order_configuration) — a different signature from the port's place_order(spec) — and keel/commands/_common.py still constructs CoinbaseClient. Installing the package registers an entry point and nothing else. These become live-money defects the moment the port migration wires it up.

Blockers

  • B1 — str(Decimal) emits scientific notation. translate.py:127,136-137,148-150. Verified: str(Decimal('0.00000001')) == '1E-8', and BTC's asset_increment is exactly 0.00000001 per tests/fixtures/rh_trading_pairs.json. A dust-sized exit puts "asset_quantity": "1E-8" on the wire. Use format(d, 'f').
    The guarding test (tests/broker_robinhood/test_translate.py:108-120, test_decimals_are_rendered_as_exact_strings_not_floats) asserts the property using two values that cannot trigger the exponent form.
  • B2 — place_order reports success for a rejected order. adapter.py:~310-335 checks only that an id came back and never reads state. A StopLimitGTC answered 200 {"id": ..., "state": "failed"} records a protective stop that does not exist at the venue.
  • B3 — estimated_price namespace inconsistent. transport.py:355 uses /api/v2/crypto/trading/estimated_price/ while transport.py:347 uses /api/v2/crypto/marketdata/best_bid_ask/. Settle from the docs and pin the URL in a comment.
  • B4 — get_order/cancel_order omit account_number. transport.py:376,387, where create_order and holdings both send it. A resulting 404 launders through _requestNone_terminal_unknown → status FAILED with zeroed money for a live resting order, corrupting reconciliation.

Should-fix

  • S1 Silent-zero previews: _estimated_price returns Decimal("0") on failure and preview_order never populates Preview.errors, so "costs nothing" and "could not price it" are indistinguishable at the confirm gate.
  • S2 preview_order approves symbols place_order refuses — the LimitGTC | StopLimitGTC branch never calls to_symbol, so ETH-USDC previews cleanly then raises UnsupportedOrder, which the port forbids catching and retrying.
  • S3 response.json() default float parsing launders unquoted JSON numbers through float before Decimal sees them. All eight fixtures quote every money value, so this is untested. Use parse_float=Decimal.
  • S4 RobinhoodTransport has no tests for the code that talks to the venue — _request, _paginate, _account(), and every literal endpoint path are uncovered. This is what let B3 ship.
  • S5 Query string hand-built without percent-encoding (transport.py:264-269); the same raw string is signed and sent, so an encoding divergence breaks signature verification.
  • S6 cancel_order can raise on the exit path (adapter.py:430), against the package's own stated principle that "a raise on the exit path can trap a position".

What the review found clean

The cancel contract is correct — no path returns True without a per-id venue confirmation; the re-poll is real and test-proven; an already-filled order returns False. market_ioc_quote is genuinely unreachable, double-gated, with no divide-by-estimated-price anywhere. Every Preview is constructed synthetic=True. The canned transport is genuinely offline. Packaging is dev-group only, so keel-trader gains no runtime dependency on pynacl.

Metadata

Metadata

Assignees

No one assigned

    Labels

    fixBug fix (groups under Fixes)phase-3Execution/rails/agent

    Type

    No type

    Projects

    No projects

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions