feat: add equity read MCP methods
This commit is contained in:
@@ -0,0 +1,29 @@
|
||||
package equity
|
||||
|
||||
import "s1d3sw1ped/robinhood-agentic-mcp/client"
|
||||
|
||||
// Client wraps Robinhood equity MCP tools.
|
||||
type Client struct {
|
||||
c client.Caller
|
||||
}
|
||||
|
||||
// New returns an equity client that invokes tools through c.
|
||||
func New(c client.Caller) *Client {
|
||||
return &Client{c: c}
|
||||
}
|
||||
|
||||
// Tools returns the MCP names this package implements.
|
||||
func Tools() []string {
|
||||
return []string{
|
||||
toolPositions,
|
||||
toolTaxLots,
|
||||
toolQuotes,
|
||||
toolOrders,
|
||||
toolTradability,
|
||||
toolHistoricals,
|
||||
toolFundamentals,
|
||||
toolPriceBook,
|
||||
toolTechnicalIndicators,
|
||||
toolNews,
|
||||
}
|
||||
}
|
||||
+571
@@ -0,0 +1,571 @@
|
||||
package equity
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"time"
|
||||
|
||||
decimal "github.com/alpacahq/alpacadecimal"
|
||||
"s1d3sw1ped/robinhood-agentic-mcp/client"
|
||||
"s1d3sw1ped/robinhood-agentic-mcp/internal/wire"
|
||||
)
|
||||
|
||||
const (
|
||||
toolPositions = "get_equity_positions"
|
||||
toolTaxLots = "get_equity_tax_lots"
|
||||
toolQuotes = "get_equity_quotes"
|
||||
toolOrders = "get_equity_orders"
|
||||
toolTradability = "get_equity_tradability"
|
||||
toolHistoricals = "get_equity_historicals"
|
||||
toolFundamentals = "get_equity_fundamentals"
|
||||
toolPriceBook = "get_equity_price_book"
|
||||
toolTechnicalIndicators = "get_equity_technical_indicators"
|
||||
toolNews = "get_equity_news"
|
||||
)
|
||||
|
||||
// PositionsRequest is the argument set for get_equity_positions.
|
||||
type PositionsRequest struct {
|
||||
AccountNumber string
|
||||
Cursor string
|
||||
}
|
||||
|
||||
// PositionsResult is the parsed get_equity_positions payload.
|
||||
type PositionsResult struct{}
|
||||
|
||||
// TaxLotsRequest is the argument set for get_equity_tax_lots.
|
||||
type TaxLotsRequest struct {
|
||||
AccountNumber string
|
||||
Symbol string
|
||||
Cursor string
|
||||
}
|
||||
|
||||
// TaxLotsResult is the parsed get_equity_tax_lots payload.
|
||||
type TaxLotsResult struct{}
|
||||
|
||||
// QuotesRequest is the argument set for get_equity_quotes.
|
||||
type QuotesRequest struct {
|
||||
Symbols []string
|
||||
}
|
||||
|
||||
// Quote is a top-of-book print from get_equity_quotes.
|
||||
type Quote struct {
|
||||
Symbol string
|
||||
Bid decimal.Decimal
|
||||
Ask decimal.Decimal
|
||||
Last decimal.Decimal
|
||||
PrevClose decimal.Decimal
|
||||
Volume decimal.Decimal
|
||||
}
|
||||
|
||||
// QuotesResult is the parsed get_equity_quotes payload.
|
||||
type QuotesResult struct {
|
||||
Quotes []Quote
|
||||
}
|
||||
|
||||
// OrdersRequest is the argument set for get_equity_orders.
|
||||
type OrdersRequest struct {
|
||||
AccountNumber string
|
||||
OrderID string
|
||||
State string
|
||||
Symbol string
|
||||
CreatedAtGTE string
|
||||
PlacedAgent string
|
||||
Cursor string
|
||||
}
|
||||
|
||||
// OrdersResult is the parsed get_equity_orders payload.
|
||||
type OrdersResult struct{}
|
||||
|
||||
// TradabilityRequest is the argument set for get_equity_tradability.
|
||||
type TradabilityRequest struct {
|
||||
AccountNumber string
|
||||
Symbols []string
|
||||
}
|
||||
|
||||
// TradabilityResult is the parsed get_equity_tradability payload.
|
||||
type TradabilityResult struct{}
|
||||
|
||||
// HistoricalsRequest is the argument set for get_equity_historicals.
|
||||
type HistoricalsRequest struct {
|
||||
Symbols []string
|
||||
StartTime time.Time
|
||||
EndTime time.Time
|
||||
Interval string
|
||||
Bounds string
|
||||
AdjustmentType string
|
||||
}
|
||||
|
||||
// Bar is one equity historical candle from get_equity_historicals.
|
||||
type Bar struct {
|
||||
Symbol string
|
||||
Time time.Time
|
||||
Open, High, Low, Close decimal.Decimal
|
||||
Volume decimal.Decimal
|
||||
Interpolated bool
|
||||
}
|
||||
|
||||
// HistoricalsResult is the parsed get_equity_historicals payload.
|
||||
type HistoricalsResult struct {
|
||||
Bars []Bar
|
||||
}
|
||||
|
||||
// FundamentalsRequest is the argument set for get_equity_fundamentals.
|
||||
type FundamentalsRequest struct {
|
||||
Symbols []string
|
||||
Bounds string
|
||||
}
|
||||
|
||||
// FundamentalsResult is the parsed get_equity_fundamentals payload.
|
||||
type FundamentalsResult struct{}
|
||||
|
||||
// PriceBookRequest is the argument set for get_equity_price_book.
|
||||
type PriceBookRequest struct {
|
||||
Symbols []string
|
||||
}
|
||||
|
||||
// PriceBookResult is the parsed get_equity_price_book payload.
|
||||
type PriceBookResult struct{}
|
||||
|
||||
// TechnicalIndicatorsRequest is the argument set for get_equity_technical_indicators.
|
||||
type TechnicalIndicatorsRequest struct {
|
||||
Symbol string
|
||||
Type string
|
||||
Interval string
|
||||
StartTime time.Time
|
||||
EndTime time.Time
|
||||
Bounds string
|
||||
AdjustmentType string
|
||||
Output string
|
||||
Period *int
|
||||
NumStd *decimal.Decimal
|
||||
FastPeriod *int
|
||||
SlowPeriod *int
|
||||
SignalPeriod *int
|
||||
Multiplier *decimal.Decimal
|
||||
Method string
|
||||
}
|
||||
|
||||
// TechnicalIndicatorsResult is the parsed get_equity_technical_indicators payload.
|
||||
type TechnicalIndicatorsResult struct{}
|
||||
|
||||
// NewsRequest is the argument set for get_equity_news.
|
||||
type NewsRequest struct {
|
||||
Symbol string
|
||||
Limit int
|
||||
Cursor string
|
||||
}
|
||||
|
||||
// NewsResult is the parsed get_equity_news payload.
|
||||
type NewsResult struct{}
|
||||
|
||||
// Positions calls get_equity_positions.
|
||||
func (c *Client) Positions(ctx context.Context, req PositionsRequest) (PositionsResult, error) {
|
||||
args := map[string]any{}
|
||||
if req.AccountNumber != "" {
|
||||
args["account_number"] = req.AccountNumber
|
||||
}
|
||||
if req.Cursor != "" {
|
||||
args["cursor"] = req.Cursor
|
||||
}
|
||||
var out PositionsResult
|
||||
if err := c.parse(ctx, toolPositions, args, &out); err != nil {
|
||||
return PositionsResult{}, err
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
// TaxLots calls get_equity_tax_lots.
|
||||
func (c *Client) TaxLots(ctx context.Context, req TaxLotsRequest) (TaxLotsResult, error) {
|
||||
args := map[string]any{}
|
||||
if req.AccountNumber != "" {
|
||||
args["account_number"] = req.AccountNumber
|
||||
}
|
||||
if req.Symbol != "" {
|
||||
args["symbol"] = req.Symbol
|
||||
}
|
||||
if req.Cursor != "" {
|
||||
args["cursor"] = req.Cursor
|
||||
}
|
||||
var out TaxLotsResult
|
||||
if err := c.parse(ctx, toolTaxLots, args, &out); err != nil {
|
||||
return TaxLotsResult{}, err
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
// Quotes calls get_equity_quotes.
|
||||
func (c *Client) Quotes(ctx context.Context, req QuotesRequest) (QuotesResult, error) {
|
||||
args := map[string]any{}
|
||||
if len(req.Symbols) > 0 {
|
||||
args["symbols"] = req.Symbols
|
||||
}
|
||||
raw, err := c.c.Call(ctx, toolQuotes, args)
|
||||
if err != nil {
|
||||
return QuotesResult{}, err
|
||||
}
|
||||
quotes, err := parseQuotes(raw)
|
||||
if err != nil {
|
||||
return QuotesResult{}, client.ToolErrorf(toolQuotes, "parse: %w", err)
|
||||
}
|
||||
return QuotesResult{Quotes: quotes}, nil
|
||||
}
|
||||
|
||||
// Orders calls get_equity_orders.
|
||||
func (c *Client) Orders(ctx context.Context, req OrdersRequest) (OrdersResult, error) {
|
||||
args := map[string]any{}
|
||||
if req.AccountNumber != "" {
|
||||
args["account_number"] = req.AccountNumber
|
||||
}
|
||||
if req.OrderID != "" {
|
||||
args["order_id"] = req.OrderID
|
||||
}
|
||||
if req.State != "" {
|
||||
args["state"] = req.State
|
||||
}
|
||||
if req.Symbol != "" {
|
||||
args["symbol"] = req.Symbol
|
||||
}
|
||||
if req.CreatedAtGTE != "" {
|
||||
args["created_at_gte"] = req.CreatedAtGTE
|
||||
}
|
||||
if req.PlacedAgent != "" {
|
||||
args["placed_agent"] = req.PlacedAgent
|
||||
}
|
||||
if req.Cursor != "" {
|
||||
args["cursor"] = req.Cursor
|
||||
}
|
||||
var out OrdersResult
|
||||
if err := c.parse(ctx, toolOrders, args, &out); err != nil {
|
||||
return OrdersResult{}, err
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
// Tradability calls get_equity_tradability.
|
||||
func (c *Client) Tradability(ctx context.Context, req TradabilityRequest) (TradabilityResult, error) {
|
||||
args := map[string]any{}
|
||||
if req.AccountNumber != "" {
|
||||
args["account_number"] = req.AccountNumber
|
||||
}
|
||||
if len(req.Symbols) > 0 {
|
||||
args["symbols"] = req.Symbols
|
||||
}
|
||||
var out TradabilityResult
|
||||
if err := c.parse(ctx, toolTradability, args, &out); err != nil {
|
||||
return TradabilityResult{}, err
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
// Historicals calls get_equity_historicals.
|
||||
func (c *Client) Historicals(ctx context.Context, req HistoricalsRequest) (HistoricalsResult, error) {
|
||||
args := map[string]any{
|
||||
"start_time": req.StartTime.UTC().Format(time.RFC3339),
|
||||
}
|
||||
if len(req.Symbols) > 0 {
|
||||
args["symbols"] = req.Symbols
|
||||
}
|
||||
if !req.EndTime.IsZero() {
|
||||
args["end_time"] = req.EndTime.UTC().Format(time.RFC3339)
|
||||
}
|
||||
if req.Interval != "" {
|
||||
args["interval"] = req.Interval
|
||||
}
|
||||
if req.Bounds != "" {
|
||||
args["bounds"] = req.Bounds
|
||||
}
|
||||
if req.AdjustmentType != "" {
|
||||
args["adjustment_type"] = req.AdjustmentType
|
||||
}
|
||||
raw, err := c.c.Call(ctx, toolHistoricals, args)
|
||||
if err != nil {
|
||||
return HistoricalsResult{}, err
|
||||
}
|
||||
bars, err := parseHistoricals(raw)
|
||||
if err != nil {
|
||||
return HistoricalsResult{}, client.ToolErrorf(toolHistoricals, "parse: %w", err)
|
||||
}
|
||||
return HistoricalsResult{Bars: bars}, nil
|
||||
}
|
||||
|
||||
// Fundamentals calls get_equity_fundamentals.
|
||||
func (c *Client) Fundamentals(ctx context.Context, req FundamentalsRequest) (FundamentalsResult, error) {
|
||||
args := map[string]any{}
|
||||
if len(req.Symbols) > 0 {
|
||||
args["symbols"] = req.Symbols
|
||||
}
|
||||
if req.Bounds != "" {
|
||||
args["bounds"] = req.Bounds
|
||||
}
|
||||
var out FundamentalsResult
|
||||
if err := c.parse(ctx, toolFundamentals, args, &out); err != nil {
|
||||
return FundamentalsResult{}, err
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
// PriceBook calls get_equity_price_book.
|
||||
func (c *Client) PriceBook(ctx context.Context, req PriceBookRequest) (PriceBookResult, error) {
|
||||
args := map[string]any{}
|
||||
if len(req.Symbols) > 0 {
|
||||
args["symbols"] = req.Symbols
|
||||
}
|
||||
var out PriceBookResult
|
||||
if err := c.parse(ctx, toolPriceBook, args, &out); err != nil {
|
||||
return PriceBookResult{}, err
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
// TechnicalIndicators calls get_equity_technical_indicators.
|
||||
func (c *Client) TechnicalIndicators(ctx context.Context, req TechnicalIndicatorsRequest) (TechnicalIndicatorsResult, error) {
|
||||
args := map[string]any{
|
||||
"start_time": req.StartTime.UTC().Format(time.RFC3339),
|
||||
}
|
||||
if req.Symbol != "" {
|
||||
args["symbol"] = req.Symbol
|
||||
}
|
||||
if req.Type != "" {
|
||||
args["type"] = req.Type
|
||||
}
|
||||
if req.Interval != "" {
|
||||
args["interval"] = req.Interval
|
||||
}
|
||||
if !req.EndTime.IsZero() {
|
||||
args["end_time"] = req.EndTime.UTC().Format(time.RFC3339)
|
||||
}
|
||||
if req.Bounds != "" {
|
||||
args["bounds"] = req.Bounds
|
||||
}
|
||||
if req.AdjustmentType != "" {
|
||||
args["adjustment_type"] = req.AdjustmentType
|
||||
}
|
||||
if req.Output != "" {
|
||||
args["output"] = req.Output
|
||||
}
|
||||
if req.Period != nil {
|
||||
args["period"] = *req.Period
|
||||
}
|
||||
if req.NumStd != nil {
|
||||
args["num_std"] = wire.Encode(*req.NumStd)
|
||||
}
|
||||
if req.FastPeriod != nil {
|
||||
args["fast_period"] = *req.FastPeriod
|
||||
}
|
||||
if req.SlowPeriod != nil {
|
||||
args["slow_period"] = *req.SlowPeriod
|
||||
}
|
||||
if req.SignalPeriod != nil {
|
||||
args["signal_period"] = *req.SignalPeriod
|
||||
}
|
||||
if req.Multiplier != nil {
|
||||
args["multiplier"] = wire.Encode(*req.Multiplier)
|
||||
}
|
||||
if req.Method != "" {
|
||||
args["method"] = req.Method
|
||||
}
|
||||
var out TechnicalIndicatorsResult
|
||||
if err := c.parse(ctx, toolTechnicalIndicators, args, &out); err != nil {
|
||||
return TechnicalIndicatorsResult{}, err
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
// News calls get_equity_news.
|
||||
func (c *Client) News(ctx context.Context, req NewsRequest) (NewsResult, error) {
|
||||
args := map[string]any{}
|
||||
if req.Symbol != "" {
|
||||
args["symbol"] = req.Symbol
|
||||
}
|
||||
if req.Limit != 0 {
|
||||
args["limit"] = req.Limit
|
||||
}
|
||||
if req.Cursor != "" {
|
||||
args["cursor"] = req.Cursor
|
||||
}
|
||||
var out NewsResult
|
||||
if err := c.parse(ctx, toolNews, args, &out); err != nil {
|
||||
return NewsResult{}, err
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func (c *Client) parse(ctx context.Context, tool string, args map[string]any, dest any) error {
|
||||
raw, err := c.c.Call(ctx, tool, args)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err := json.Unmarshal(wire.Unwrap(raw), dest); err != nil {
|
||||
return client.ToolErrorf(tool, "parse: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
type quoteFields struct {
|
||||
Symbol string `json:"symbol"`
|
||||
Last any `json:"last"`
|
||||
LastTradePrice any `json:"last_trade_price"`
|
||||
LastNonRegTradePrice any `json:"last_non_reg_trade_price"`
|
||||
Bid any `json:"bid"`
|
||||
BidPrice any `json:"bid_price"`
|
||||
Ask any `json:"ask"`
|
||||
AskPrice any `json:"ask_price"`
|
||||
PreviousClose any `json:"previous_close"`
|
||||
AdjustedPreviousClose any `json:"adjusted_previous_close"`
|
||||
Volume any `json:"volume"`
|
||||
}
|
||||
|
||||
type quoteRow struct {
|
||||
Quote *quoteFields `json:"quote"`
|
||||
Close *struct {
|
||||
Symbol string `json:"symbol"`
|
||||
Price any `json:"price"`
|
||||
} `json:"close"`
|
||||
quoteFields
|
||||
}
|
||||
|
||||
func (r quoteRow) asQuote() (Quote, bool, error) {
|
||||
f := r.quoteFields
|
||||
if r.Quote != nil {
|
||||
f = *r.Quote
|
||||
}
|
||||
if f.Symbol == "" && r.Close != nil {
|
||||
f.Symbol = r.Close.Symbol
|
||||
}
|
||||
if f.Symbol == "" {
|
||||
return Quote{}, false, nil
|
||||
}
|
||||
var closePx any
|
||||
if r.Close != nil {
|
||||
closePx = r.Close.Price
|
||||
}
|
||||
last, err := firstDec(f.LastTradePrice, f.Last, f.LastNonRegTradePrice)
|
||||
if err != nil {
|
||||
return Quote{}, false, err
|
||||
}
|
||||
prev, err := firstDec(closePx, f.PreviousClose, f.AdjustedPreviousClose)
|
||||
if err != nil {
|
||||
return Quote{}, false, err
|
||||
}
|
||||
bid, err := firstDec(f.BidPrice, f.Bid)
|
||||
if err != nil {
|
||||
return Quote{}, false, err
|
||||
}
|
||||
ask, err := firstDec(f.AskPrice, f.Ask)
|
||||
if err != nil {
|
||||
return Quote{}, false, err
|
||||
}
|
||||
vol, err := firstDec(f.Volume)
|
||||
if err != nil {
|
||||
return Quote{}, false, err
|
||||
}
|
||||
return Quote{
|
||||
Symbol: f.Symbol,
|
||||
Last: last,
|
||||
PrevClose: prev,
|
||||
Bid: bid,
|
||||
Ask: ask,
|
||||
Volume: vol,
|
||||
}, true, nil
|
||||
}
|
||||
|
||||
func parseQuotes(raw json.RawMessage) ([]Quote, error) {
|
||||
var wrap struct {
|
||||
Quotes []quoteRow `json:"quotes"`
|
||||
}
|
||||
if err := json.Unmarshal(wire.Unwrap(raw), &wrap); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
out := make([]Quote, 0, len(wrap.Quotes))
|
||||
for _, row := range wrap.Quotes {
|
||||
q, ok, err := row.asQuote()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
out = append(out, q)
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
type histPointJSON 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 histSeriesJSON struct {
|
||||
Symbol string `json:"symbol"`
|
||||
DataPoints []histPointJSON `json:"data_points"`
|
||||
}
|
||||
|
||||
func parseHistoricals(raw json.RawMessage) ([]Bar, error) {
|
||||
var wrap struct {
|
||||
Historicals []histSeriesJSON `json:"historicals"`
|
||||
}
|
||||
if err := json.Unmarshal(wire.Unwrap(raw), &wrap); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var out []Bar
|
||||
for _, series := range wrap.Historicals {
|
||||
if series.Symbol == "" {
|
||||
continue
|
||||
}
|
||||
for _, p := range series.DataPoints {
|
||||
ts, err := time.Parse(time.RFC3339, p.BeginsAt)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
o, err := firstDec(p.Open)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
h, err := firstDec(p.High)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
l, err := firstDec(p.Low)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
cl, err := firstDec(p.Close)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
vol, err := firstDec(p.Volume)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
out = append(out, Bar{
|
||||
Symbol: series.Symbol,
|
||||
Time: ts,
|
||||
Open: o,
|
||||
High: h,
|
||||
Low: l,
|
||||
Close: cl,
|
||||
Volume: vol,
|
||||
Interpolated: p.Interpolated,
|
||||
})
|
||||
}
|
||||
}
|
||||
return out, 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
|
||||
}
|
||||
@@ -0,0 +1,304 @@
|
||||
package equity_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"sort"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
decimal "github.com/alpacahq/alpacadecimal"
|
||||
"github.com/google/go-cmp/cmp"
|
||||
"s1d3sw1ped/robinhood-agentic-mcp/client"
|
||||
"s1d3sw1ped/robinhood-agentic-mcp/equity"
|
||||
"s1d3sw1ped/robinhood-agentic-mcp/internal/rhntest"
|
||||
)
|
||||
|
||||
func TestEquity_toolNames(t *testing.T) {
|
||||
t.Parallel()
|
||||
start := time.Date(2026, 8, 18, 13, 30, 0, 0, time.UTC)
|
||||
end := time.Date(2026, 8, 18, 20, 0, 0, 0, time.UTC)
|
||||
period := 14
|
||||
fast := 12
|
||||
slow := 26
|
||||
signal := 9
|
||||
numStd := decimal.RequireFromString("2")
|
||||
mult := decimal.RequireFromString("3")
|
||||
tests := []struct {
|
||||
name string
|
||||
call func(*equity.Client) error
|
||||
wantName string
|
||||
wantArgs map[string]any
|
||||
}{
|
||||
{
|
||||
name: "Positions",
|
||||
call: func(c *equity.Client) error {
|
||||
_, err := c.Positions(context.Background(), equity.PositionsRequest{
|
||||
AccountNumber: "acct-1",
|
||||
Cursor: "c1",
|
||||
})
|
||||
return err
|
||||
},
|
||||
wantName: "get_equity_positions",
|
||||
wantArgs: map[string]any{"account_number": "acct-1", "cursor": "c1"},
|
||||
},
|
||||
{
|
||||
name: "TaxLots",
|
||||
call: func(c *equity.Client) error {
|
||||
_, err := c.TaxLots(context.Background(), equity.TaxLotsRequest{
|
||||
AccountNumber: "acct-1",
|
||||
Symbol: "MU",
|
||||
Cursor: "c1",
|
||||
})
|
||||
return err
|
||||
},
|
||||
wantName: "get_equity_tax_lots",
|
||||
wantArgs: map[string]any{"account_number": "acct-1", "symbol": "MU", "cursor": "c1"},
|
||||
},
|
||||
{
|
||||
name: "Quotes",
|
||||
call: func(c *equity.Client) error {
|
||||
_, err := c.Quotes(context.Background(), equity.QuotesRequest{Symbols: []string{"MU"}})
|
||||
return err
|
||||
},
|
||||
wantName: "get_equity_quotes",
|
||||
wantArgs: map[string]any{"symbols": []string{"MU"}},
|
||||
},
|
||||
{
|
||||
name: "Orders",
|
||||
call: func(c *equity.Client) error {
|
||||
_, err := c.Orders(context.Background(), equity.OrdersRequest{
|
||||
AccountNumber: "acct-1",
|
||||
OrderID: "o1",
|
||||
State: "filled",
|
||||
Symbol: "MU",
|
||||
CreatedAtGTE: "2026-08-18",
|
||||
PlacedAgent: "agentic",
|
||||
Cursor: "c1",
|
||||
})
|
||||
return err
|
||||
},
|
||||
wantName: "get_equity_orders",
|
||||
wantArgs: map[string]any{
|
||||
"account_number": "acct-1",
|
||||
"order_id": "o1",
|
||||
"state": "filled",
|
||||
"symbol": "MU",
|
||||
"created_at_gte": "2026-08-18",
|
||||
"placed_agent": "agentic",
|
||||
"cursor": "c1",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "Tradability",
|
||||
call: func(c *equity.Client) error {
|
||||
_, err := c.Tradability(context.Background(), equity.TradabilityRequest{
|
||||
AccountNumber: "acct-1",
|
||||
Symbols: []string{"MU"},
|
||||
})
|
||||
return err
|
||||
},
|
||||
wantName: "get_equity_tradability",
|
||||
wantArgs: map[string]any{"account_number": "acct-1", "symbols": []string{"MU"}},
|
||||
},
|
||||
{
|
||||
name: "Historicals",
|
||||
call: func(c *equity.Client) error {
|
||||
_, err := c.Historicals(context.Background(), equity.HistoricalsRequest{
|
||||
Symbols: []string{"MU"},
|
||||
StartTime: start,
|
||||
})
|
||||
return err
|
||||
},
|
||||
wantName: "get_equity_historicals",
|
||||
wantArgs: map[string]any{
|
||||
"symbols": []string{"MU"},
|
||||
"start_time": "2026-08-18T13:30:00Z",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "Fundamentals",
|
||||
call: func(c *equity.Client) error {
|
||||
_, err := c.Fundamentals(context.Background(), equity.FundamentalsRequest{
|
||||
Symbols: []string{"MU"},
|
||||
Bounds: "regular",
|
||||
})
|
||||
return err
|
||||
},
|
||||
wantName: "get_equity_fundamentals",
|
||||
wantArgs: map[string]any{"symbols": []string{"MU"}, "bounds": "regular"},
|
||||
},
|
||||
{
|
||||
name: "PriceBook",
|
||||
call: func(c *equity.Client) error {
|
||||
_, err := c.PriceBook(context.Background(), equity.PriceBookRequest{Symbols: []string{"MU"}})
|
||||
return err
|
||||
},
|
||||
wantName: "get_equity_price_book",
|
||||
wantArgs: map[string]any{"symbols": []string{"MU"}},
|
||||
},
|
||||
{
|
||||
name: "TechnicalIndicators",
|
||||
call: func(c *equity.Client) error {
|
||||
_, err := c.TechnicalIndicators(context.Background(), equity.TechnicalIndicatorsRequest{
|
||||
Symbol: "MU",
|
||||
Type: "macd",
|
||||
Interval: "minute",
|
||||
StartTime: start,
|
||||
EndTime: end,
|
||||
Bounds: "regular",
|
||||
AdjustmentType: "split",
|
||||
Output: "latest",
|
||||
Period: &period,
|
||||
NumStd: &numStd,
|
||||
FastPeriod: &fast,
|
||||
SlowPeriod: &slow,
|
||||
SignalPeriod: &signal,
|
||||
Multiplier: &mult,
|
||||
Method: "classic",
|
||||
})
|
||||
return err
|
||||
},
|
||||
wantName: "get_equity_technical_indicators",
|
||||
wantArgs: map[string]any{
|
||||
"symbol": "MU",
|
||||
"type": "macd",
|
||||
"interval": "minute",
|
||||
"start_time": "2026-08-18T13:30:00Z",
|
||||
"end_time": "2026-08-18T20:00:00Z",
|
||||
"bounds": "regular",
|
||||
"adjustment_type": "split",
|
||||
"output": "latest",
|
||||
"period": 14,
|
||||
"num_std": "2",
|
||||
"fast_period": 12,
|
||||
"slow_period": 26,
|
||||
"signal_period": 9,
|
||||
"multiplier": "3",
|
||||
"method": "classic",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "News",
|
||||
call: func(c *equity.Client) error {
|
||||
_, err := c.News(context.Background(), equity.NewsRequest{
|
||||
Symbol: "MU",
|
||||
Limit: 5,
|
||||
Cursor: "c1",
|
||||
})
|
||||
return err
|
||||
},
|
||||
wantName: "get_equity_news",
|
||||
wantArgs: map[string]any{"symbol": "MU", "limit": 5, "cursor": "c1"},
|
||||
},
|
||||
}
|
||||
for _, tc := range tests {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
var gotName string
|
||||
var gotArgs map[string]any
|
||||
c := equity.New(client.Func(func(ctx context.Context, name string, args map[string]any) (json.RawMessage, error) {
|
||||
gotName, gotArgs = name, args
|
||||
return json.RawMessage(`{}`), nil
|
||||
}))
|
||||
if err := tc.call(c); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if gotName != tc.wantName {
|
||||
t.Fatalf("%s %+v", gotName, gotArgs)
|
||||
}
|
||||
if diff := cmp.Diff(tc.wantArgs, gotArgs); diff != "" {
|
||||
t.Fatal(diff)
|
||||
}
|
||||
if tc.name == "Historicals" {
|
||||
if _, ok := gotArgs["interval"]; ok {
|
||||
t.Fatalf("interval injected: %+v", gotArgs)
|
||||
}
|
||||
if _, ok := gotArgs["bounds"]; ok {
|
||||
t.Fatalf("bounds injected: %+v", gotArgs)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestTools(t *testing.T) {
|
||||
t.Parallel()
|
||||
want := []string{
|
||||
"get_equity_fundamentals",
|
||||
"get_equity_historicals",
|
||||
"get_equity_news",
|
||||
"get_equity_orders",
|
||||
"get_equity_positions",
|
||||
"get_equity_price_book",
|
||||
"get_equity_quotes",
|
||||
"get_equity_tax_lots",
|
||||
"get_equity_technical_indicators",
|
||||
"get_equity_tradability",
|
||||
}
|
||||
got := append([]string(nil), equity.Tools()...)
|
||||
sort.Strings(got)
|
||||
if diff := cmp.Diff(want, got); diff != "" {
|
||||
t.Fatal(diff)
|
||||
}
|
||||
}
|
||||
|
||||
func TestQuotes_rhntest(t *testing.T) {
|
||||
t.Parallel()
|
||||
s := rhntest.New(t)
|
||||
s.Set("get_equity_quotes", json.RawMessage(`{"quotes":[{"symbol":"MU","quote":{"symbol":"MU","last_trade_price":"100","bid_price":"99.9","ask_price":"100.1"},"close":{"symbol":"MU","price":"98"}}]}`))
|
||||
c := equity.New(&client.Client{URL: s.URL})
|
||||
got, err := c.Quotes(context.Background(), equity.QuotesRequest{Symbols: []string{"MU"}})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if len(got.Quotes) != 1 {
|
||||
t.Fatalf("%+v", got)
|
||||
}
|
||||
q := got.Quotes[0]
|
||||
if q.Symbol != "MU" {
|
||||
t.Fatalf("%+v", q)
|
||||
}
|
||||
if !q.Last.Equal(decimal.RequireFromString("100")) {
|
||||
t.Fatalf("last %s", q.Last)
|
||||
}
|
||||
if !q.Bid.Equal(decimal.RequireFromString("99.9")) {
|
||||
t.Fatalf("bid %s", q.Bid)
|
||||
}
|
||||
if !q.PrevClose.Equal(decimal.RequireFromString("98")) {
|
||||
t.Fatalf("prev close %s", q.PrevClose)
|
||||
}
|
||||
}
|
||||
|
||||
func TestHistoricals_rhntest(t *testing.T) {
|
||||
t.Parallel()
|
||||
s := rhntest.New(t)
|
||||
s.Set("get_equity_historicals", json.RawMessage(`{"historicals":[{"symbol":"MU","data_points":[
|
||||
{"begins_at":"2026-08-18T13:30:00Z","open":"10","high":"11","low":"9","close":"10","volume":"100","interpolated":false},
|
||||
{"begins_at":"2026-08-18T13:31:00Z","open":"10","high":"10","low":"10","close":"10","volume":"1","interpolated":true}
|
||||
]}]}`))
|
||||
c := equity.New(&client.Client{URL: s.URL})
|
||||
got, err := c.Historicals(context.Background(), equity.HistoricalsRequest{
|
||||
Symbols: []string{"MU"},
|
||||
StartTime: time.Date(2026, 8, 18, 13, 30, 0, 0, time.UTC),
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if len(got.Bars) != 2 {
|
||||
t.Fatalf("bars %d", len(got.Bars))
|
||||
}
|
||||
b0 := got.Bars[0]
|
||||
if b0.Symbol != "MU" || b0.Interpolated {
|
||||
t.Fatalf("%+v", b0)
|
||||
}
|
||||
if !b0.Time.Equal(time.Date(2026, 8, 18, 13, 30, 0, 0, time.UTC)) {
|
||||
t.Fatalf("time %s", b0.Time)
|
||||
}
|
||||
if !b0.Open.Equal(decimal.RequireFromString("10")) || !b0.High.Equal(decimal.RequireFromString("11")) || !b0.Low.Equal(decimal.RequireFromString("9")) || !b0.Close.Equal(decimal.RequireFromString("10")) || !b0.Volume.Equal(decimal.RequireFromString("100")) {
|
||||
t.Fatalf("%+v", b0)
|
||||
}
|
||||
if !got.Bars[1].Interpolated {
|
||||
t.Fatalf("%+v", got.Bars[1])
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user