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:
+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)
|
||||
}
|
||||
}
|
||||
+81
-7
@@ -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"`
|
||||
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
|
||||
}
|
||||
|
||||
+33
-10
@@ -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) {
|
||||
@@ -98,14 +118,13 @@ func TestEquity_writeArgs(t *testing.T) {
|
||||
},
|
||||
wantName: "place_equity_order",
|
||||
wantArgs: map[string]any{
|
||||
"account_number": "acct",
|
||||
"symbol": "MU",
|
||||
"side": "sell",
|
||||
"type": "stop_market",
|
||||
"quantity": "3",
|
||||
"stop_price": "99.6",
|
||||
"ref_id": "sell:2026-08-18:MU",
|
||||
"idempotency_key": "sell:2026-08-18:MU",
|
||||
"account_number": "acct",
|
||||
"symbol": "MU",
|
||||
"side": "sell",
|
||||
"type": "stop_market",
|
||||
"quantity": "3",
|
||||
"stop_price": "99.6",
|
||||
"ref_id": "sell:2026-08-18:MU",
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -135,8 +154,7 @@ func TestEquity_writeArgs(t *testing.T) {
|
||||
"tax_lots": []map[string]any{
|
||||
{"open_lot_id": "lot-1", "quantity": "1.5"},
|
||||
},
|
||||
"ref_id": "sell:lots",
|
||||
"idempotency_key": "sell:lots",
|
||||
"ref_id": "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)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user