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:
+46
-5
@@ -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) {
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
@@ -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.
|
||||
|
||||
@@ -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
@@ -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) {
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
@@ -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
@@ -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)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 ""
|
||||
}
|
||||
@@ -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
@@ -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) {
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
@@ -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) {
|
||||
|
||||
@@ -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
@@ -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.
|
||||
|
||||
@@ -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
@@ -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) {
|
||||
|
||||
@@ -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)
|
||||
@@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user