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

393 lines
10 KiB
Go

package market
import (
"context"
"encoding/json"
"time"
decimal "github.com/alpacahq/alpacadecimal"
"s1d3sw1ped/robinhood-agentic-mcp/client"
"s1d3sw1ped/robinhood-agentic-mcp/internal/wire"
)
const (
toolIndexes = "get_indexes"
toolIndexQuotes = "get_index_quotes"
toolIndexHistoricals = "get_index_historicals"
toolFinancials = "get_financials"
toolEarningsResults = "get_earnings_results"
toolEarningsCalendar = "get_earnings_calendar"
toolSECFilingIndex = "get_sec_filing_index"
toolSECFiling = "get_sec_filing"
toolSECFilingFacts = "get_sec_filing_facts"
toolSECFilingFactsCatalog = "get_sec_filing_facts_catalog"
)
// IndexesRequest is the argument set for get_indexes.
type IndexesRequest struct {
Symbols string // comma-separated; live schema is a string, not an array
}
// Index is one market index from get_indexes.
type Index struct {
ID string
Symbol string
Name string
}
// IndexesResult is the parsed get_indexes payload.
type IndexesResult struct {
Indexes []Index
}
// IndexQuotesRequest is the argument set for get_index_quotes.
type IndexQuotesRequest struct {
InstrumentIDs []string
}
// IndexQuote is a live index level from get_index_quotes.
type IndexQuote struct {
InstrumentID string
Value decimal.Decimal
State string
}
// IndexQuotesResult is the parsed get_index_quotes payload.
type IndexQuotesResult struct {
Quotes []IndexQuote
}
// IndexHistoricalsRequest is the argument set for get_index_historicals.
type IndexHistoricalsRequest struct {
InstrumentIDs []string
StartTime time.Time
EndTime time.Time
Interval string // required — no hidden default
}
// IndexBar is one OHLC bar from get_index_historicals.
type IndexBar struct {
InstrumentID string
Time time.Time
Open, High, Low, Close decimal.Decimal
Interpolated bool
}
// IndexHistoricalsResult is the parsed get_index_historicals payload.
type IndexHistoricalsResult struct {
Bars []IndexBar
}
// FinancialsRequest is the argument set for get_financials.
type FinancialsRequest struct {
Symbols []string
Period string
Limit int
}
// FinancialPeriod is one fiscal period from get_financials.
type FinancialPeriod struct {
Symbol string
Period string
Revenue decimal.Decimal
GrossProfit decimal.Decimal
NetIncome decimal.Decimal
NetMargin decimal.Decimal
}
// FinancialsResult is the parsed get_financials payload.
type FinancialsResult struct {
Periods []FinancialPeriod
}
// EarningsResultsRequest is the argument set for get_earnings_results.
type EarningsResultsRequest struct {
Symbol string
}
// EarningsResultsResult is the parsed get_earnings_results payload.
type EarningsResultsResult struct {
NextReportDate string `json:"next_report_date"`
ReportDate string `json:"report_date"`
}
// EarningsCalendarRequest is the argument set for get_earnings_calendar.
type EarningsCalendarRequest struct {
StartDate string
Days int
Filter string
}
// EarningsEvent is one calendar row from get_earnings_calendar.
type EarningsEvent struct {
Symbol string
ReportDate string
}
// EarningsCalendarResult is the parsed get_earnings_calendar payload.
type EarningsCalendarResult struct {
Events []EarningsEvent
}
// SECFilingIndexRequest is the argument set for get_sec_filing_index.
type SECFilingIndexRequest struct {
Symbol string
FormType []string
Since string
Until string
Cursor string
}
// SECFilingRef is one filing from get_sec_filing_index.
type SECFilingRef struct {
FilingID string
FormType string
FiledAt string
}
// SECFilingIndexResult is the parsed get_sec_filing_index payload.
type SECFilingIndexResult struct {
Filings []SECFilingRef
NextCursor string
}
// SECFilingRequest is the argument set for get_sec_filing.
type SECFilingRequest struct {
FilingID string
Section string
}
// SECSection is one table-of-contents row or section body.
type SECSection struct {
ID string
Title string
Text string
}
// SECFilingResult is the parsed get_sec_filing payload.
type SECFilingResult struct {
FilingID string
Sections []SECSection
Text string
}
// SECFilingFactsRequest is the argument set for get_sec_filing_facts.
type SECFilingFactsRequest struct {
FilingIDs []string
Concepts []string
}
// SECFilingFactsResult is the parsed get_sec_filing_facts payload.
type SECFact struct {
Concept string
Value string
Unit string
}
type SECFilingFactsResult struct {
Facts []SECFact
}
// SECFilingFactsCatalogRequest is the argument set for get_sec_filing_facts_catalog.
type SECFilingFactsCatalogRequest struct {
FilingID string
ConceptContains string
AxisNameIn []string
Offset int
}
// SECFilingFactsCatalogResult is the parsed get_sec_filing_facts_catalog payload.
type SECConcept struct {
Name string
}
type SECFilingFactsCatalogResult struct {
Concepts []SECConcept
}
// Indexes calls get_indexes.
func (c *Client) Indexes(ctx context.Context, req IndexesRequest) (IndexesResult, error) {
args := map[string]any{}
if req.Symbols != "" {
args["symbols"] = req.Symbols
}
var out IndexesResult
if err := c.parse(ctx, toolIndexes, args, &out); err != nil {
return IndexesResult{}, err
}
return out, nil
}
// IndexQuotes calls get_index_quotes.
func (c *Client) IndexQuotes(ctx context.Context, req IndexQuotesRequest) (IndexQuotesResult, error) {
args := map[string]any{}
if len(req.InstrumentIDs) > 0 {
args["instrument_ids"] = req.InstrumentIDs
}
var out IndexQuotesResult
if err := c.parse(ctx, toolIndexQuotes, args, &out); err != nil {
return IndexQuotesResult{}, err
}
return out, nil
}
// IndexHistoricals calls get_index_historicals. start_time and interval are always sent (required; no hidden default).
func (c *Client) IndexHistoricals(ctx context.Context, req IndexHistoricalsRequest) (IndexHistoricalsResult, error) {
args := map[string]any{
"start_time": req.StartTime.UTC().Format(time.RFC3339),
"interval": req.Interval,
}
if len(req.InstrumentIDs) > 0 {
args["instrument_ids"] = req.InstrumentIDs
}
if !req.EndTime.IsZero() {
args["end_time"] = req.EndTime.UTC().Format(time.RFC3339)
}
var out IndexHistoricalsResult
if err := c.parse(ctx, toolIndexHistoricals, args, &out); err != nil {
return IndexHistoricalsResult{}, err
}
return out, nil
}
// Financials calls get_financials.
func (c *Client) Financials(ctx context.Context, req FinancialsRequest) (FinancialsResult, error) {
args := map[string]any{}
if len(req.Symbols) > 0 {
args["symbols"] = req.Symbols
}
if req.Period != "" {
args["period"] = req.Period
}
if req.Limit != 0 {
args["limit"] = req.Limit
}
var out FinancialsResult
if err := c.parse(ctx, toolFinancials, args, &out); err != nil {
return FinancialsResult{}, err
}
return out, nil
}
// EarningsResults calls get_earnings_results.
func (c *Client) EarningsResults(ctx context.Context, req EarningsResultsRequest) (EarningsResultsResult, error) {
args := map[string]any{}
if req.Symbol != "" {
args["symbol"] = req.Symbol
}
var out EarningsResultsResult
if err := c.parse(ctx, toolEarningsResults, args, &out); err != nil {
return EarningsResultsResult{}, err
}
return out, nil
}
// EarningsCalendar calls get_earnings_calendar.
func (c *Client) EarningsCalendar(ctx context.Context, req EarningsCalendarRequest) (EarningsCalendarResult, error) {
args := map[string]any{}
if req.StartDate != "" {
args["start_date"] = req.StartDate
}
if req.Days != 0 {
args["days"] = req.Days
}
if req.Filter != "" {
args["filter"] = req.Filter
}
var out EarningsCalendarResult
if err := c.parse(ctx, toolEarningsCalendar, args, &out); err != nil {
return EarningsCalendarResult{}, err
}
return out, nil
}
// SECFilingIndex calls get_sec_filing_index.
func (c *Client) SECFilingIndex(ctx context.Context, req SECFilingIndexRequest) (SECFilingIndexResult, error) {
args := map[string]any{}
if req.Symbol != "" {
args["symbol"] = req.Symbol
}
if len(req.FormType) > 0 {
args["form_type"] = req.FormType
}
if req.Since != "" {
args["since"] = req.Since
}
if req.Until != "" {
args["until"] = req.Until
}
if req.Cursor != "" {
args["cursor"] = req.Cursor
}
var out SECFilingIndexResult
if err := c.parse(ctx, toolSECFilingIndex, args, &out); err != nil {
return SECFilingIndexResult{}, err
}
return out, nil
}
// SECFiling calls get_sec_filing.
func (c *Client) SECFiling(ctx context.Context, req SECFilingRequest) (SECFilingResult, error) {
args := map[string]any{}
if req.FilingID != "" {
args["filing_id"] = req.FilingID
}
if req.Section != "" {
args["section"] = req.Section
}
var out SECFilingResult
if err := c.parse(ctx, toolSECFiling, args, &out); err != nil {
return SECFilingResult{}, err
}
return out, nil
}
// SECFilingFacts calls get_sec_filing_facts.
func (c *Client) SECFilingFacts(ctx context.Context, req SECFilingFactsRequest) (SECFilingFactsResult, error) {
args := map[string]any{}
if len(req.FilingIDs) > 0 {
args["filing_ids"] = req.FilingIDs
}
if len(req.Concepts) > 0 {
args["concepts"] = req.Concepts
}
var out SECFilingFactsResult
if err := c.parse(ctx, toolSECFilingFacts, args, &out); err != nil {
return SECFilingFactsResult{}, err
}
return out, nil
}
// SECFilingFactsCatalog calls get_sec_filing_facts_catalog.
func (c *Client) SECFilingFactsCatalog(ctx context.Context, req SECFilingFactsCatalogRequest) (SECFilingFactsCatalogResult, error) {
args := map[string]any{}
if req.FilingID != "" {
args["filing_id"] = req.FilingID
}
if req.ConceptContains != "" {
args["concept_contains"] = req.ConceptContains
}
if len(req.AxisNameIn) > 0 {
args["axis_name_in"] = req.AxisNameIn
}
if req.Offset != 0 {
args["offset"] = req.Offset
}
var out SECFilingFactsCatalogResult
if err := c.parse(ctx, toolSECFilingFactsCatalog, args, &out); err != nil {
return SECFilingFactsCatalogResult{}, err
}
return out, nil
}
func (c *Client) parse(ctx context.Context, tool string, args map[string]any, dest any) error {
raw, err := c.c.Call(ctx, tool, args)
if err != nil {
return err
}
if err := json.Unmarshal(wire.Unwrap(raw), dest); err != nil {
return client.ToolErrorf(tool, "parse: %w", err)
}
return nil
}