feat: add accounts MCP methods
This commit is contained in:
@@ -0,0 +1,274 @@
|
||||
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
|
||||
}
|
||||
|
||||
// RealizedPnLResult is the parsed get_realized_pnl payload.
|
||||
type RealizedPnLResult struct{}
|
||||
|
||||
// PnLTradeHistoryRequest is the argument set for get_pnl_trade_history.
|
||||
type PnLTradeHistoryRequest struct {
|
||||
AccountNumber string
|
||||
Span string
|
||||
Symbol string
|
||||
Cursor string
|
||||
}
|
||||
|
||||
// PnLTradeHistoryResult is the parsed get_pnl_trade_history payload.
|
||||
type PnLTradeHistoryResult struct{}
|
||||
|
||||
// AccountNumberRequest is a single account_number argument.
|
||||
type AccountNumberRequest struct {
|
||||
AccountNumber string
|
||||
}
|
||||
|
||||
// UpgradeInfoResult is the parsed upgrade-info payload.
|
||||
type UpgradeInfoResult struct{}
|
||||
|
||||
// OnboardingInfoResult is the parsed crypto onboarding payload.
|
||||
type OnboardingInfoResult struct{}
|
||||
|
||||
// SearchRequest is the argument set for search.
|
||||
type SearchRequest struct {
|
||||
Query string
|
||||
AssetType string
|
||||
Limit int
|
||||
}
|
||||
|
||||
// SearchResult is the parsed search payload.
|
||||
type SearchResult struct{}
|
||||
|
||||
// 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
|
||||
}
|
||||
Reference in New Issue
Block a user