Files
robinhood-agentic-mcp/accounts/accounts.go
T
s1d3sw1ped a6bf8632ce fix: parse MCP result payloads and drop place idempotency_key
Typed result structs replace empty envelopes. Equity place sends
ref_id only so live additionalProperties:false schemas accept the call.
2026-09-01 14:18:39 -05:00

316 lines
8.5 KiB
Go

package accounts
import (
"context"
"encoding/json"
decimal "github.com/alpacahq/alpacadecimal"
"s1d3sw1ped/robinhood-agentic-mcp/client"
"s1d3sw1ped/robinhood-agentic-mcp/internal/wire"
)
const (
toolAccounts = "get_accounts"
toolPortfolio = "get_portfolio"
toolRealizedPnL = "get_realized_pnl"
toolPnLTradeHistory = "get_pnl_trade_history"
toolLimitedMarginUpgrade = "get_limited_margin_upgrade_info"
toolOptionLevelUpgrade = "get_option_level_upgrade_info"
toolCryptoOnboarding = "get_crypto_account_onboarding_info"
toolSearch = "search"
)
// AccountsRequest is the argument set for get_accounts (none).
type AccountsRequest struct{}
// Account is one brokerage account from get_accounts.
type Account struct {
AccountNumber string
ID string
RHSAccountNumber string
Type string
AgenticAllowed bool
Cash bool
BuyingPower *decimal.Decimal
}
// AccountsResult is the parsed get_accounts payload.
type AccountsResult struct {
Accounts []Account
}
// PortfolioRequest is the argument set for get_portfolio.
type PortfolioRequest struct {
AccountNumber string
}
// PortfolioResult is the parsed get_portfolio payload.
type PortfolioResult struct {
BuyingPower decimal.Decimal
}
// RealizedPnLRequest is the argument set for get_realized_pnl.
type RealizedPnLRequest struct {
AccountNumber string
Span string
StartDate string
EndDate string
AssetClasses []string
DisplayCurrency string
Timezone string
}
// PnLBucket is one span bucket from get_realized_pnl.
type PnLBucket struct {
Label string
Amount decimal.Decimal
Percent decimal.Decimal
Trades int
}
// RealizedPnLResult is the parsed get_realized_pnl payload.
type RealizedPnLResult struct {
Total decimal.Decimal
Percent decimal.Decimal
Buckets []PnLBucket
}
// PnLTradeHistoryRequest is the argument set for get_pnl_trade_history.
type PnLTradeHistoryRequest struct {
AccountNumber string
Span string
Symbol string
Cursor string
}
// PnLTrade is one realizing trade from get_pnl_trade_history.
type PnLTrade struct {
Symbol string
Side string
Quantity decimal.Decimal
Price decimal.Decimal
PnL decimal.Decimal
}
// PnLTradeHistoryResult is the parsed get_pnl_trade_history payload.
type PnLTradeHistoryResult struct {
Trades []PnLTrade
NextCursor string
}
// AccountNumberRequest is a single account_number argument.
type AccountNumberRequest struct {
AccountNumber string
}
// UpgradeInfoResult is the parsed upgrade-info payload.
type UpgradeInfoResult struct {
URL string
WebURL string
MobileURL string
}
// OnboardingInfoResult is the parsed crypto onboarding payload.
type OnboardingInfoResult struct {
URL string
WebURL string
}
// SearchRequest is the argument set for search.
type SearchRequest struct {
Query string
AssetType string
Limit int
}
// SearchHit is one instrument/pair/index from search.
type SearchHit struct {
Symbol string
Name string
ID string
InstrumentID string
}
// SearchResult is the parsed search payload.
type SearchResult struct {
Results []SearchHit
}
// Accounts calls get_accounts and returns every account (no IRA/margin filter).
func (c *Client) Accounts(ctx context.Context, req AccountsRequest) (AccountsResult, error) {
raw, err := c.c.Call(ctx, toolAccounts, map[string]any{})
if err != nil {
return AccountsResult{}, err
}
var wrap struct {
Accounts []struct {
AccountNumber string `json:"account_number"`
ID string `json:"id"`
RHSAccountNumber string `json:"rhs_account_number"`
Type string `json:"type"`
AgenticAllowed bool `json:"agentic_allowed"`
Cash bool `json:"cash"`
BuyingPower any `json:"buying_power"`
} `json:"accounts"`
}
if err := json.Unmarshal(wire.Unwrap(raw), &wrap); err != nil {
return AccountsResult{}, client.ToolErrorf(toolAccounts, "parse: %w", err)
}
out := make([]Account, 0, len(wrap.Accounts))
for _, a := range wrap.Accounts {
bp, err := wire.DecOpt(a.BuyingPower)
if err != nil {
return AccountsResult{}, client.ToolErrorf(toolAccounts, "parse: %w", err)
}
out = append(out, Account{
AccountNumber: a.AccountNumber,
ID: a.ID,
RHSAccountNumber: a.RHSAccountNumber,
Type: a.Type,
AgenticAllowed: a.AgenticAllowed,
Cash: a.Cash,
BuyingPower: bp,
})
}
return AccountsResult{Accounts: out}, nil
}
// Portfolio calls get_portfolio.
func (c *Client) Portfolio(ctx context.Context, req PortfolioRequest) (PortfolioResult, error) {
args := map[string]any{}
if req.AccountNumber != "" {
args["account_number"] = req.AccountNumber
}
raw, err := c.c.Call(ctx, toolPortfolio, args)
if err != nil {
return PortfolioResult{}, err
}
var loose struct {
BuyingPower struct {
BuyingPower any `json:"buying_power"`
} `json:"buying_power"`
}
if err := json.Unmarshal(wire.Unwrap(raw), &loose); err != nil {
return PortfolioResult{}, client.ToolErrorf(toolPortfolio, "parse: %w", err)
}
bp, err := wire.Dec(loose.BuyingPower.BuyingPower)
if err != nil {
return PortfolioResult{}, client.ToolErrorf(toolPortfolio, "parse: %w", err)
}
return PortfolioResult{BuyingPower: bp}, nil
}
// RealizedPnL calls get_realized_pnl.
func (c *Client) RealizedPnL(ctx context.Context, req RealizedPnLRequest) (RealizedPnLResult, error) {
args := map[string]any{}
if req.AccountNumber != "" {
args["account_number"] = req.AccountNumber
}
if req.Span != "" {
args["span"] = req.Span
}
if req.StartDate != "" {
args["start_date"] = req.StartDate
}
if req.EndDate != "" {
args["end_date"] = req.EndDate
}
if len(req.AssetClasses) > 0 {
args["asset_classes"] = req.AssetClasses
}
if req.DisplayCurrency != "" {
args["display_currency"] = req.DisplayCurrency
}
if req.Timezone != "" {
args["timezone"] = req.Timezone
}
var out RealizedPnLResult
if err := c.parse(ctx, toolRealizedPnL, args, &out); err != nil {
return RealizedPnLResult{}, err
}
return out, nil
}
// PnLTradeHistory calls get_pnl_trade_history.
func (c *Client) PnLTradeHistory(ctx context.Context, req PnLTradeHistoryRequest) (PnLTradeHistoryResult, error) {
args := map[string]any{}
if req.AccountNumber != "" {
args["account_number"] = req.AccountNumber
}
if req.Span != "" {
args["span"] = req.Span
}
if req.Symbol != "" {
args["symbol"] = req.Symbol
}
if req.Cursor != "" {
args["cursor"] = req.Cursor
}
var out PnLTradeHistoryResult
if err := c.parse(ctx, toolPnLTradeHistory, args, &out); err != nil {
return PnLTradeHistoryResult{}, err
}
return out, nil
}
// LimitedMarginUpgradeInfo calls get_limited_margin_upgrade_info.
func (c *Client) LimitedMarginUpgradeInfo(ctx context.Context, req AccountNumberRequest) (UpgradeInfoResult, error) {
return c.upgradeInfo(ctx, toolLimitedMarginUpgrade, req)
}
// OptionLevelUpgradeInfo calls get_option_level_upgrade_info.
func (c *Client) OptionLevelUpgradeInfo(ctx context.Context, req AccountNumberRequest) (UpgradeInfoResult, error) {
return c.upgradeInfo(ctx, toolOptionLevelUpgrade, req)
}
func (c *Client) upgradeInfo(ctx context.Context, tool string, req AccountNumberRequest) (UpgradeInfoResult, error) {
args := map[string]any{}
if req.AccountNumber != "" {
args["account_number"] = req.AccountNumber
}
var out UpgradeInfoResult
if err := c.parse(ctx, tool, args, &out); err != nil {
return UpgradeInfoResult{}, err
}
return out, nil
}
// CryptoOnboardingInfo calls get_crypto_account_onboarding_info.
func (c *Client) CryptoOnboardingInfo(ctx context.Context, req struct{}) (OnboardingInfoResult, error) {
var out OnboardingInfoResult
if err := c.parse(ctx, toolCryptoOnboarding, map[string]any{}, &out); err != nil {
return OnboardingInfoResult{}, err
}
return out, nil
}
// Search calls search.
func (c *Client) Search(ctx context.Context, req SearchRequest) (SearchResult, error) {
args := map[string]any{}
if req.Query != "" {
args["query"] = req.Query
}
if req.AssetType != "" {
args["asset_type"] = req.AssetType
}
if req.Limit != 0 {
args["limit"] = req.Limit
}
var out SearchResult
if err := c.parse(ctx, toolSearch, args, &out); err != nil {
return SearchResult{}, err
}
return out, nil
}
func (c *Client) parse(ctx context.Context, tool string, args map[string]any, dest any) error {
raw, err := c.c.Call(ctx, tool, args)
if err != nil {
return err
}
if err := json.Unmarshal(wire.Unwrap(raw), dest); err != nil {
return client.ToolErrorf(tool, "parse: %w", err)
}
return nil
}