package crypto import ( "context" "encoding/json" decimal "github.com/alpacahq/alpacadecimal" "s1d3sw1ped/robinhood-agentic-mcp/client" "s1d3sw1ped/robinhood-agentic-mcp/internal/wire" ) const ( toolPairs = "get_currency_pairs" toolQuotes = "get_crypto_quotes" toolPositions = "get_crypto_positions" toolOrders = "get_crypto_orders" toolPreview = "preview_crypto_order" toolPlace = "place_crypto_order" toolCancel = "cancel_crypto_order" ) // PairsRequest is the argument set for get_currency_pairs. type PairsRequest struct { Cursor string Limit int } // CurrencyPair is one row from get_currency_pairs. type CurrencyPair struct { ID string Symbol string } // PairsResult is the parsed get_currency_pairs payload. type PairsResult struct { Pairs []CurrencyPair NextCursor string } // QuotesRequest is the argument set for get_crypto_quotes. type QuotesRequest struct { Symbols []string Timezone string RHSAccountNumber string } // CryptoQuote is one pair quote from get_crypto_quotes. type CryptoQuote struct { Symbol string Bid decimal.Decimal Ask decimal.Decimal Mark decimal.Decimal PrevClose decimal.Decimal } // QuotesResult is the parsed get_crypto_quotes payload. type QuotesResult struct { Quotes []CryptoQuote } // PositionsRequest is the argument set for get_crypto_positions. type PositionsRequest struct { RHSAccountNumber string Cursor string } // CryptoPosition is one holding from get_crypto_positions. type CryptoPosition struct { Symbol string Quantity decimal.Decimal CostBasis decimal.Decimal } // PositionsResult is the parsed get_crypto_positions payload. type PositionsResult struct { Positions []CryptoPosition NextCursor string } // OrdersRequest is the argument set for get_crypto_orders. type OrdersRequest struct { RHSAccountNumber string OrderID string State string StateGroup string Side client.Side Symbol string CreatedAtGTE string UpdatedAtGTE string Cursor string } // OrdersResult is the parsed get_crypto_orders payload. type OrdersResult struct { Orders []Order NextCursor string } // PlaceOrderRequest is the argument set for preview_crypto_order and place_crypto_order. type PlaceOrderRequest struct { RHSAccountNumber string Symbol string Side client.Side Type client.OrderType Qty *decimal.Decimal DollarAmount *decimal.Decimal LimitPrice *decimal.Decimal StopPrice *decimal.Decimal TimeInForce client.TimeInForce // empty → omit (Robinhood defaults gtc/gfd by type) RefID string } // PreviewResult is the pre-trade check from preview_crypto_order. type PreviewResult struct { Errors []string `json:"errors"` Warnings []string `json:"warnings"` Quantity decimal.Decimal `json:"-"` Price decimal.Decimal `json:"-"` } // Order is a placed or listed crypto order. type Order struct { ID string `json:"id"` Symbol string `json:"symbol"` Side string `json:"side"` State string `json:"state"` Qty decimal.Decimal `json:"-"` } // CancelOrderRequest is the argument set for cancel_crypto_order. type CancelOrderRequest struct { RHSAccountNumber string OrderID string } // Pairs calls get_currency_pairs. func (c *Client) Pairs(ctx context.Context, req PairsRequest) (PairsResult, error) { args := map[string]any{} if req.Cursor != "" { args["cursor"] = req.Cursor } if req.Limit != 0 { args["limit"] = req.Limit } var out PairsResult if err := c.parse(ctx, toolPairs, args, &out); err != nil { return PairsResult{}, err } return out, nil } // Quotes calls get_crypto_quotes. func (c *Client) Quotes(ctx context.Context, req QuotesRequest) (QuotesResult, error) { args := map[string]any{} if len(req.Symbols) > 0 { args["symbols"] = req.Symbols } if req.Timezone != "" { args["timezone"] = req.Timezone } if req.RHSAccountNumber != "" { args["rhs_account_number"] = req.RHSAccountNumber } var out QuotesResult if err := c.parse(ctx, toolQuotes, args, &out); err != nil { return QuotesResult{}, err } return out, nil } // Positions calls get_crypto_positions. func (c *Client) Positions(ctx context.Context, req PositionsRequest) (PositionsResult, error) { args := map[string]any{} if req.RHSAccountNumber != "" { args["rhs_account_number"] = req.RHSAccountNumber } if req.Cursor != "" { args["cursor"] = req.Cursor } var out PositionsResult if err := c.parse(ctx, toolPositions, args, &out); err != nil { return PositionsResult{}, err } return out, nil } // Orders calls get_crypto_orders. func (c *Client) Orders(ctx context.Context, req OrdersRequest) (OrdersResult, error) { args := map[string]any{} if req.RHSAccountNumber != "" { args["rhs_account_number"] = req.RHSAccountNumber } if req.OrderID != "" { args["order_id"] = req.OrderID } if req.State != "" { args["state"] = req.State } if req.StateGroup != "" { args["state_group"] = req.StateGroup } if req.Side != "" { args["side"] = string(req.Side) } if req.Symbol != "" { args["symbol"] = req.Symbol } if req.CreatedAtGTE != "" { args["created_at_gte"] = req.CreatedAtGTE } if req.UpdatedAtGTE != "" { args["updated_at_gte"] = req.UpdatedAtGTE } if req.Cursor != "" { args["cursor"] = req.Cursor } var out OrdersResult if err := c.parse(ctx, toolOrders, args, &out); err != nil { return OrdersResult{}, err } return out, nil } // PreviewOrder calls preview_crypto_order. RefID is not sent. func (c *Client) PreviewOrder(ctx context.Context, req PlaceOrderRequest) (PreviewResult, error) { var out PreviewResult if err := c.parse(ctx, toolPreview, placeArgs(req, false), &out); err != nil { return PreviewResult{}, err } return out, nil } // PlaceOrder calls place_crypto_order. RefID is sent as ref_id. func (c *Client) PlaceOrder(ctx context.Context, req PlaceOrderRequest) (Order, error) { raw, err := c.c.Call(ctx, toolPlace, placeArgs(req, true)) if err != nil { return Order{}, err } ords, err := parseOrders(raw) if err != nil { return Order{}, client.ToolErrorf(toolPlace, "parse: %w", err) } if len(ords) > 0 { return ords[0], nil } return Order{}, nil } // CancelOrder calls cancel_crypto_order. func (c *Client) CancelOrder(ctx context.Context, req CancelOrderRequest) error { args := map[string]any{} if req.RHSAccountNumber != "" { args["rhs_account_number"] = req.RHSAccountNumber } if req.OrderID != "" { args["order_id"] = req.OrderID } _, err := c.c.Call(ctx, toolCancel, args) return err } func placeArgs(req PlaceOrderRequest, withRef bool) map[string]any { args := map[string]any{} if req.RHSAccountNumber != "" { args["rhs_account_number"] = req.RHSAccountNumber } if req.Symbol != "" { args["symbol"] = req.Symbol } if req.Side != "" { args["side"] = string(req.Side) } if req.Type != "" { args["type"] = string(req.Type) } if req.Qty != nil { args["quantity"] = wire.Encode(*req.Qty) } if req.DollarAmount != nil { args["dollar_amount"] = wire.Encode(*req.DollarAmount) } if req.LimitPrice != nil { args["limit_price"] = wire.Encode(*req.LimitPrice) } if req.StopPrice != nil { args["stop_price"] = wire.Encode(*req.StopPrice) } if req.TimeInForce != "" { args["time_in_force"] = string(req.TimeInForce) } if withRef && req.RefID != "" { args["ref_id"] = req.RefID } return args } 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 }