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
|
||||
}
|
||||
Reference in New Issue
Block a user