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:
2026-09-01 14:18:39 -05:00
parent f52a19181d
commit a6bf8632ce
22 changed files with 2225 additions and 74 deletions
+46 -5
View File
@@ -60,8 +60,20 @@ type RealizedPnLRequest struct {
Timezone string
}
// PnLBucket is one span bucket from get_realized_pnl.
type PnLBucket struct {
Label string
Amount decimal.Decimal
Percent decimal.Decimal
Trades int
}
// RealizedPnLResult is the parsed get_realized_pnl payload.
type RealizedPnLResult struct{}
type RealizedPnLResult struct {
Total decimal.Decimal
Percent decimal.Decimal
Buckets []PnLBucket
}
// PnLTradeHistoryRequest is the argument set for get_pnl_trade_history.
type PnLTradeHistoryRequest struct {
@@ -71,8 +83,20 @@ type PnLTradeHistoryRequest struct {
Cursor string
}
// PnLTrade is one realizing trade from get_pnl_trade_history.
type PnLTrade struct {
Symbol string
Side string
Quantity decimal.Decimal
Price decimal.Decimal
PnL decimal.Decimal
}
// PnLTradeHistoryResult is the parsed get_pnl_trade_history payload.
type PnLTradeHistoryResult struct{}
type PnLTradeHistoryResult struct {
Trades []PnLTrade
NextCursor string
}
// AccountNumberRequest is a single account_number argument.
type AccountNumberRequest struct {
@@ -80,10 +104,17 @@ type AccountNumberRequest struct {
}
// UpgradeInfoResult is the parsed upgrade-info payload.
type UpgradeInfoResult struct{}
type UpgradeInfoResult struct {
URL string
WebURL string
MobileURL string
}
// OnboardingInfoResult is the parsed crypto onboarding payload.
type OnboardingInfoResult struct{}
type OnboardingInfoResult struct {
URL string
WebURL string
}
// SearchRequest is the argument set for search.
type SearchRequest struct {
@@ -92,8 +123,18 @@ type SearchRequest struct {
Limit int
}
// SearchHit is one instrument/pair/index from search.
type SearchHit struct {
Symbol string
Name string
ID string
InstrumentID string
}
// SearchResult is the parsed search payload.
type SearchResult struct{}
type SearchResult struct {
Results []SearchHit
}
// Accounts calls get_accounts and returns every account (no IRA/margin filter).
func (c *Client) Accounts(ctx context.Context, req AccountsRequest) (AccountsResult, error) {
+14
View File
@@ -208,3 +208,17 @@ func TestPortfolio_rhntest(t *testing.T) {
t.Fatalf("buying power %s", got.BuyingPower)
}
}
func TestSearch_parsesResults(t *testing.T) {
t.Parallel()
c := accounts.New(client.Func(func(ctx context.Context, name string, args map[string]any) (json.RawMessage, error) {
return json.RawMessage(`{"results":[{"symbol":"AAPL","name":"Apple","instrument_id":"i1"}]}`), nil
}))
got, err := c.Search(context.Background(), accounts.SearchRequest{Query: "apple"})
if err != nil {
t.Fatal(err)
}
if len(got.Results) != 1 || got.Results[0].Symbol != "AAPL" || got.Results[0].InstrumentID != "i1" {
t.Fatalf("%+v", got)
}
}
+190
View File
@@ -0,0 +1,190 @@
package accounts
import (
"encoding/json"
decimal "github.com/alpacahq/alpacadecimal"
"s1d3sw1ped/robinhood-agentic-mcp/internal/wire"
)
func (r *RealizedPnLResult) UnmarshalJSON(b []byte) error {
var wrap struct {
Total any `json:"total"`
TotalGain any `json:"total_gain"`
Percent any `json:"percent"`
TotalPct any `json:"total_percent"`
Buckets []struct {
Label string `json:"label"`
Name string `json:"name"`
Amount any `json:"amount"`
Gain any `json:"gain"`
Percent any `json:"percent"`
Trades int `json:"trades"`
Count int `json:"count"`
} `json:"buckets"`
Results []struct {
Label string `json:"label"`
Amount any `json:"amount"`
Percent any `json:"percent"`
Trades int `json:"trades"`
} `json:"results"`
}
if err := json.Unmarshal(wire.Unwrap(b), &wrap); err != nil {
return err
}
var err error
if r.Total, err = firstDec(wrap.Total, wrap.TotalGain); err != nil {
return err
}
if r.Percent, err = firstDec(wrap.Percent, wrap.TotalPct); err != nil {
return err
}
src := wrap.Buckets
if len(src) == 0 {
for _, row := range wrap.Results {
src = append(src, struct {
Label string `json:"label"`
Name string `json:"name"`
Amount any `json:"amount"`
Gain any `json:"gain"`
Percent any `json:"percent"`
Trades int `json:"trades"`
Count int `json:"count"`
}{Label: row.Label, Amount: row.Amount, Percent: row.Percent, Trades: row.Trades})
}
}
r.Buckets = make([]PnLBucket, 0, len(src))
for _, row := range src {
label := row.Label
if label == "" {
label = row.Name
}
amt, err := firstDec(row.Amount, row.Gain)
if err != nil {
return err
}
pct, err := firstDec(row.Percent)
if err != nil {
return err
}
n := row.Trades
if n == 0 {
n = row.Count
}
r.Buckets = append(r.Buckets, PnLBucket{Label: label, Amount: amt, Percent: pct, Trades: n})
}
return nil
}
func (r *PnLTradeHistoryResult) UnmarshalJSON(b []byte) error {
type row struct {
Symbol string `json:"symbol"`
Side string `json:"side"`
Quantity any `json:"quantity"`
Price any `json:"price"`
PnL any `json:"realized_gain"`
Gain any `json:"gain"`
}
rows, next, err := wire.UnmarshalRows[row](b, "trades", "results")
if err != nil {
return err
}
r.NextCursor = next
r.Trades = make([]PnLTrade, 0, len(rows))
for _, row := range rows {
qty, err := firstDec(row.Quantity)
if err != nil {
return err
}
px, err := firstDec(row.Price)
if err != nil {
return err
}
pnl, err := firstDec(row.PnL, row.Gain)
if err != nil {
return err
}
r.Trades = append(r.Trades, PnLTrade{Symbol: row.Symbol, Side: row.Side, Quantity: qty, Price: px, PnL: pnl})
}
return nil
}
func (r *UpgradeInfoResult) UnmarshalJSON(b []byte) error {
var wrap struct {
URL string `json:"url"`
WebURL string `json:"web_url"`
MobileURL string `json:"mobile_url"`
Links struct {
Web string `json:"web"`
Mobile string `json:"mobile"`
} `json:"links"`
}
if err := json.Unmarshal(wire.Unwrap(b), &wrap); err != nil {
return err
}
r.URL = wrap.URL
r.WebURL = wrap.WebURL
if r.WebURL == "" {
r.WebURL = wrap.Links.Web
}
r.MobileURL = wrap.MobileURL
if r.MobileURL == "" {
r.MobileURL = wrap.Links.Mobile
}
if r.URL == "" {
r.URL = r.WebURL
}
return nil
}
func (r *OnboardingInfoResult) UnmarshalJSON(b []byte) error {
var wrap struct {
URL string `json:"url"`
WebURL string `json:"web_url"`
}
if err := json.Unmarshal(wire.Unwrap(b), &wrap); err != nil {
return err
}
r.URL = wrap.URL
r.WebURL = wrap.WebURL
if r.URL == "" {
r.URL = r.WebURL
}
return nil
}
func (r *SearchResult) UnmarshalJSON(b []byte) error {
type row struct {
Symbol string `json:"symbol"`
Name string `json:"name"`
ID string `json:"id"`
InstrumentID string `json:"instrument_id"`
}
rows, _, err := wire.UnmarshalRows[row](b, "results", "instruments")
if err != nil {
return err
}
r.Results = make([]SearchHit, 0, len(rows))
for _, row := range rows {
r.Results = append(r.Results, SearchHit{
Symbol: row.Symbol,
Name: row.Name,
ID: row.ID,
InstrumentID: row.InstrumentID,
})
}
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
}
+58 -9
View File
@@ -25,8 +25,17 @@ type PairsRequest struct {
Limit int
}
// CurrencyPair is one row from get_currency_pairs.
type CurrencyPair struct {
ID string
Symbol string
}
// PairsResult is the parsed get_currency_pairs payload.
type PairsResult struct{}
type PairsResult struct {
Pairs []CurrencyPair
NextCursor string
}
// QuotesRequest is the argument set for get_crypto_quotes.
type QuotesRequest struct {
@@ -35,8 +44,19 @@ type QuotesRequest struct {
RHSAccountNumber string
}
// CryptoQuote is one pair quote from get_crypto_quotes.
type CryptoQuote struct {
Symbol string
Bid decimal.Decimal
Ask decimal.Decimal
Mark decimal.Decimal
PrevClose decimal.Decimal
}
// QuotesResult is the parsed get_crypto_quotes payload.
type QuotesResult struct{}
type QuotesResult struct {
Quotes []CryptoQuote
}
// PositionsRequest is the argument set for get_crypto_positions.
type PositionsRequest struct {
@@ -44,8 +64,18 @@ type PositionsRequest struct {
Cursor string
}
// CryptoPosition is one holding from get_crypto_positions.
type CryptoPosition struct {
Symbol string
Quantity decimal.Decimal
CostBasis decimal.Decimal
}
// PositionsResult is the parsed get_crypto_positions payload.
type PositionsResult struct{}
type PositionsResult struct {
Positions []CryptoPosition
NextCursor string
}
// OrdersRequest is the argument set for get_crypto_orders.
type OrdersRequest struct {
@@ -61,7 +91,10 @@ type OrdersRequest struct {
}
// OrdersResult is the parsed get_crypto_orders payload.
type OrdersResult struct{}
type OrdersResult struct {
Orders []Order
NextCursor string
}
// PlaceOrderRequest is the argument set for preview_crypto_order and place_crypto_order.
type PlaceOrderRequest struct {
@@ -78,11 +111,20 @@ type PlaceOrderRequest struct {
}
// PreviewResult is the pre-trade check from preview_crypto_order.
type PreviewResult struct{}
type PreviewResult struct {
Errors []string `json:"errors"`
Warnings []string `json:"warnings"`
Quantity decimal.Decimal `json:"-"`
Price decimal.Decimal `json:"-"`
}
// Order is a placed crypto order.
// Order is a placed or listed crypto order.
type Order struct {
ID string `json:"id"`
Symbol string `json:"symbol"`
Side string `json:"side"`
State string `json:"state"`
Qty decimal.Decimal `json:"-"`
}
// CancelOrderRequest is the argument set for cancel_crypto_order.
@@ -190,11 +232,18 @@ func (c *Client) PreviewOrder(ctx context.Context, req PlaceOrderRequest) (Previ
// PlaceOrder calls place_crypto_order. RefID is sent as ref_id.
func (c *Client) PlaceOrder(ctx context.Context, req PlaceOrderRequest) (Order, error) {
var out Order
if err := c.parse(ctx, toolPlace, placeArgs(req, true), &out); err != nil {
raw, err := c.c.Call(ctx, toolPlace, placeArgs(req, true))
if err != nil {
return Order{}, err
}
return out, nil
ords, err := parseOrders(raw)
if err != nil {
return Order{}, client.ToolErrorf(toolPlace, "parse: %w", err)
}
if len(ords) > 0 {
return ords[0], nil
}
return Order{}, nil
}
// CancelOrder calls cancel_crypto_order.
+174
View File
@@ -0,0 +1,174 @@
package crypto
import (
"encoding/json"
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"`
Symbol string `json:"symbol"`
Side string `json:"side"`
State string `json:"state"`
Status string `json:"status"`
Quantity any `json:"quantity"`
}
if err := json.Unmarshal(b, &row); err != nil {
return err
}
o.ID, o.Symbol, o.Side = row.ID, row.Symbol, row.Side
o.State = row.State
if o.State == "" {
o.State = row.Status
}
var err error
o.Qty, err = firstDec(row.Quantity)
return err
}
func parseOrders(raw json.RawMessage) ([]Order, error) {
rows, _, err := wire.UnmarshalRows[Order](raw, "results", "orders")
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 *PairsResult) UnmarshalJSON(b []byte) error {
type row struct {
ID string `json:"id"`
Symbol string `json:"symbol"`
}
rows, next, err := wire.UnmarshalRows[row](b, "results", "pairs")
if err != nil {
return err
}
r.NextCursor = next
r.Pairs = make([]CurrencyPair, 0, len(rows))
for _, row := range rows {
r.Pairs = append(r.Pairs, CurrencyPair{ID: row.ID, Symbol: row.Symbol})
}
return nil
}
func (r *QuotesResult) UnmarshalJSON(b []byte) error {
type row struct {
Symbol string `json:"symbol"`
Bid any `json:"bid"`
Ask any `json:"ask"`
Mark any `json:"mark"`
PrevClose any `json:"open_price"`
Close any `json:"previous_close"`
}
rows, _, err := wire.UnmarshalRows[row](b, "results", "quotes")
if err != nil {
return err
}
r.Quotes = make([]CryptoQuote, 0, len(rows))
for _, row := range rows {
bid, err := firstDec(row.Bid)
if err != nil {
return err
}
ask, err := firstDec(row.Ask)
if err != nil {
return err
}
mark, err := firstDec(row.Mark)
if err != nil {
return err
}
prev, err := firstDec(row.PrevClose, row.Close)
if err != nil {
return err
}
r.Quotes = append(r.Quotes, CryptoQuote{Symbol: row.Symbol, Bid: bid, Ask: ask, Mark: mark, PrevClose: prev})
}
return nil
}
func (r *PositionsResult) UnmarshalJSON(b []byte) error {
type row struct {
Symbol string `json:"symbol"`
Asset string `json:"asset"`
Quantity any `json:"quantity"`
CostBasis any `json:"cost_basis"`
}
rows, next, err := wire.UnmarshalRows[row](b, "results", "positions")
if err != nil {
return err
}
r.NextCursor = next
r.Positions = make([]CryptoPosition, 0, len(rows))
for _, row := range rows {
sym := row.Symbol
if sym == "" {
sym = row.Asset
}
qty, err := firstDec(row.Quantity)
if err != nil {
return err
}
cost, err := firstDec(row.CostBasis)
if err != nil {
return err
}
r.Positions = append(r.Positions, CryptoPosition{Symbol: sym, Quantity: qty, CostBasis: cost})
}
return nil
}
func (r *OrdersResult) UnmarshalJSON(b []byte) error {
rows, next, err := wire.UnmarshalRows[Order](b, "results", "orders")
if err != nil {
return err
}
r.Orders = rows
r.NextCursor = next
return nil
}
func (r *PreviewResult) UnmarshalJSON(b []byte) error {
var wrap struct {
Errors []string `json:"errors"`
Warnings []string `json:"warnings"`
Quantity any `json:"quantity"`
Price any `json:"price"`
}
if err := json.Unmarshal(wire.Unwrap(b), &wrap); err != nil {
return err
}
r.Errors, r.Warnings = wrap.Errors, wrap.Warnings
var err error
if r.Quantity, err = firstDec(wrap.Quantity); err != nil {
return err
}
r.Price, err = firstDec(wrap.Price)
return err
}
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
}
+95 -8
View File
@@ -29,8 +29,18 @@ type PositionsRequest struct {
Cursor string
}
// Position is one holding from get_equity_positions.
type Position struct {
Symbol string
Qty decimal.Decimal
AvgCost decimal.Decimal
}
// PositionsResult is the parsed get_equity_positions payload.
type PositionsResult struct{}
type PositionsResult struct {
Positions []Position
NextCursor string
}
// TaxLotsRequest is the argument set for get_equity_tax_lots.
type TaxLotsRequest struct {
@@ -39,8 +49,21 @@ type TaxLotsRequest struct {
Cursor string
}
// TaxLotRow is one open tax lot from get_equity_tax_lots.
type TaxLotRow struct {
OpenLotID string
Quantity decimal.Decimal
CostBasis decimal.Decimal
AcquiredAt string
Term string
QuantityAvail decimal.Decimal
}
// TaxLotsResult is the parsed get_equity_tax_lots payload.
type TaxLotsResult struct{}
type TaxLotsResult struct {
Lots []TaxLotRow
NextCursor string
}
// QuotesRequest is the argument set for get_equity_quotes.
type QuotesRequest struct {
@@ -74,7 +97,10 @@ type OrdersRequest struct {
}
// OrdersResult is the parsed get_equity_orders payload.
type OrdersResult struct{}
type OrdersResult struct {
Orders []Order
NextCursor string
}
// TradabilityRequest is the argument set for get_equity_tradability.
type TradabilityRequest struct {
@@ -82,8 +108,17 @@ type TradabilityRequest struct {
Symbols []string
}
// SymbolTradability is per-symbol eligibility from get_equity_tradability.
type SymbolTradability struct {
Symbol string
Tradable bool
Fractional bool
}
// TradabilityResult is the parsed get_equity_tradability payload.
type TradabilityResult struct{}
type TradabilityResult struct {
Symbols []SymbolTradability
}
// HistoricalsRequest is the argument set for get_equity_historicals.
type HistoricalsRequest struct {
@@ -115,16 +150,44 @@ type FundamentalsRequest struct {
Bounds string
}
// Fundamentals is one symbol's facts from get_equity_fundamentals.
type Fundamentals struct {
Symbol string
AvgVolume decimal.Decimal
InstrumentKind string
MarketCap decimal.Decimal
PE decimal.Decimal
High52Week decimal.Decimal
Low52Week decimal.Decimal
}
// FundamentalsResult is the parsed get_equity_fundamentals payload.
type FundamentalsResult struct{}
type FundamentalsResult struct {
Fundamentals []Fundamentals
}
// PriceBookRequest is the argument set for get_equity_price_book.
type PriceBookRequest struct {
Symbols []string
}
// BookLevel is one bid or ask rung.
type BookLevel struct {
Price decimal.Decimal
Quantity decimal.Decimal
}
// PriceBook is a Level-2 snapshot for one symbol.
type PriceBook struct {
Symbol string
Bids []BookLevel
Asks []BookLevel
}
// PriceBookResult is the parsed get_equity_price_book payload.
type PriceBookResult struct{}
type PriceBookResult struct {
Books []PriceBook
}
// TechnicalIndicatorsRequest is the argument set for get_equity_technical_indicators.
type TechnicalIndicatorsRequest struct {
@@ -145,8 +208,20 @@ type TechnicalIndicatorsRequest struct {
Method string
}
// IndicatorPoint is one computed indicator bar.
type IndicatorPoint struct {
Time time.Time
Value decimal.Decimal
Upper decimal.Decimal
Lower decimal.Decimal
MACD decimal.Decimal
Signal decimal.Decimal
}
// TechnicalIndicatorsResult is the parsed get_equity_technical_indicators payload.
type TechnicalIndicatorsResult struct{}
type TechnicalIndicatorsResult struct {
Points []IndicatorPoint
}
// NewsRequest is the argument set for get_equity_news.
type NewsRequest struct {
@@ -155,8 +230,20 @@ type NewsRequest struct {
Cursor string
}
// NewsArticle is one article from get_equity_news.
type NewsArticle struct {
Title string
URL string
PublishedAt string
Source string
Summary string
}
// NewsResult is the parsed get_equity_news payload.
type NewsResult struct{}
type NewsResult struct {
Articles []NewsArticle
NextCursor string
}
// Positions calls get_equity_positions.
func (c *Client) Positions(ctx context.Context, req PositionsRequest) (PositionsResult, error) {
+328
View File
@@ -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
}
+70
View File
@@ -0,0 +1,70 @@
package equity_test
import (
"context"
"encoding/json"
"testing"
decimal "github.com/alpacahq/alpacadecimal"
"s1d3sw1ped/robinhood-agentic-mcp/client"
"s1d3sw1ped/robinhood-agentic-mcp/equity"
)
func TestPositions_parsesEnvelope(t *testing.T) {
t.Parallel()
c := equity.New(client.Func(func(ctx context.Context, name string, args map[string]any) (json.RawMessage, error) {
return json.RawMessage(`{"positions":[{"symbol":"MU","quantity":"3","average_buy_price":"99.6"}],"next_cursor":"n1"}`), nil
}))
got, err := c.Positions(context.Background(), equity.PositionsRequest{AccountNumber: "acct"})
if err != nil {
t.Fatal(err)
}
if got.NextCursor != "n1" || len(got.Positions) != 1 || got.Positions[0].Symbol != "MU" {
t.Fatalf("%+v", got)
}
if !got.Positions[0].Qty.Equal(decimal.NewFromInt(3)) || !got.Positions[0].AvgCost.Equal(decimal.RequireFromString("99.6")) {
t.Fatalf("money %+v", got.Positions[0])
}
}
func TestOrders_parsesList(t *testing.T) {
t.Parallel()
c := equity.New(client.Func(func(ctx context.Context, name string, args map[string]any) (json.RawMessage, error) {
return json.RawMessage(`{"orders":[{"id":"o1","symbol":"MU","state":"filled","quantity":"2"}]}`), nil
}))
got, err := c.Orders(context.Background(), equity.OrdersRequest{AccountNumber: "acct"})
if err != nil {
t.Fatal(err)
}
if len(got.Orders) != 1 || got.Orders[0].ID != "o1" || !got.Orders[0].Qty.Equal(decimal.NewFromInt(2)) {
t.Fatalf("%+v", got)
}
}
func TestTradability_parsesFlags(t *testing.T) {
t.Parallel()
c := equity.New(client.Func(func(ctx context.Context, name string, args map[string]any) (json.RawMessage, error) {
return json.RawMessage(`{"symbol":"MU","tradable":true,"fractional_tradable":true}`), nil
}))
got, err := c.Tradability(context.Background(), equity.TradabilityRequest{AccountNumber: "acct", Symbols: []string{"MU"}})
if err != nil {
t.Fatal(err)
}
if len(got.Symbols) != 1 || !got.Symbols[0].Tradable || !got.Symbols[0].Fractional {
t.Fatalf("%+v", got)
}
}
func TestNews_parsesArticles(t *testing.T) {
t.Parallel()
c := equity.New(client.Func(func(ctx context.Context, name string, args map[string]any) (json.RawMessage, error) {
return json.RawMessage(`{"results":[{"title":"Hi","url":"https://x","published_at":"2026-01-01T00:00:00Z"}],"next_cursor":"c9"}`), nil
}))
got, err := c.News(context.Background(), equity.NewsRequest{Symbol: "MU"})
if err != nil {
t.Fatal(err)
}
if got.NextCursor != "c9" || len(got.Articles) != 1 || got.Articles[0].Title != "Hi" {
t.Fatalf("%+v", got)
}
}
+80 -6
View File
@@ -2,6 +2,7 @@ package equity
import (
"context"
"encoding/json"
decimal "github.com/alpacahq/alpacadecimal"
"s1d3sw1ped/robinhood-agentic-mcp/client"
@@ -42,9 +43,73 @@ type ReviewResult struct {
Warnings []string `json:"warnings"`
}
// Order is a placed equity order.
// Order is a placed or listed equity order.
type Order struct {
ID string `json:"id"`
IdempotencyKey string `json:"idempotency_key"`
Symbol string `json:"symbol"`
Side string `json:"side"`
Qty decimal.Decimal `json:"-"`
FilledQty decimal.Decimal `json:"-"`
Price decimal.Decimal `json:"-"`
State string `json:"state"`
}
func (o *Order) UnmarshalJSON(b []byte) error {
var row struct {
ID string `json:"id"`
IdempotencyKey string `json:"idempotency_key"`
Symbol string `json:"symbol"`
Side string `json:"side"`
Quantity any `json:"quantity"`
Qty any `json:"qty"`
FilledQty any `json:"filled_quantity"`
CumulativeQty any `json:"cumulative_quantity"`
Price any `json:"price"`
AveragePrice any `json:"average_price"`
State string `json:"state"`
Status string `json:"status"`
}
if err := json.Unmarshal(b, &row); err != nil {
return err
}
o.ID = row.ID
o.IdempotencyKey = row.IdempotencyKey
o.Symbol = row.Symbol
o.Side = row.Side
o.State = row.State
if o.State == "" {
o.State = row.Status
}
var err error
if o.Qty, err = firstDec(row.Quantity, row.Qty); err != nil {
return err
}
if o.FilledQty, err = firstDec(row.FilledQty, row.CumulativeQty); err != nil {
return err
}
if o.Price, err = firstDec(row.Price, row.AveragePrice); 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
}
// CancelOrderRequest is the argument set for cancel_equity_order.
@@ -62,13 +127,23 @@ func (c *Client) ReviewOrder(ctx context.Context, req PlaceOrderRequest) (Review
return out, nil
}
// PlaceOrder calls place_equity_order. RefID is sent as both ref_id and idempotency_key.
// PlaceOrder calls place_equity_order. RefID is sent as ref_id only.
func (c *Client) PlaceOrder(ctx context.Context, req PlaceOrderRequest) (Order, error) {
var out Order
if err := c.parse(ctx, toolPlace, placeArgs(req, true), &out); err != nil {
raw, err := c.c.Call(ctx, toolPlace, placeArgs(req, true))
if err != nil {
return Order{}, err
}
return out, nil
ords, err := parseOrders(raw)
if err != nil {
return Order{}, client.ToolErrorf(toolPlace, "parse: %w", err)
}
if len(ords) == 1 {
return ords[0], nil
}
if len(ords) > 1 {
return ords[0], nil
}
return Order{}, nil
}
// CancelOrder calls cancel_equity_order.
@@ -128,7 +203,6 @@ func placeArgs(req PlaceOrderRequest, withRef bool) map[string]any {
}
if withRef && req.RefID != "" {
args["ref_id"] = req.RefID
args["idempotency_key"] = req.RefID
}
return args
}
+25 -2
View File
@@ -40,6 +40,26 @@ func TestPlaceOrder_decimalStrings(t *testing.T) {
if got["ref_id"] != "buy:2026-08-18:MU" {
t.Fatalf("ref %v", got["ref_id"])
}
if _, ok := got["idempotency_key"]; ok {
t.Fatalf("idempotency_key %v", got["idempotency_key"])
}
}
func TestPlaceOrder_ordersEnvelope(t *testing.T) {
t.Parallel()
c := equity.New(client.Func(func(ctx context.Context, name string, args map[string]any) (json.RawMessage, error) {
return json.RawMessage(`{"orders":[{"id":"o2","symbol":"MU","side":"buy","quantity":"3","state":"confirmed"}]}`), nil
}))
got, err := c.PlaceOrder(context.Background(), equity.PlaceOrderRequest{Symbol: "MU"})
if err != nil {
t.Fatal(err)
}
if got.ID != "o2" || got.Symbol != "MU" || got.Side != "buy" || got.State != "confirmed" {
t.Fatalf("%+v", got)
}
if !got.Qty.Equal(decimal.NewFromInt(3)) {
t.Fatalf("qty %s", got.Qty)
}
}
func TestEquity_writeArgs(t *testing.T) {
@@ -105,7 +125,6 @@ func TestEquity_writeArgs(t *testing.T) {
"quantity": "3",
"stop_price": "99.6",
"ref_id": "sell:2026-08-18:MU",
"idempotency_key": "sell:2026-08-18:MU",
},
},
{
@@ -136,7 +155,6 @@ func TestEquity_writeArgs(t *testing.T) {
{"open_lot_id": "lot-1", "quantity": "1.5"},
},
"ref_id": "sell:lots",
"idempotency_key": "sell:lots",
},
},
{
@@ -182,6 +200,11 @@ func TestEquity_writeArgs(t *testing.T) {
t.Fatalf("time_in_force injected: %+v", gotArgs)
}
}
if tc.wantName == "place_equity_order" {
if _, ok := gotArgs["idempotency_key"]; ok {
t.Fatalf("idempotency_key on place: %+v", gotArgs)
}
}
})
}
}
+76
View File
@@ -0,0 +1,76 @@
package wire
import (
"encoding/json"
"net/url"
"strings"
)
// UnmarshalRows unpacks a list from an object (trying keys in order) or a JSON array.
// next is next_cursor, else the cursor query param of next, else next itself when it
// is not a URL.
func UnmarshalRows[T any](raw json.RawMessage, keys ...string) (rows []T, next string, err error) {
data := Unwrap(raw)
var obj map[string]json.RawMessage
if json.Unmarshal(data, &obj) != nil {
var list []T
if err := json.Unmarshal(data, &list); err != nil {
return nil, "", err
}
return list, "", nil
}
next = cursorFromMap(obj)
for _, k := range keys {
item, ok := obj[k]
if !ok || len(item) == 0 || string(item) == "null" {
continue
}
var list []T
if json.Unmarshal(item, &list) == nil {
return list, next, nil
}
var one T
if json.Unmarshal(item, &one) == nil {
return []T{one}, next, nil
}
}
return nil, next, nil
}
func cursorFromMap(obj map[string]json.RawMessage) string {
if c := stringField(obj, "next_cursor"); c != "" {
return c
}
return Cursor(stringField(obj, "next"), "")
}
func stringField(obj map[string]json.RawMessage, key string) string {
raw, ok := obj[key]
if !ok || len(raw) == 0 || string(raw) == "null" {
return ""
}
var s string
if json.Unmarshal(raw, &s) == nil {
return s
}
return ""
}
// Cursor prefers nextCursor; otherwise extracts cursor from a next URL.
func Cursor(next, nextCursor string) string {
if nextCursor != "" {
return nextCursor
}
next = strings.TrimSpace(next)
if next == "" {
return ""
}
u, err := url.Parse(next)
if err != nil || (u.Scheme == "" && u.Host == "" && !strings.Contains(next, "?")) {
return next
}
if v := u.Query().Get("cursor"); v != "" {
return v
}
return ""
}
+49
View File
@@ -0,0 +1,49 @@
package wire_test
import (
"encoding/json"
"testing"
"s1d3sw1ped/robinhood-agentic-mcp/internal/wire"
)
func TestUnmarshalRows_objectAndArray(t *testing.T) {
t.Parallel()
type row struct {
ID string `json:"id"`
}
rows, next, err := wire.UnmarshalRows[row](json.RawMessage(`{"data":{"positions":[{"id":"a"}],"next_cursor":"c2"}}`), "positions", "results")
if err != nil {
t.Fatal(err)
}
if len(rows) != 1 || rows[0].ID != "a" || next != "c2" {
t.Fatalf("%+v %q", rows, next)
}
rows, next, err = wire.UnmarshalRows[row](json.RawMessage(`[{"id":"b"}]`), "positions")
if err != nil {
t.Fatal(err)
}
if len(rows) != 1 || rows[0].ID != "b" || next != "" {
t.Fatalf("%+v %q", rows, next)
}
rows, next, err = wire.UnmarshalRows[row](json.RawMessage(`{"results":{"id":"one"},"next":"https://x/?cursor=n3"}`), "positions", "results")
if err != nil {
t.Fatal(err)
}
if len(rows) != 1 || rows[0].ID != "one" || next != "n3" {
t.Fatalf("%+v %q", rows, next)
}
}
func TestCursor(t *testing.T) {
t.Parallel()
if wire.Cursor("https://x/y?cursor=abc", "") != "abc" {
t.Fatal("url")
}
if wire.Cursor("rawtok", "pref") != "pref" {
t.Fatal("pref")
}
if wire.Cursor("rawtok", "") != "rawtok" {
t.Fatal("bare")
}
}
+93 -9
View File
@@ -5,6 +5,7 @@ import (
"encoding/json"
"time"
decimal "github.com/alpacahq/alpacadecimal"
"s1d3sw1ped/robinhood-agentic-mcp/client"
"s1d3sw1ped/robinhood-agentic-mcp/internal/wire"
)
@@ -27,16 +28,34 @@ 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{}
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{}
type IndexQuotesResult struct {
Quotes []IndexQuote
}
// IndexHistoricalsRequest is the argument set for get_index_historicals.
type IndexHistoricalsRequest struct {
@@ -46,8 +65,18 @@ type IndexHistoricalsRequest struct {
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{}
type IndexHistoricalsResult struct {
Bars []IndexBar
}
// FinancialsRequest is the argument set for get_financials.
type FinancialsRequest struct {
@@ -56,8 +85,20 @@ type FinancialsRequest struct {
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{}
type FinancialsResult struct {
Periods []FinancialPeriod
}
// EarningsResultsRequest is the argument set for get_earnings_results.
type EarningsResultsRequest struct {
@@ -77,8 +118,16 @@ type EarningsCalendarRequest struct {
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{}
type EarningsCalendarResult struct {
Events []EarningsEvent
}
// SECFilingIndexRequest is the argument set for get_sec_filing_index.
type SECFilingIndexRequest struct {
@@ -89,8 +138,18 @@ type SECFilingIndexRequest struct {
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{}
type SECFilingIndexResult struct {
Filings []SECFilingRef
NextCursor string
}
// SECFilingRequest is the argument set for get_sec_filing.
type SECFilingRequest struct {
@@ -98,8 +157,19 @@ type SECFilingRequest struct {
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{}
type SECFilingResult struct {
FilingID string
Sections []SECSection
Text string
}
// SECFilingFactsRequest is the argument set for get_sec_filing_facts.
type SECFilingFactsRequest struct {
@@ -108,7 +178,15 @@ type SECFilingFactsRequest struct {
}
// SECFilingFactsResult is the parsed get_sec_filing_facts payload.
type SECFilingFactsResult struct{}
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 {
@@ -119,7 +197,13 @@ type SECFilingFactsCatalogRequest struct {
}
// SECFilingFactsCatalogResult is the parsed get_sec_filing_facts_catalog payload.
type SECFilingFactsCatalogResult struct{}
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) {
+14
View File
@@ -241,3 +241,17 @@ func TestEarningsResults_rhntest(t *testing.T) {
t.Fatal(diff)
}
}
func TestEarningsResults_liveEnvelope(t *testing.T) {
t.Parallel()
c := market.New(client.Func(func(ctx context.Context, name string, args map[string]any) (json.RawMessage, error) {
return json.RawMessage(`{"data":{"results":[{"report":{"date":"2026-07-15"}}]}}`), nil
}))
got, err := c.EarningsResults(context.Background(), market.EarningsResultsRequest{Symbol: "MU"})
if err != nil {
t.Fatal(err)
}
if got.ReportDate != "2026-07-15" || got.NextReportDate != "2026-07-15" {
t.Fatalf("%+v", got)
}
}
+298
View File
@@ -0,0 +1,298 @@
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
}
+70 -6
View File
@@ -5,6 +5,7 @@ import (
"encoding/json"
"time"
decimal "github.com/alpacahq/alpacadecimal"
"s1d3sw1ped/robinhood-agentic-mcp/client"
"s1d3sw1ped/robinhood-agentic-mcp/internal/wire"
)
@@ -24,8 +25,17 @@ type ChainsRequest struct {
UnderlyingSymbol string
}
// OptionChain is one chain from get_option_chains.
type OptionChain struct {
ID string
UnderlyingSymbol string
ExpirationDates []string
}
// ChainsResult is the parsed get_option_chains payload.
type ChainsResult struct{}
type ChainsResult struct {
Chains []OptionChain
}
// InstrumentsRequest is the argument set for get_option_instruments.
type InstrumentsRequest struct {
@@ -40,16 +50,42 @@ type InstrumentsRequest struct {
Cursor string
}
// OptionInstrument is one contract from get_option_instruments.
type OptionInstrument struct {
ID string
ChainID string
ChainSymbol string
ExpirationDate string
StrikePrice decimal.Decimal
Type string
State string
}
// InstrumentsResult is the parsed get_option_instruments payload.
type InstrumentsResult struct{}
type InstrumentsResult struct {
Instruments []OptionInstrument
NextCursor string
}
// QuotesRequest is the argument set for get_option_quotes.
type QuotesRequest struct {
InstrumentIDs []string
}
// OptionQuote is one contract quote from get_option_quotes.
type OptionQuote struct {
InstrumentID string
Bid decimal.Decimal
Ask decimal.Decimal
Last decimal.Decimal
PrevClose decimal.Decimal
Mark decimal.Decimal
}
// QuotesResult is the parsed get_option_quotes payload.
type QuotesResult struct{}
type QuotesResult struct {
Quotes []OptionQuote
}
// PositionsRequest is the argument set for get_option_positions.
type PositionsRequest struct {
@@ -65,8 +101,22 @@ type PositionsRequest struct {
Cursor string
}
// OptionPosition is one holding from get_option_positions.
type OptionPosition struct {
OptionID string
ChainID string
Type string
OptionType string
Quantity decimal.Decimal
AveragePrice decimal.Decimal
ExpirationDate string
}
// PositionsResult is the parsed get_option_positions payload.
type PositionsResult struct{}
type PositionsResult struct {
Positions []OptionPosition
NextCursor string
}
// OrdersRequest is the argument set for get_option_orders.
type OrdersRequest struct {
@@ -81,7 +131,10 @@ type OrdersRequest struct {
}
// OrdersResult is the parsed get_option_orders payload.
type OrdersResult struct{}
type OrdersResult struct {
Orders []Order
NextCursor string
}
// HistoricalsRequest is the argument set for get_option_historicals.
type HistoricalsRequest struct {
@@ -92,8 +145,19 @@ type HistoricalsRequest struct {
Bounds string
}
// OptionBar is one OHLC bar from get_option_historicals.
type OptionBar struct {
InstrumentID string
Time time.Time
Open, High, Low, Close decimal.Decimal
Volume decimal.Decimal
Interpolated bool
}
// HistoricalsResult is the parsed get_option_historicals payload.
type HistoricalsResult struct{}
type HistoricalsResult struct {
Bars []OptionBar
}
// Chains calls get_option_chains.
func (c *Client) Chains(ctx context.Context, req ChainsRequest) (ChainsResult, error) {
+275
View File
@@ -0,0 +1,275 @@
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
}
+33 -9
View File
@@ -57,11 +57,18 @@ type ReplaceOrderRequest struct {
}
// ReviewResult is the pre-trade check from review_option_order.
type ReviewResult struct{}
type ReviewResult struct {
Errors []string `json:"errors"`
Warnings []string `json:"warnings"`
Alerts []string `json:"alerts"`
}
// Order is a placed or replaced option order.
// Order is a placed, replaced, or listed option order.
type Order struct {
ID string `json:"id"`
State string `json:"state"`
Qty decimal.Decimal `json:"-"`
Price decimal.Decimal `json:"-"`
}
// CancelOrderRequest is the argument set for cancel_option_order.
@@ -81,7 +88,10 @@ type ExerciseRequest struct {
}
// ExerciseResult is the parsed exercise_option payload.
type ExerciseResult struct{}
type ExerciseResult struct {
ID string `json:"id"`
State string `json:"state"`
}
// CancelExerciseRequest is the argument set for cancel_option_exercise.
type CancelExerciseRequest struct {
@@ -100,11 +110,18 @@ func (c *Client) ReviewOrder(ctx context.Context, req PlaceOrderRequest) (Review
// PlaceOrder calls place_option_order. RefID is sent as ref_id.
func (c *Client) PlaceOrder(ctx context.Context, req PlaceOrderRequest) (Order, error) {
var out Order
if err := c.parse(ctx, toolPlace, placeArgs(req, true, false), &out); err != nil {
raw, err := c.c.Call(ctx, toolPlace, placeArgs(req, true, false))
if err != nil {
return Order{}, err
}
return out, nil
ords, err := parseOrders(raw)
if err != nil {
return Order{}, client.ToolErrorf(toolPlace, "parse: %w", err)
}
if len(ords) > 0 {
return ords[0], nil
}
return Order{}, nil
}
// CancelOrder calls cancel_option_order.
@@ -137,11 +154,18 @@ func (c *Client) ReplaceOrder(ctx context.Context, req ReplaceOrderRequest) (Ord
if req.OrderID != "" {
args["order_id"] = req.OrderID
}
var out Order
if err := c.parse(ctx, toolReplace, args, &out); err != nil {
raw, err := c.c.Call(ctx, toolReplace, args)
if err != nil {
return Order{}, err
}
return out, nil
ords, err := parseOrders(raw)
if err != nil {
return Order{}, client.ToolErrorf(toolReplace, "parse: %w", err)
}
if len(ords) > 0 {
return ords[0], nil
}
return Order{}, nil
}
// Exercise calls exercise_option.
+111
View File
@@ -0,0 +1,111 @@
package scanner
import (
"encoding/json"
"s1d3sw1ped/robinhood-agentic-mcp/internal/wire"
)
func (r *FilterSpecsResult) UnmarshalJSON(b []byte) error {
type row struct {
FilterType string `json:"filter_type"`
Name string `json:"name"`
}
rows, _, err := wire.UnmarshalRows[row](b, "specs", "filters", "results")
if err != nil {
return err
}
r.Specs = make([]FilterSpec, 0, len(rows))
for _, row := range rows {
r.Specs = append(r.Specs, FilterSpec{FilterType: row.FilterType, Name: row.Name})
}
return nil
}
func (r *DatapointsResult) UnmarshalJSON(b []byte) error {
type row struct {
Name string `json:"name"`
}
rows, _, err := wire.UnmarshalRows[row](b, "datapoints", "results")
if err != nil {
return err
}
r.Datapoints = make([]Datapoint, 0, len(rows))
for _, row := range rows {
r.Datapoints = append(r.Datapoints, Datapoint{Name: row.Name})
}
return nil
}
func unmarshalScan(b []byte) (ScanResult, error) {
var wrap struct {
ID string `json:"id"`
ScanID string `json:"scan_id"`
Title string `json:"title"`
Total int `json:"total"`
Rows []struct {
Symbol string `json:"symbol"`
Ticker string `json:"ticker"`
InstrumentID string `json:"instrument_id"`
} `json:"rows"`
Results []struct {
Symbol string `json:"symbol"`
InstrumentID string `json:"instrument_id"`
} `json:"results"`
}
if err := json.Unmarshal(wire.Unwrap(b), &wrap); err != nil {
return ScanResult{}, err
}
out := ScanResult{ID: wrap.ID, Title: wrap.Title, Total: wrap.Total}
if out.ID == "" {
out.ID = wrap.ScanID
}
src := wrap.Rows
if len(src) == 0 {
for _, row := range wrap.Results {
src = append(src, struct {
Symbol string `json:"symbol"`
Ticker string `json:"ticker"`
InstrumentID string `json:"instrument_id"`
}{Symbol: row.Symbol, InstrumentID: row.InstrumentID})
}
}
for _, row := range src {
sym := row.Symbol
if sym == "" {
sym = row.Ticker
}
out.Rows = append(out.Rows, ScanRow{Symbol: sym, InstrumentID: row.InstrumentID})
}
return out, nil
}
func (r *CreateResult) UnmarshalJSON(b []byte) error {
s, err := unmarshalScan(b)
r.ScanResult = s
return err
}
func (r *PreviewResult) UnmarshalJSON(b []byte) error {
s, err := unmarshalScan(b)
r.ScanResult = s
return err
}
func (r *RunResult) UnmarshalJSON(b []byte) error {
s, err := unmarshalScan(b)
r.ScanResult = s
return err
}
func (r *UpdateFiltersResult) UnmarshalJSON(b []byte) error {
s, err := unmarshalScan(b)
r.ScanResult = s
return err
}
func (r *UpdateConfigResult) UnmarshalJSON(b []byte) error {
s, err := unmarshalScan(b)
r.ScanResult = s
return err
}
+46 -7
View File
@@ -42,14 +42,29 @@ type Column struct {
// FilterSpecsRequest is the argument set for get_scanner_filter_specs (none).
type FilterSpecsRequest struct{}
// FilterSpec is one filter type from get_scanner_filter_specs.
type FilterSpec struct {
FilterType string `json:"filter_type"`
Name string `json:"name"`
}
// FilterSpecsResult is the parsed get_scanner_filter_specs payload.
type FilterSpecsResult struct{}
type FilterSpecsResult struct {
Specs []FilterSpec
}
// DatapointsRequest is the argument set for get_scanner_datapoints (none).
type DatapointsRequest struct{}
// Datapoint is one expression token from get_scanner_datapoints.
type Datapoint struct {
Name string `json:"name"`
}
// DatapointsResult is the parsed get_scanner_datapoints payload.
type DatapointsResult struct{}
type DatapointsResult struct {
Datapoints []Datapoint
}
// ScansRequest is the argument set for get_scans (none).
type ScansRequest struct{}
@@ -74,8 +89,24 @@ type CreateRequest struct {
Title string
}
// ScanResult is a saved scan plus optional live rows.
type ScanResult struct {
ID string
Title string
Rows []ScanRow
Total int
}
// ScanRow is one live scanner match.
type ScanRow struct {
Symbol string
InstrumentID string
}
// CreateResult is the parsed create_scan payload.
type CreateResult struct{}
type CreateResult struct {
ScanResult
}
// PreviewRequest is the argument set for preview_scan.
type PreviewRequest struct {
@@ -84,7 +115,9 @@ type PreviewRequest struct {
}
// PreviewResult is the parsed preview_scan payload.
type PreviewResult struct{}
type PreviewResult struct {
ScanResult
}
// RunRequest is the argument set for run_scan.
type RunRequest struct {
@@ -92,7 +125,9 @@ type RunRequest struct {
}
// RunResult is the parsed run_scan payload.
type RunResult struct{}
type RunResult struct {
ScanResult
}
// UpdateFiltersRequest is the argument set for update_scan_filters.
type UpdateFiltersRequest struct {
@@ -101,7 +136,9 @@ type UpdateFiltersRequest struct {
}
// UpdateFiltersResult is the parsed update_scan_filters payload.
type UpdateFiltersResult struct{}
type UpdateFiltersResult struct {
ScanResult
}
// UpdateConfigRequest is the argument set for update_scan_config.
type UpdateConfigRequest struct {
@@ -112,7 +149,9 @@ type UpdateConfigRequest struct {
}
// UpdateConfigResult is the parsed update_scan_config payload.
type UpdateConfigResult struct{}
type UpdateConfigResult struct {
ScanResult
}
// FilterSpecs calls get_scanner_filter_specs.
func (c *Client) FilterSpecs(ctx context.Context, req FilterSpecsRequest) (FilterSpecsResult, error) {
+55
View File
@@ -0,0 +1,55 @@
package watchlists
import (
"encoding/json"
"s1d3sw1ped/robinhood-agentic-mcp/internal/wire"
)
func (r *OptionListResult) UnmarshalJSON(b []byte) error {
type row struct {
OptionID string `json:"option_id"`
ID string `json:"id"`
Symbol string `json:"symbol"`
Title string `json:"title"`
PositionType string `json:"position_type"`
}
rows, _, err := wire.UnmarshalRows[row](b, "items", "results", "options")
if err != nil {
return err
}
r.Items = make([]OptionWatchItem, 0, len(rows))
for _, row := range rows {
id := row.OptionID
if id == "" {
id = row.ID
}
r.Items = append(r.Items, OptionWatchItem{
OptionID: id, Symbol: row.Symbol, Title: row.Title, PositionType: row.PositionType,
})
}
return nil
}
func (r *PopularResult) UnmarshalJSON(b []byte) error {
type row struct {
ID string `json:"id"`
Title string `json:"title"`
Name string `json:"name"`
}
rows, _, err := wire.UnmarshalRows[row](b, "watchlists", "results")
if err != nil {
return err
}
r.Watchlists = make([]Watchlist, 0, len(rows))
for _, row := range rows {
title := row.Title
if title == "" {
title = row.Name
}
r.Watchlists = append(r.Watchlists, Watchlist{ID: row.ID, Title: title})
}
return nil
}
var _ json.Unmarshaler = (*OptionListResult)(nil)
+14 -2
View File
@@ -63,14 +63,26 @@ type ItemsResult struct {
// OptionListRequest is the argument set for get_option_watchlist (none).
type OptionListRequest struct{}
// OptionWatchItem is one contract on the options watchlist.
type OptionWatchItem struct {
OptionID string
Symbol string
Title string
PositionType string
}
// OptionListResult is the parsed get_option_watchlist payload.
type OptionListResult struct{}
type OptionListResult struct {
Items []OptionWatchItem
}
// PopularRequest is the argument set for get_popular_watchlists (none).
type PopularRequest struct{}
// PopularResult is the parsed get_popular_watchlists payload.
type PopularResult struct{}
type PopularResult struct {
Watchlists []Watchlist
}
// CreateRequest is the argument set for create_watchlist.
type CreateRequest struct {