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) // Live Agentic shape (Jerry CT113): data.results[].bars[] with *_price fields. s.Set("get_equity_historicals", json.RawMessage(`{"data":{"results":[{"symbol":"MU","bars":[ {"begins_at":"2026-08-18T13:30:00Z","open_price":"10","high_price":"11","low_price":"9","close_price":"10","volume":"100","session":"reg","interpolated":false}, {"begins_at":"2026-08-18T13:31:00Z","open_price":"10","high_price":"10","low_price":"10","close_price":"10","volume":"1","session":"reg","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]) } } func TestHistoricals_legacyShortNames(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.Open.Equal(decimal.RequireFromString("10")) || !b0.High.Equal(decimal.RequireFromString("11")) || !b0.Volume.Equal(decimal.RequireFromString("100")) { t.Fatalf("%+v", b0) } }