338 lines
9.5 KiB
Go
338 lines
9.5 KiB
Go
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": json.Number("2"),
|
|
"fast_period": 12,
|
|
"slow_period": 26,
|
|
"signal_period": 9,
|
|
"multiplier": json.Number("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{
|
|
"cancel_equity_order",
|
|
"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",
|
|
"place_equity_order",
|
|
"review_equity_order",
|
|
}
|
|
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 TestQuotes_rhntestResultsEnvelope(t *testing.T) {
|
|
t.Parallel()
|
|
s := rhntest.New(t)
|
|
s.Set("get_equity_quotes", json.RawMessage(`{"data":{"results":[{"quote":{"symbol":"SPY","last_trade_price":"763.470000","bid_price":"763.760000","ask_price":"764.100000"},"close":{"symbol":"SPY","price":"765.72"}}]}}`))
|
|
c := equity.New(&client.Client{URL: s.URL})
|
|
got, err := c.Quotes(context.Background(), equity.QuotesRequest{Symbols: []string{"SPY"}})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if len(got.Quotes) != 1 {
|
|
t.Fatalf("%+v", got)
|
|
}
|
|
q := got.Quotes[0]
|
|
if q.Symbol != "SPY" {
|
|
t.Fatalf("%+v", q)
|
|
}
|
|
if !q.Last.Equal(decimal.RequireFromString("763.470000")) {
|
|
t.Fatalf("last %s", q.Last)
|
|
}
|
|
if !q.Bid.Equal(decimal.RequireFromString("763.760000")) {
|
|
t.Fatalf("bid %s", q.Bid)
|
|
}
|
|
if !q.Ask.Equal(decimal.RequireFromString("764.100000")) {
|
|
t.Fatalf("ask %s", q.Ask)
|
|
}
|
|
if !q.PrevClose.Equal(decimal.RequireFromString("765.72")) {
|
|
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])
|
|
}
|
|
}
|