a6bf8632ce
Typed result structs replace empty envelopes. Equity place sends ref_id only so live additionalProperties:false schemas accept the call.
276 lines
6.7 KiB
Go
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
|
|
}
|