package options import ( "context" "encoding/json" "time" decimal "github.com/alpacahq/alpacadecimal" "s1d3sw1ped/robinhood-agentic-mcp/client" "s1d3sw1ped/robinhood-agentic-mcp/internal/wire" ) const ( toolChains = "get_option_chains" toolInstruments = "get_option_instruments" toolQuotes = "get_option_quotes" toolPositions = "get_option_positions" toolOrders = "get_option_orders" toolHistoricals = "get_option_historicals" ) // ChainsRequest is the argument set for get_option_chains. type ChainsRequest struct { IDs string UnderlyingSymbol string } // OptionChain is one chain from get_option_chains. type OptionChain struct { ID string UnderlyingSymbol string ExpirationDates []string } // ChainsResult is the parsed get_option_chains payload. type ChainsResult struct { Chains []OptionChain } // InstrumentsRequest is the argument set for get_option_instruments. type InstrumentsRequest struct { ChainID string ChainSymbol string ExpirationDates string StrikePrice string Type string State string Tradability string IDs string Cursor string } // OptionInstrument is one contract from get_option_instruments. type OptionInstrument struct { ID string ChainID string ChainSymbol string ExpirationDate string StrikePrice decimal.Decimal Type string State string } // InstrumentsResult is the parsed get_option_instruments payload. type InstrumentsResult struct { Instruments []OptionInstrument NextCursor string } // QuotesRequest is the argument set for get_option_quotes. type QuotesRequest struct { InstrumentIDs []string } // OptionQuote is one contract quote from get_option_quotes. type OptionQuote struct { InstrumentID string Bid decimal.Decimal Ask decimal.Decimal Last decimal.Decimal PrevClose decimal.Decimal Mark decimal.Decimal } // QuotesResult is the parsed get_option_quotes payload. type QuotesResult struct { Quotes []OptionQuote } // PositionsRequest is the argument set for get_option_positions. type PositionsRequest struct { AccountNumber string Nonzero bool ChainIDs string OptionIDs string Type string OptionType string ExpirationDate string ExpirationDateLTE string ExpirationDateGTE string Cursor string } // OptionPosition is one holding from get_option_positions. type OptionPosition struct { OptionID string ChainID string Type string OptionType string Quantity decimal.Decimal AveragePrice decimal.Decimal ExpirationDate string } // PositionsResult is the parsed get_option_positions payload. type PositionsResult struct { Positions []OptionPosition NextCursor string } // OrdersRequest is the argument set for get_option_orders. type OrdersRequest struct { AccountNumber string OrderID string State string CreatedAtGTE string ChainIDs string UnderlyingType string PlacedAgent string Cursor string } // OrdersResult is the parsed get_option_orders payload. type OrdersResult struct { Orders []Order NextCursor string } // HistoricalsRequest is the argument set for get_option_historicals. type HistoricalsRequest struct { InstrumentIDs []string StartTime time.Time EndTime time.Time Interval string Bounds string } // OptionBar is one OHLC bar from get_option_historicals. type OptionBar struct { InstrumentID string Time time.Time Open, High, Low, Close decimal.Decimal Volume decimal.Decimal Interpolated bool } // HistoricalsResult is the parsed get_option_historicals payload. type HistoricalsResult struct { Bars []OptionBar } // Chains calls get_option_chains. func (c *Client) Chains(ctx context.Context, req ChainsRequest) (ChainsResult, error) { args := map[string]any{} if req.IDs != "" { args["ids"] = req.IDs } if req.UnderlyingSymbol != "" { args["underlying_symbol"] = req.UnderlyingSymbol } var out ChainsResult if err := c.parse(ctx, toolChains, args, &out); err != nil { return ChainsResult{}, err } return out, nil } // Instruments calls get_option_instruments. func (c *Client) Instruments(ctx context.Context, req InstrumentsRequest) (InstrumentsResult, error) { args := map[string]any{} if req.ChainID != "" { args["chain_id"] = req.ChainID } if req.ChainSymbol != "" { args["chain_symbol"] = req.ChainSymbol } if req.ExpirationDates != "" { args["expiration_dates"] = req.ExpirationDates } if req.StrikePrice != "" { args["strike_price"] = req.StrikePrice } if req.Type != "" { args["type"] = req.Type } if req.State != "" { args["state"] = req.State } if req.Tradability != "" { args["tradability"] = req.Tradability } if req.IDs != "" { args["ids"] = req.IDs } if req.Cursor != "" { args["cursor"] = req.Cursor } var out InstrumentsResult if err := c.parse(ctx, toolInstruments, args, &out); err != nil { return InstrumentsResult{}, err } return out, nil } // Quotes calls get_option_quotes. func (c *Client) Quotes(ctx context.Context, req QuotesRequest) (QuotesResult, error) { args := map[string]any{} if len(req.InstrumentIDs) > 0 { args["instrument_ids"] = req.InstrumentIDs } var out QuotesResult if err := c.parse(ctx, toolQuotes, args, &out); err != nil { return QuotesResult{}, err } return out, nil } // Positions calls get_option_positions. func (c *Client) Positions(ctx context.Context, req PositionsRequest) (PositionsResult, error) { args := map[string]any{} if req.AccountNumber != "" { args["account_number"] = req.AccountNumber } if req.Nonzero { args["nonzero"] = true } if req.ChainIDs != "" { args["chain_ids"] = req.ChainIDs } if req.OptionIDs != "" { args["option_ids"] = req.OptionIDs } if req.Type != "" { args["type"] = req.Type } if req.OptionType != "" { args["option_type"] = req.OptionType } if req.ExpirationDate != "" { args["expiration_date"] = req.ExpirationDate } if req.ExpirationDateLTE != "" { args["expiration_date_lte"] = req.ExpirationDateLTE } if req.ExpirationDateGTE != "" { args["expiration_date_gte"] = req.ExpirationDateGTE } 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_option_orders. func (c *Client) Orders(ctx context.Context, req OrdersRequest) (OrdersResult, error) { args := map[string]any{} if req.AccountNumber != "" { args["account_number"] = req.AccountNumber } if req.OrderID != "" { args["order_id"] = req.OrderID } if req.State != "" { args["state"] = req.State } if req.CreatedAtGTE != "" { args["created_at_gte"] = req.CreatedAtGTE } if req.ChainIDs != "" { args["chain_ids"] = req.ChainIDs } if req.UnderlyingType != "" { args["underlying_type"] = req.UnderlyingType } if req.PlacedAgent != "" { args["placed_agent"] = req.PlacedAgent } 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 } // Historicals calls get_option_historicals. func (c *Client) Historicals(ctx context.Context, req HistoricalsRequest) (HistoricalsResult, error) { args := map[string]any{ "start_time": req.StartTime.UTC().Format(time.RFC3339), } if len(req.InstrumentIDs) > 0 { args["instrument_ids"] = req.InstrumentIDs } if !req.EndTime.IsZero() { args["end_time"] = req.EndTime.UTC().Format(time.RFC3339) } if req.Interval != "" { args["interval"] = req.Interval } if req.Bounds != "" { args["bounds"] = req.Bounds } var out HistoricalsResult if err := c.parse(ctx, toolHistoricals, args, &out); err != nil { return HistoricalsResult{}, 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 }