Files
s1d3sw1ped a6bf8632ce fix: parse MCP result payloads and drop place idempotency_key
Typed result structs replace empty envelopes. Equity place sends
ref_id only so live additionalProperties:false schemas accept the call.
2026-09-01 14:18:39 -05:00

336 lines
8.2 KiB
Go

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
}