Files
robinhood-agentic-mcp/equity/read_test.go
T
ash 27ca0a97c4
CI / Test and build (pull_request) Successful in 16s
equity: Accept open_price aliases in historicals
Live Agentic get_equity_historicals returns
data.results[].bars[] with open_price/high_price/low_price/close_price.
parseHistoricals only read historicals[].data_points[] with short OHLC
names, so CT113 saw bars_len=30 on the wire but 0 parsed bars and VWAP
stayed 0 all day (insufficient_data mass skip).

Accept results|historicals envelopes, bars|data_points arrays, and
*_price aliases alongside short names. Fixture uses the live shape;
legacy short-name envelope stays covered.

Link: #18
2026-09-09 10:07:23 +00:00

363 lines
11 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)
// 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)
}
}