Skip to content

Robinhood: no pre-flight sizing check, so a sub-minimum or off-increment order finds out at the venue #410

Description

@eaitbrahim

Split out of #198, now closed as superseded.

The gap

trading_pairs carries the per-pair rules — min_order_amount, asset_increment, quote_increment, max_order_size — and nothing reads them before submission. RobinhoodTransport.get_trading_pairs is one of only two transport methods the adapter never invokes, and says so at transport.py:405:

They exist because they are the inputs the obvious next feature needs: asset_increment, quote_increment, and max_order_size are what would let this package round a size to the venue's tick LOCALLY instead of discovering the violation as a rejection.

Three constraints the implementation must respect

1. The rows are not uniform. From transport.py:416:

Every row carries symbol, asset_code, quote_code, asset_increment, quote_increment, max_order_size, status and is_api_tradable. min_order_amount is carried by 63 of the 89 pairs — BTC-USD (0.1) and ETH-USD among them — and absent from the other 26.

So min_order_amount is optional per pair. Code that assumes the endpoint is uniform is the exact bug #230 fixed: #217 F3 inspected results[0] only, results[0] is BILL-USD (one of the 26 without it), and #218 then removed the field from the fixture on that false negative. Any probe or check here reads the row for the pair being traded, never results[0].

min_order_size does not exist at all — do not look for it.

2. It must not raise on the exit path. Carried over from #198's original wording, and it is the constraint that makes this non-trivial. A pre-flight check that refuses an undersized order is correct for an entry and dangerous for an exit: refusing to sell a holding because it rounds below the venue minimum strands a position keel has decided to close. The exit path needs a different disposition — round, or proceed and let the venue answer — not a hard refusal.

3. It costs a request. get_trading_pairs against a 100 req/min limit with no backoff (see the backoff issue). Cache per symbol for the process lifetime, the way RobinhoodTransport._account() caches the account number.

Scope

  • Read the pair row for the symbol being ordered; validate size against min_order_amount (when present), asset_increment, and max_order_size.
  • Entry: refuse with a clear reason. Exit: never refuse — round to increment and proceed.
  • Cache the pair rules; do not re-fetch per order.
  • Tests against a canned transport covering: pair with min_order_amount, pair without it, off-increment size, over max_order_size, and the exit path for each.

Metadata

Metadata

Assignees

No one assigned

    Labels

    phase-3Execution/rails/agentprerequisiteBlocks other issues; do first

    Type

    No type

    Projects

    No projects

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions