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.
This commit is contained in:
+70
-6
@@ -5,6 +5,7 @@ import (
|
||||
"encoding/json"
|
||||
"time"
|
||||
|
||||
decimal "github.com/alpacahq/alpacadecimal"
|
||||
"s1d3sw1ped/robinhood-agentic-mcp/client"
|
||||
"s1d3sw1ped/robinhood-agentic-mcp/internal/wire"
|
||||
)
|
||||
@@ -24,8 +25,17 @@ type ChainsRequest struct {
|
||||
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{}
|
||||
type ChainsResult struct {
|
||||
Chains []OptionChain
|
||||
}
|
||||
|
||||
// InstrumentsRequest is the argument set for get_option_instruments.
|
||||
type InstrumentsRequest struct {
|
||||
@@ -40,16 +50,42 @@ type InstrumentsRequest struct {
|
||||
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{}
|
||||
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{}
|
||||
type QuotesResult struct {
|
||||
Quotes []OptionQuote
|
||||
}
|
||||
|
||||
// PositionsRequest is the argument set for get_option_positions.
|
||||
type PositionsRequest struct {
|
||||
@@ -65,8 +101,22 @@ type PositionsRequest struct {
|
||||
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{}
|
||||
type PositionsResult struct {
|
||||
Positions []OptionPosition
|
||||
NextCursor string
|
||||
}
|
||||
|
||||
// OrdersRequest is the argument set for get_option_orders.
|
||||
type OrdersRequest struct {
|
||||
@@ -81,7 +131,10 @@ type OrdersRequest struct {
|
||||
}
|
||||
|
||||
// OrdersResult is the parsed get_option_orders payload.
|
||||
type OrdersResult struct{}
|
||||
type OrdersResult struct {
|
||||
Orders []Order
|
||||
NextCursor string
|
||||
}
|
||||
|
||||
// HistoricalsRequest is the argument set for get_option_historicals.
|
||||
type HistoricalsRequest struct {
|
||||
@@ -92,8 +145,19 @@ type HistoricalsRequest struct {
|
||||
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{}
|
||||
type HistoricalsResult struct {
|
||||
Bars []OptionBar
|
||||
}
|
||||
|
||||
// Chains calls get_option_chains.
|
||||
func (c *Client) Chains(ctx context.Context, req ChainsRequest) (ChainsResult, error) {
|
||||
|
||||
@@ -0,0 +1,275 @@
|
||||
package options
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"time"
|
||||
|
||||
decimal "github.com/alpacahq/alpacadecimal"
|
||||
"s1d3sw1ped/robinhood-agentic-mcp/internal/wire"
|
||||
)
|
||||
|
||||
func (o *Order) UnmarshalJSON(b []byte) error {
|
||||
var row struct {
|
||||
ID string `json:"id"`
|
||||
State string `json:"state"`
|
||||
Status string `json:"status"`
|
||||
Quantity any `json:"quantity"`
|
||||
Price any `json:"price"`
|
||||
}
|
||||
if err := json.Unmarshal(b, &row); err != nil {
|
||||
return err
|
||||
}
|
||||
o.ID = row.ID
|
||||
o.State = row.State
|
||||
if o.State == "" {
|
||||
o.State = row.Status
|
||||
}
|
||||
var err error
|
||||
if o.Qty, err = firstDec(row.Quantity); err != nil {
|
||||
return err
|
||||
}
|
||||
if o.Price, err = firstDec(row.Price); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func parseOrders(raw json.RawMessage) ([]Order, error) {
|
||||
rows, _, err := wire.UnmarshalRows[Order](raw, "orders", "results")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if len(rows) > 0 {
|
||||
return rows, nil
|
||||
}
|
||||
var one Order
|
||||
if err := json.Unmarshal(wire.Unwrap(raw), &one); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if one.ID != "" {
|
||||
return []Order{one}, nil
|
||||
}
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func (r *ChainsResult) UnmarshalJSON(b []byte) error {
|
||||
type row struct {
|
||||
ID string `json:"id"`
|
||||
UnderlyingSymbol string `json:"underlying_symbol"`
|
||||
Symbol string `json:"symbol"`
|
||||
ExpirationDates []string `json:"expiration_dates"`
|
||||
}
|
||||
rows, _, err := wire.UnmarshalRows[row](b, "results", "chains")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
r.Chains = make([]OptionChain, 0, len(rows))
|
||||
for _, row := range rows {
|
||||
sym := row.UnderlyingSymbol
|
||||
if sym == "" {
|
||||
sym = row.Symbol
|
||||
}
|
||||
r.Chains = append(r.Chains, OptionChain{ID: row.ID, UnderlyingSymbol: sym, ExpirationDates: row.ExpirationDates})
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *InstrumentsResult) UnmarshalJSON(b []byte) error {
|
||||
type row struct {
|
||||
ID string `json:"id"`
|
||||
ChainID string `json:"chain_id"`
|
||||
ChainSymbol string `json:"chain_symbol"`
|
||||
ExpirationDate string `json:"expiration_date"`
|
||||
StrikePrice any `json:"strike_price"`
|
||||
Type string `json:"type"`
|
||||
State string `json:"state"`
|
||||
}
|
||||
rows, next, err := wire.UnmarshalRows[row](b, "results", "instruments")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
r.NextCursor = next
|
||||
r.Instruments = make([]OptionInstrument, 0, len(rows))
|
||||
for _, row := range rows {
|
||||
px, err := firstDec(row.StrikePrice)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
r.Instruments = append(r.Instruments, OptionInstrument{
|
||||
ID: row.ID, ChainID: row.ChainID, ChainSymbol: row.ChainSymbol,
|
||||
ExpirationDate: row.ExpirationDate, StrikePrice: px, Type: row.Type, State: row.State,
|
||||
})
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *QuotesResult) UnmarshalJSON(b []byte) error {
|
||||
type row struct {
|
||||
InstrumentID string `json:"instrument_id"`
|
||||
ID string `json:"id"`
|
||||
Bid any `json:"bid"`
|
||||
BidPrice any `json:"bid_price"`
|
||||
Ask any `json:"ask"`
|
||||
AskPrice any `json:"ask_price"`
|
||||
Last any `json:"last"`
|
||||
LastPrice any `json:"last_trade_price"`
|
||||
PrevClose any `json:"previous_close"`
|
||||
Mark any `json:"mark"`
|
||||
MarkPrice any `json:"mark_price"`
|
||||
}
|
||||
rows, _, err := wire.UnmarshalRows[row](b, "quotes", "results")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
r.Quotes = make([]OptionQuote, 0, len(rows))
|
||||
for _, row := range rows {
|
||||
id := row.InstrumentID
|
||||
if id == "" {
|
||||
id = row.ID
|
||||
}
|
||||
bid, err := firstDec(row.BidPrice, row.Bid)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
ask, err := firstDec(row.AskPrice, row.Ask)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
last, err := firstDec(row.LastPrice, row.Last)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
prev, err := firstDec(row.PrevClose)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
mark, err := firstDec(row.MarkPrice, row.Mark)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
r.Quotes = append(r.Quotes, OptionQuote{InstrumentID: id, Bid: bid, Ask: ask, Last: last, PrevClose: prev, Mark: mark})
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *PositionsResult) UnmarshalJSON(b []byte) error {
|
||||
type row struct {
|
||||
OptionID string `json:"option_id"`
|
||||
ID string `json:"id"`
|
||||
ChainID string `json:"chain_id"`
|
||||
Type string `json:"type"`
|
||||
OptionType string `json:"option_type"`
|
||||
Quantity any `json:"quantity"`
|
||||
AveragePrice any `json:"average_price"`
|
||||
ExpirationDate string `json:"expiration_date"`
|
||||
}
|
||||
rows, next, err := wire.UnmarshalRows[row](b, "results", "positions")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
r.NextCursor = next
|
||||
r.Positions = make([]OptionPosition, 0, len(rows))
|
||||
for _, row := range rows {
|
||||
id := row.OptionID
|
||||
if id == "" {
|
||||
id = row.ID
|
||||
}
|
||||
qty, err := firstDec(row.Quantity)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
avg, err := firstDec(row.AveragePrice)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
r.Positions = append(r.Positions, OptionPosition{
|
||||
OptionID: id, ChainID: row.ChainID, Type: row.Type, OptionType: row.OptionType,
|
||||
Quantity: qty, AveragePrice: avg, ExpirationDate: row.ExpirationDate,
|
||||
})
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *OrdersResult) UnmarshalJSON(b []byte) error {
|
||||
rows, next, err := wire.UnmarshalRows[Order](b, "orders", "results")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if len(rows) == 0 {
|
||||
var one Order
|
||||
if json.Unmarshal(wire.Unwrap(b), &one) == nil && one.ID != "" {
|
||||
rows = []Order{one}
|
||||
}
|
||||
}
|
||||
r.Orders = rows
|
||||
r.NextCursor = next
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *HistoricalsResult) UnmarshalJSON(b []byte) error {
|
||||
type pt struct {
|
||||
BeginsAt string `json:"begins_at"`
|
||||
Open any `json:"open"`
|
||||
High any `json:"high"`
|
||||
Low any `json:"low"`
|
||||
Close any `json:"close"`
|
||||
Volume any `json:"volume"`
|
||||
Interpolated bool `json:"interpolated"`
|
||||
}
|
||||
type series struct {
|
||||
InstrumentID string `json:"instrument_id"`
|
||||
ID string `json:"id"`
|
||||
DataPoints []pt `json:"data_points"`
|
||||
}
|
||||
rows, _, err := wire.UnmarshalRows[series](b, "historicals", "results")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
for _, s := range rows {
|
||||
id := s.InstrumentID
|
||||
if id == "" {
|
||||
id = s.ID
|
||||
}
|
||||
for _, p := range s.DataPoints {
|
||||
ts, err := time.Parse(time.RFC3339, p.BeginsAt)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
o, err := firstDec(p.Open)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
h, err := firstDec(p.High)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
l, err := firstDec(p.Low)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
cl, err := firstDec(p.Close)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
vol, err := firstDec(p.Volume)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
r.Bars = append(r.Bars, OptionBar{InstrumentID: id, Time: ts, Open: o, High: h, Low: l, Close: cl, Volume: vol, Interpolated: p.Interpolated})
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func firstDec(vs ...any) (decimal.Decimal, error) {
|
||||
for _, v := range vs {
|
||||
if v == nil {
|
||||
continue
|
||||
}
|
||||
if s, ok := v.(string); ok && s == "" {
|
||||
continue
|
||||
}
|
||||
return wire.Dec(v)
|
||||
}
|
||||
return decimal.Zero, nil
|
||||
}
|
||||
+34
-10
@@ -57,11 +57,18 @@ type ReplaceOrderRequest struct {
|
||||
}
|
||||
|
||||
// ReviewResult is the pre-trade check from review_option_order.
|
||||
type ReviewResult struct{}
|
||||
type ReviewResult struct {
|
||||
Errors []string `json:"errors"`
|
||||
Warnings []string `json:"warnings"`
|
||||
Alerts []string `json:"alerts"`
|
||||
}
|
||||
|
||||
// Order is a placed or replaced option order.
|
||||
// Order is a placed, replaced, or listed option order.
|
||||
type Order struct {
|
||||
ID string `json:"id"`
|
||||
ID string `json:"id"`
|
||||
State string `json:"state"`
|
||||
Qty decimal.Decimal `json:"-"`
|
||||
Price decimal.Decimal `json:"-"`
|
||||
}
|
||||
|
||||
// CancelOrderRequest is the argument set for cancel_option_order.
|
||||
@@ -81,7 +88,10 @@ type ExerciseRequest struct {
|
||||
}
|
||||
|
||||
// ExerciseResult is the parsed exercise_option payload.
|
||||
type ExerciseResult struct{}
|
||||
type ExerciseResult struct {
|
||||
ID string `json:"id"`
|
||||
State string `json:"state"`
|
||||
}
|
||||
|
||||
// CancelExerciseRequest is the argument set for cancel_option_exercise.
|
||||
type CancelExerciseRequest struct {
|
||||
@@ -100,11 +110,18 @@ func (c *Client) ReviewOrder(ctx context.Context, req PlaceOrderRequest) (Review
|
||||
|
||||
// PlaceOrder calls place_option_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, false), &out); err != nil {
|
||||
raw, err := c.c.Call(ctx, toolPlace, placeArgs(req, true, false))
|
||||
if err != nil {
|
||||
return Order{}, err
|
||||
}
|
||||
return out, nil
|
||||
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_option_order.
|
||||
@@ -137,11 +154,18 @@ func (c *Client) ReplaceOrder(ctx context.Context, req ReplaceOrderRequest) (Ord
|
||||
if req.OrderID != "" {
|
||||
args["order_id"] = req.OrderID
|
||||
}
|
||||
var out Order
|
||||
if err := c.parse(ctx, toolReplace, args, &out); err != nil {
|
||||
raw, err := c.c.Call(ctx, toolReplace, args)
|
||||
if err != nil {
|
||||
return Order{}, err
|
||||
}
|
||||
return out, nil
|
||||
ords, err := parseOrders(raw)
|
||||
if err != nil {
|
||||
return Order{}, client.ToolErrorf(toolReplace, "parse: %w", err)
|
||||
}
|
||||
if len(ords) > 0 {
|
||||
return ords[0], nil
|
||||
}
|
||||
return Order{}, nil
|
||||
}
|
||||
|
||||
// Exercise calls exercise_option.
|
||||
|
||||
Reference in New Issue
Block a user