258 lines
6.6 KiB
Go
258 lines
6.6 KiB
Go
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
|
|
}
|
|
|
|
// PairsResult is the parsed get_currency_pairs payload.
|
|
type PairsResult struct{}
|
|
|
|
// QuotesRequest is the argument set for get_crypto_quotes.
|
|
type QuotesRequest struct {
|
|
Symbols []string
|
|
Timezone string
|
|
RHSAccountNumber string
|
|
}
|
|
|
|
// QuotesResult is the parsed get_crypto_quotes payload.
|
|
type QuotesResult struct{}
|
|
|
|
// PositionsRequest is the argument set for get_crypto_positions.
|
|
type PositionsRequest struct {
|
|
RHSAccountNumber string
|
|
Cursor string
|
|
}
|
|
|
|
// PositionsResult is the parsed get_crypto_positions payload.
|
|
type PositionsResult struct{}
|
|
|
|
// 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{}
|
|
|
|
// 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{}
|
|
|
|
// Order is a placed crypto order.
|
|
type Order struct {
|
|
ID string `json:"id"`
|
|
}
|
|
|
|
// 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) {
|
|
var out Order
|
|
if err := c.parse(ctx, toolPlace, placeArgs(req, true), &out); err != nil {
|
|
return Order{}, err
|
|
}
|
|
return out, 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
|
|
}
|