Files
robinhood-agentic-mcp/equity/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

329 lines
8.0 KiB
Go

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
}