Files
robinhood-agentic-mcp/options/results.go
T
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

276 lines
6.7 KiB
Go

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
}