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:
@@ -0,0 +1,328 @@
|
||||
package equity
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"time"
|
||||
|
||||
"s1d3sw1ped/robinhood-agentic-mcp/internal/wire"
|
||||
)
|
||||
|
||||
func (r *PositionsResult) UnmarshalJSON(b []byte) error {
|
||||
type row struct {
|
||||
Symbol string `json:"symbol"`
|
||||
Quantity any `json:"quantity"`
|
||||
Qty any `json:"qty"`
|
||||
AverageBuyPrice any `json:"average_buy_price"`
|
||||
AvgCost any `json:"avg_cost"`
|
||||
}
|
||||
rows, next, err := wire.UnmarshalRows[row](b, "positions", "results")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
r.NextCursor = next
|
||||
r.Positions = make([]Position, 0, len(rows))
|
||||
for _, row := range rows {
|
||||
qty, err := firstDec(row.Quantity, row.Qty)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
avg, err := firstDec(row.AverageBuyPrice, row.AvgCost)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
r.Positions = append(r.Positions, Position{Symbol: row.Symbol, Qty: qty, AvgCost: avg})
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *TaxLotsResult) UnmarshalJSON(b []byte) error {
|
||||
type row struct {
|
||||
OpenLotID string `json:"open_lot_id"`
|
||||
ID string `json:"id"`
|
||||
Quantity any `json:"quantity"`
|
||||
QuantityAvailable any `json:"quantity_available"`
|
||||
CostBasis any `json:"cost_basis"`
|
||||
AcquiredAt string `json:"acquired_at"`
|
||||
AcquisitionDate string `json:"acquisition_date"`
|
||||
Term string `json:"term"`
|
||||
HoldingPeriod string `json:"holding_period"`
|
||||
}
|
||||
rows, next, err := wire.UnmarshalRows[row](b, "tax_lots", "lots", "results")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
r.NextCursor = next
|
||||
r.Lots = make([]TaxLotRow, 0, len(rows))
|
||||
for _, row := range rows {
|
||||
id := row.OpenLotID
|
||||
if id == "" {
|
||||
id = row.ID
|
||||
}
|
||||
qty, err := firstDec(row.Quantity)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
avail, err := firstDec(row.QuantityAvailable)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
cost, err := firstDec(row.CostBasis)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
when := row.AcquiredAt
|
||||
if when == "" {
|
||||
when = row.AcquisitionDate
|
||||
}
|
||||
term := row.Term
|
||||
if term == "" {
|
||||
term = row.HoldingPeriod
|
||||
}
|
||||
r.Lots = append(r.Lots, TaxLotRow{
|
||||
OpenLotID: id,
|
||||
Quantity: qty,
|
||||
QuantityAvail: avail,
|
||||
CostBasis: cost,
|
||||
AcquiredAt: when,
|
||||
Term: term,
|
||||
})
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *OrdersResult) UnmarshalJSON(b []byte) error {
|
||||
rows, next, err := wire.UnmarshalRows[Order](b, "orders", "results")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
r.NextCursor = next
|
||||
if len(rows) == 0 {
|
||||
var one Order
|
||||
if err := json.Unmarshal(wire.Unwrap(b), &one); err == nil && one.ID != "" {
|
||||
rows = []Order{one}
|
||||
}
|
||||
}
|
||||
r.Orders = rows
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *TradabilityResult) UnmarshalJSON(b []byte) error {
|
||||
type row struct {
|
||||
Symbol string `json:"symbol"`
|
||||
Tradable bool `json:"tradable"`
|
||||
IsTradable bool `json:"is_tradable"`
|
||||
Fractional bool `json:"fractional"`
|
||||
FractionalOk bool `json:"fractional_tradable"`
|
||||
FractionalTrad bool `json:"fractionally_tradable"`
|
||||
}
|
||||
rows, _, err := wire.UnmarshalRows[row](b, "results", "tradability", "instruments")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if len(rows) == 0 {
|
||||
var one row
|
||||
if json.Unmarshal(wire.Unwrap(b), &one) == nil && (one.Symbol != "" || one.Tradable || one.IsTradable || one.Fractional) {
|
||||
rows = []row{one}
|
||||
}
|
||||
}
|
||||
r.Symbols = make([]SymbolTradability, 0, len(rows))
|
||||
for _, row := range rows {
|
||||
r.Symbols = append(r.Symbols, SymbolTradability{
|
||||
Symbol: row.Symbol,
|
||||
Tradable: row.Tradable || row.IsTradable,
|
||||
Fractional: row.Fractional || row.FractionalOk || row.FractionalTrad,
|
||||
})
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *FundamentalsResult) UnmarshalJSON(b []byte) error {
|
||||
type row struct {
|
||||
Symbol string `json:"symbol"`
|
||||
AverageVolume any `json:"average_volume"`
|
||||
AvgVolume any `json:"avg_volume"`
|
||||
Type string `json:"type"`
|
||||
Instrument string `json:"instrument_kind"`
|
||||
MarketCap any `json:"market_cap"`
|
||||
PE any `json:"pe_ratio"`
|
||||
High52 any `json:"high_52_weeks"`
|
||||
Low52 any `json:"low_52_weeks"`
|
||||
}
|
||||
rows, _, err := wire.UnmarshalRows[row](b, "fundamentals", "results")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if len(rows) == 0 {
|
||||
var one row
|
||||
if json.Unmarshal(wire.Unwrap(b), &one) == nil && one.Symbol != "" {
|
||||
rows = []row{one}
|
||||
}
|
||||
}
|
||||
r.Fundamentals = make([]Fundamentals, 0, len(rows))
|
||||
for _, row := range rows {
|
||||
kind := row.Instrument
|
||||
if kind == "" {
|
||||
kind = row.Type
|
||||
}
|
||||
avg, err := firstDec(row.AverageVolume, row.AvgVolume)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
cap, err := firstDec(row.MarketCap)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
pe, err := firstDec(row.PE)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
hi, err := firstDec(row.High52)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
lo, err := firstDec(row.Low52)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
r.Fundamentals = append(r.Fundamentals, Fundamentals{
|
||||
Symbol: row.Symbol,
|
||||
AvgVolume: avg,
|
||||
InstrumentKind: kind,
|
||||
MarketCap: cap,
|
||||
PE: pe,
|
||||
High52Week: hi,
|
||||
Low52Week: lo,
|
||||
})
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *PriceBookResult) UnmarshalJSON(b []byte) error {
|
||||
type level struct {
|
||||
Price any `json:"price"`
|
||||
Quantity any `json:"quantity"`
|
||||
Size any `json:"size"`
|
||||
}
|
||||
type book struct {
|
||||
Symbol string `json:"symbol"`
|
||||
Bids []level `json:"bids"`
|
||||
Asks []level `json:"asks"`
|
||||
}
|
||||
rows, _, err := wire.UnmarshalRows[book](b, "books", "price_books", "results")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if len(rows) == 0 {
|
||||
var one book
|
||||
if json.Unmarshal(wire.Unwrap(b), &one) == nil && one.Symbol != "" {
|
||||
rows = []book{one}
|
||||
}
|
||||
}
|
||||
levels := func(in []level) ([]BookLevel, error) {
|
||||
out := make([]BookLevel, 0, len(in))
|
||||
for _, lv := range in {
|
||||
px, err := firstDec(lv.Price)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
qty, err := firstDec(lv.Quantity, lv.Size)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
out = append(out, BookLevel{Price: px, Quantity: qty})
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
r.Books = make([]PriceBook, 0, len(rows))
|
||||
for _, row := range rows {
|
||||
bids, err := levels(row.Bids)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
asks, err := levels(row.Asks)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
r.Books = append(r.Books, PriceBook{Symbol: row.Symbol, Bids: bids, Asks: asks})
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *TechnicalIndicatorsResult) UnmarshalJSON(b []byte) error {
|
||||
type row struct {
|
||||
BeginsAt string `json:"begins_at"`
|
||||
Time string `json:"time"`
|
||||
Timestamp string `json:"timestamp"`
|
||||
Value any `json:"value"`
|
||||
Upper any `json:"upper"`
|
||||
Lower any `json:"lower"`
|
||||
MACD any `json:"macd"`
|
||||
Signal any `json:"signal"`
|
||||
}
|
||||
rows, _, err := wire.UnmarshalRows[row](b, "data_points", "points", "results", "values")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
r.Points = make([]IndicatorPoint, 0, len(rows))
|
||||
for _, row := range rows {
|
||||
s := row.BeginsAt
|
||||
if s == "" {
|
||||
s = row.Time
|
||||
}
|
||||
if s == "" {
|
||||
s = row.Timestamp
|
||||
}
|
||||
ts, err := time.Parse(time.RFC3339, s)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
val, err := firstDec(row.Value)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
up, err := firstDec(row.Upper)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
lo, err := firstDec(row.Lower)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
macd, err := firstDec(row.MACD)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
sig, err := firstDec(row.Signal)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
r.Points = append(r.Points, IndicatorPoint{Time: ts, Value: val, Upper: up, Lower: lo, MACD: macd, Signal: sig})
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *NewsResult) UnmarshalJSON(b []byte) error {
|
||||
type row struct {
|
||||
Title string `json:"title"`
|
||||
URL string `json:"url"`
|
||||
PublishedAt string `json:"published_at"`
|
||||
Source string `json:"source"`
|
||||
Summary string `json:"summary"`
|
||||
}
|
||||
rows, next, err := wire.UnmarshalRows[row](b, "news", "articles", "results")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
r.NextCursor = next
|
||||
r.Articles = make([]NewsArticle, 0, len(rows))
|
||||
for _, row := range rows {
|
||||
r.Articles = append(r.Articles, NewsArticle{
|
||||
Title: row.Title,
|
||||
URL: row.URL,
|
||||
PublishedAt: row.PublishedAt,
|
||||
Source: row.Source,
|
||||
Summary: row.Summary,
|
||||
})
|
||||
}
|
||||
return nil
|
||||
}
|
||||
Reference in New Issue
Block a user