feat(robinhood): read-only portfolio source and account-bound live trading (#1428, #1442)

One Robinhood login reaches several accounts, and every account-scoped tool
takes account_number. Nothing sent one, and every consumer read the reply one
level too shallow (data.positions where Robinhood nests data.data.positions),
so the portfolio could not use Robinhood and the live runner and pre-trade gate
failed closed on every read.

Shapes: one module, connectors/robinhood/mcp.py, reads every Robinhood reply,
built only from the shapes posted on #1428 (observed get_accounts and
get_portfolio, advertised get_equity_positions outputSchema). A non-empty
data.next, a null list or item, or an unmapped data key is an error, never a
shorter account.

Portfolio (#1428): robinhood-live-mcp-readonly shares the MCP server and
OAuth grant. A connection instance carries account_ref, chosen from
get_accounts through the API, the CLI (connector select-account, setup
--account) or the connection center, with nothing preselected. A source with
no account errors before any call. Positions stay unpriced, because the profile
declares no quotes.read. A non-zero or unreported options, crypto, futures,
event-contract, mutual-fund or fixed-income value fails the source.

Live (#1442): /mandate/commit requires an account for such a broker and checks
it against get_accounts (listed, not deactivated, agentic_allowed). The Web
confirm dialog and the CLI REPL ask the user to pick from GET /live/accounts,
and never take the account from the agent-written proposal. Runner reads and
submits, the ceiling fetch and the gate's reads resolve the mandate's account
on each call and unwrap through the same module. The gate refuses an unbound
mandate or an order naming another account, and prices held positions through
the order-notional quote path, failing closed per row.

Existing gate and runner tests encoded replies Robinhood never sends. They now
use tests/robinhood_mcp_helpers.py.
This commit is contained in:
Haozhe Wu committed 2026-09-17 21:33:20 +08:00
1 parent 0de55cdd20
commit 1b888f8c3f
46 files changed
+2856 -114

No files matched your search

+31
View File
@@ -5,6 +5,21 @@ This project adheres to [Semantic Versioning](https://semver.org/).
## [Unreleased]
### Added
- **A Robinhood account can be a read-only portfolio source** (#1428). The new
`robinhood-live-mcp-readonly` profile uses the same MCP server and OAuth
grant as the trading profile. Its connection reads exactly one account,
picked from Robinhood's own `get_accounts` list in the connection center or
with `vibe-trading connector select-account <id>`. Nothing is preselected,
and a source with no account errors before any broker call. Holdings come
from `get_portfolio` and `get_equity_positions` and are mapped only from the
shapes the reporter posted. A second page of positions, a null list or item,
or an unknown field is an error, not a shorter account. Positions show
quantity and cost but no price until the quote reply is mapped. An account
that also holds options, crypto, futures, event contracts, mutual funds or
fixed income fails the source instead of showing an equity-only view.
### Changed
- **The final-answer grounding gate no longer guesses what a number is from the
@@ -61,6 +76,22 @@ This project adheres to [Semantic Versioning](https://semver.org/).
### Fixed
- **Robinhood live trading reads and trades the account the mandate names**
(#1442). The runner, the pre-trade gate and the commit-time ceiling fetch
called Robinhood with no `account_number`, and read replies one level too
shallow (`data.positions` where Robinhood nests `data.data.positions`). So
every reconciliation aborted, and the gate refused every order because it
could not read positions. The tests stayed green only because their fake
replies had a shape Robinhood never sends. A Robinhood mandate is now bound
at commit to one account from `get_accounts`: listed, not deactivated, and
open to agentic trading. The Web confirm dialog and the CLI both ask for
it, and never take it from the agent-written proposal. Every runner and gate
call sends that account, and an order naming another account is refused.
Held positions, which Robinhood reports without a price, are priced the way
a quantity order already was, and one that cannot be priced fails the
exposure check closed. Existing Robinhood mandates must be committed again
with an account.
- **Plain integers in prose are not price claims.** A list number ("输出原则
4"), a window length or a count was checked like a quoted price and could be
cut from a released answer.