a6bf8632ce
Typed result structs replace empty envelopes. Equity place sends ref_id only so live additionalProperties:false schemas accept the call.
299 lines
7.1 KiB
Go
299 lines
7.1 KiB
Go
package market
|
|
|
|
import (
|
|
"encoding/json"
|
|
"time"
|
|
|
|
decimal "github.com/alpacahq/alpacadecimal"
|
|
"s1d3sw1ped/robinhood-agentic-mcp/internal/wire"
|
|
)
|
|
|
|
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
|
|
}
|
|
|
|
func (r *IndexesResult) UnmarshalJSON(b []byte) error {
|
|
type row struct {
|
|
ID string `json:"id"`
|
|
Symbol string `json:"symbol"`
|
|
Name string `json:"name"`
|
|
}
|
|
rows, _, err := wire.UnmarshalRows[row](b, "indexes", "results")
|
|
if err != nil {
|
|
return err
|
|
}
|
|
r.Indexes = make([]Index, 0, len(rows))
|
|
for _, row := range rows {
|
|
r.Indexes = append(r.Indexes, Index{ID: row.ID, Symbol: row.Symbol, Name: row.Name})
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (r *IndexQuotesResult) UnmarshalJSON(b []byte) error {
|
|
type row struct {
|
|
InstrumentID string `json:"instrument_id"`
|
|
ID string `json:"id"`
|
|
Value any `json:"value"`
|
|
Last any `json:"last"`
|
|
State string `json:"state"`
|
|
}
|
|
rows, _, err := wire.UnmarshalRows[row](b, "quotes", "results")
|
|
if err != nil {
|
|
return err
|
|
}
|
|
r.Quotes = make([]IndexQuote, 0, len(rows))
|
|
for _, row := range rows {
|
|
id := row.InstrumentID
|
|
if id == "" {
|
|
id = row.ID
|
|
}
|
|
val, err := firstDec(row.Value, row.Last)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
r.Quotes = append(r.Quotes, IndexQuote{InstrumentID: id, Value: val, State: row.State})
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (r *IndexHistoricalsResult) 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"`
|
|
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
|
|
}
|
|
r.Bars = append(r.Bars, IndexBar{InstrumentID: id, Time: ts, Open: o, High: h, Low: l, Close: cl, Interpolated: p.Interpolated})
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (r *FinancialsResult) UnmarshalJSON(b []byte) error {
|
|
type row struct {
|
|
Symbol string `json:"symbol"`
|
|
Period string `json:"period"`
|
|
Revenue any `json:"revenue"`
|
|
GrossProfit any `json:"gross_profit"`
|
|
NetIncome any `json:"net_income"`
|
|
NetMargin any `json:"net_margin"`
|
|
}
|
|
rows, _, err := wire.UnmarshalRows[row](b, "financials", "results")
|
|
if err != nil {
|
|
return err
|
|
}
|
|
r.Periods = make([]FinancialPeriod, 0, len(rows))
|
|
for _, row := range rows {
|
|
rev, err := firstDec(row.Revenue)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
gp, err := firstDec(row.GrossProfit)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
ni, err := firstDec(row.NetIncome)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
nm, err := firstDec(row.NetMargin)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
r.Periods = append(r.Periods, FinancialPeriod{
|
|
Symbol: row.Symbol, Period: row.Period, Revenue: rev, GrossProfit: gp, NetIncome: ni, NetMargin: nm,
|
|
})
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (r *EarningsResultsResult) UnmarshalJSON(b []byte) error {
|
|
var wrap struct {
|
|
NextReportDate string `json:"next_report_date"`
|
|
ReportDate string `json:"report_date"`
|
|
Results []struct {
|
|
Report struct {
|
|
Date string `json:"date"`
|
|
} `json:"report"`
|
|
} `json:"results"`
|
|
}
|
|
if err := json.Unmarshal(wire.Unwrap(b), &wrap); err != nil {
|
|
return err
|
|
}
|
|
r.NextReportDate = wrap.NextReportDate
|
|
r.ReportDate = wrap.ReportDate
|
|
if r.ReportDate == "" && len(wrap.Results) > 0 {
|
|
r.ReportDate = wrap.Results[0].Report.Date
|
|
}
|
|
if r.NextReportDate == "" {
|
|
r.NextReportDate = r.ReportDate
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (r *EarningsCalendarResult) UnmarshalJSON(b []byte) error {
|
|
type row struct {
|
|
Symbol string `json:"symbol"`
|
|
ReportDate string `json:"report_date"`
|
|
Date string `json:"date"`
|
|
}
|
|
rows, _, err := wire.UnmarshalRows[row](b, "results", "earnings")
|
|
if err != nil {
|
|
return err
|
|
}
|
|
r.Events = make([]EarningsEvent, 0, len(rows))
|
|
for _, row := range rows {
|
|
d := row.ReportDate
|
|
if d == "" {
|
|
d = row.Date
|
|
}
|
|
r.Events = append(r.Events, EarningsEvent{Symbol: row.Symbol, ReportDate: d})
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (r *SECFilingIndexResult) UnmarshalJSON(b []byte) error {
|
|
type row struct {
|
|
FilingID string `json:"filing_id"`
|
|
ID string `json:"id"`
|
|
FormType string `json:"form_type"`
|
|
FiledAt string `json:"filed_at"`
|
|
Date string `json:"date"`
|
|
}
|
|
rows, next, err := wire.UnmarshalRows[row](b, "filings", "results")
|
|
if err != nil {
|
|
return err
|
|
}
|
|
r.NextCursor = next
|
|
r.Filings = make([]SECFilingRef, 0, len(rows))
|
|
for _, row := range rows {
|
|
id := row.FilingID
|
|
if id == "" {
|
|
id = row.ID
|
|
}
|
|
when := row.FiledAt
|
|
if when == "" {
|
|
when = row.Date
|
|
}
|
|
r.Filings = append(r.Filings, SECFilingRef{FilingID: id, FormType: row.FormType, FiledAt: when})
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (r *SECFilingResult) UnmarshalJSON(b []byte) error {
|
|
var wrap struct {
|
|
FilingID string `json:"filing_id"`
|
|
ID string `json:"id"`
|
|
Text string `json:"text"`
|
|
Sections []struct {
|
|
ID string `json:"id"`
|
|
Title string `json:"title"`
|
|
Text string `json:"text"`
|
|
} `json:"sections"`
|
|
TOC []struct {
|
|
ID string `json:"id"`
|
|
Title string `json:"title"`
|
|
} `json:"table_of_contents"`
|
|
}
|
|
if err := json.Unmarshal(wire.Unwrap(b), &wrap); err != nil {
|
|
return err
|
|
}
|
|
r.FilingID = wrap.FilingID
|
|
if r.FilingID == "" {
|
|
r.FilingID = wrap.ID
|
|
}
|
|
r.Text = wrap.Text
|
|
for _, s := range wrap.Sections {
|
|
r.Sections = append(r.Sections, SECSection{ID: s.ID, Title: s.Title, Text: s.Text})
|
|
}
|
|
if len(r.Sections) == 0 {
|
|
for _, s := range wrap.TOC {
|
|
r.Sections = append(r.Sections, SECSection{ID: s.ID, Title: s.Title})
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (r *SECFilingFactsResult) UnmarshalJSON(b []byte) error {
|
|
type row struct {
|
|
Concept string `json:"concept"`
|
|
Value string `json:"value"`
|
|
Unit string `json:"unit"`
|
|
}
|
|
rows, _, err := wire.UnmarshalRows[row](b, "facts", "results")
|
|
if err != nil {
|
|
return err
|
|
}
|
|
r.Facts = make([]SECFact, 0, len(rows))
|
|
for _, row := range rows {
|
|
r.Facts = append(r.Facts, SECFact{Concept: row.Concept, Value: row.Value, Unit: row.Unit})
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (r *SECFilingFactsCatalogResult) UnmarshalJSON(b []byte) error {
|
|
type row struct {
|
|
Name string `json:"name"`
|
|
Concept string `json:"concept"`
|
|
}
|
|
rows, _, err := wire.UnmarshalRows[row](b, "concepts", "results")
|
|
if err != nil {
|
|
return err
|
|
}
|
|
r.Concepts = make([]SECConcept, 0, len(rows))
|
|
for _, row := range rows {
|
|
n := row.Name
|
|
if n == "" {
|
|
n = row.Concept
|
|
}
|
|
r.Concepts = append(r.Concepts, SECConcept{Name: n})
|
|
}
|
|
return nil
|
|
}
|