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 }