a6bf8632ce
Typed result structs replace empty envelopes. Equity place sends ref_id only so live additionalProperties:false schemas accept the call.
316 lines
8.5 KiB
Go
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
|
|
}
|