283 lines
7.6 KiB
Go
283 lines
7.6 KiB
Go
package crypto_test
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"sort"
|
|
"testing"
|
|
|
|
decimal "github.com/alpacahq/alpacadecimal"
|
|
"github.com/google/go-cmp/cmp"
|
|
"s1d3sw1ped/robinhood-agentic-mcp/client"
|
|
"s1d3sw1ped/robinhood-agentic-mcp/crypto"
|
|
"s1d3sw1ped/robinhood-agentic-mcp/internal/rhntest"
|
|
)
|
|
|
|
func TestCrypto_toolNames(t *testing.T) {
|
|
t.Parallel()
|
|
qty := decimal.RequireFromString("0.001")
|
|
px := decimal.RequireFromString("3000")
|
|
stop := decimal.RequireFromString("50000")
|
|
dollars := decimal.RequireFromString("100")
|
|
tests := []struct {
|
|
name string
|
|
call func(*crypto.Client) error
|
|
wantName string
|
|
wantArgs map[string]any
|
|
}{
|
|
{
|
|
name: "Pairs",
|
|
call: func(c *crypto.Client) error {
|
|
_, err := c.Pairs(context.Background(), crypto.PairsRequest{
|
|
Cursor: "c1",
|
|
Limit: 25,
|
|
})
|
|
return err
|
|
},
|
|
wantName: "get_currency_pairs",
|
|
wantArgs: map[string]any{"cursor": "c1", "limit": 25},
|
|
},
|
|
{
|
|
name: "Quotes",
|
|
call: func(c *crypto.Client) error {
|
|
_, err := c.Quotes(context.Background(), crypto.QuotesRequest{
|
|
Symbols: []string{"BTC-USD", "ETH-USD"},
|
|
Timezone: "America/New_York",
|
|
RHSAccountNumber: "123456789",
|
|
})
|
|
return err
|
|
},
|
|
wantName: "get_crypto_quotes",
|
|
wantArgs: map[string]any{
|
|
"symbols": []string{"BTC-USD", "ETH-USD"},
|
|
"timezone": "America/New_York",
|
|
"rhs_account_number": "123456789",
|
|
},
|
|
},
|
|
{
|
|
name: "Positions",
|
|
call: func(c *crypto.Client) error {
|
|
_, err := c.Positions(context.Background(), crypto.PositionsRequest{
|
|
RHSAccountNumber: "123456789",
|
|
Cursor: "c1",
|
|
})
|
|
return err
|
|
},
|
|
wantName: "get_crypto_positions",
|
|
wantArgs: map[string]any{"rhs_account_number": "123456789", "cursor": "c1"},
|
|
},
|
|
{
|
|
name: "Orders",
|
|
call: func(c *crypto.Client) error {
|
|
_, err := c.Orders(context.Background(), crypto.OrdersRequest{
|
|
RHSAccountNumber: "123456789",
|
|
OrderID: "o1",
|
|
State: "filled",
|
|
StateGroup: "closed",
|
|
Side: client.Buy,
|
|
Symbol: "BTC-USD",
|
|
CreatedAtGTE: "2026-08-18T00:00:00Z",
|
|
UpdatedAtGTE: "2026-08-18T13:30:00Z",
|
|
Cursor: "c1",
|
|
})
|
|
return err
|
|
},
|
|
wantName: "get_crypto_orders",
|
|
wantArgs: map[string]any{
|
|
"rhs_account_number": "123456789",
|
|
"order_id": "o1",
|
|
"state": "filled",
|
|
"state_group": "closed",
|
|
"side": "buy",
|
|
"symbol": "BTC-USD",
|
|
"created_at_gte": "2026-08-18T00:00:00Z",
|
|
"updated_at_gte": "2026-08-18T13:30:00Z",
|
|
"cursor": "c1",
|
|
},
|
|
},
|
|
{
|
|
name: "PreviewOrder",
|
|
call: func(c *crypto.Client) error {
|
|
_, err := c.PreviewOrder(context.Background(), crypto.PlaceOrderRequest{
|
|
RHSAccountNumber: "123456789",
|
|
Symbol: "ETH",
|
|
Side: client.Buy,
|
|
Type: client.Limit,
|
|
DollarAmount: &dollars,
|
|
LimitPrice: &px,
|
|
TimeInForce: client.GTC,
|
|
RefID: "buy:eth",
|
|
})
|
|
return err
|
|
},
|
|
wantName: "preview_crypto_order",
|
|
wantArgs: map[string]any{
|
|
"rhs_account_number": "123456789",
|
|
"symbol": "ETH",
|
|
"side": "buy",
|
|
"type": "limit",
|
|
"dollar_amount": "100",
|
|
"limit_price": "3000",
|
|
"time_in_force": "gtc",
|
|
},
|
|
},
|
|
{
|
|
name: "PlaceOrder",
|
|
call: func(c *crypto.Client) error {
|
|
_, err := c.PlaceOrder(context.Background(), crypto.PlaceOrderRequest{
|
|
RHSAccountNumber: "123456789",
|
|
Symbol: "BTC-USD",
|
|
Side: client.Sell,
|
|
Type: client.StopLoss,
|
|
Qty: &qty,
|
|
StopPrice: &stop,
|
|
TimeInForce: client.GFW,
|
|
RefID: "sell:btc",
|
|
})
|
|
return err
|
|
},
|
|
wantName: "place_crypto_order",
|
|
wantArgs: map[string]any{
|
|
"rhs_account_number": "123456789",
|
|
"symbol": "BTC-USD",
|
|
"side": "sell",
|
|
"type": "stop_loss",
|
|
"quantity": "0.001",
|
|
"stop_price": "50000",
|
|
"time_in_force": "gfw",
|
|
"ref_id": "sell:btc",
|
|
},
|
|
},
|
|
{
|
|
name: "CancelOrder",
|
|
call: func(c *crypto.Client) error {
|
|
return c.CancelOrder(context.Background(), crypto.CancelOrderRequest{
|
|
RHSAccountNumber: "123456789",
|
|
OrderID: "o1",
|
|
})
|
|
},
|
|
wantName: "cancel_crypto_order",
|
|
wantArgs: map[string]any{"rhs_account_number": "123456789", "order_id": "o1"},
|
|
},
|
|
}
|
|
for _, tc := range tests {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
t.Parallel()
|
|
var gotName string
|
|
var gotArgs map[string]any
|
|
c := crypto.New(client.Func(func(ctx context.Context, name string, args map[string]any) (json.RawMessage, error) {
|
|
gotName, gotArgs = name, args
|
|
return json.RawMessage(`{"id":"o1"}`), 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 == "PreviewOrder" {
|
|
if _, ok := gotArgs["ref_id"]; ok {
|
|
t.Fatalf("ref_id on preview: %+v", gotArgs)
|
|
}
|
|
}
|
|
if tc.name == "PlaceOrder" {
|
|
if _, ok := gotArgs["idempotency_key"]; ok {
|
|
t.Fatalf("idempotency_key on place: %+v", gotArgs)
|
|
}
|
|
if gotArgs["rhs_account_number"] != "123456789" {
|
|
t.Fatalf("rhs_account_number %v", gotArgs["rhs_account_number"])
|
|
}
|
|
if gotArgs["type"] != "stop_loss" {
|
|
t.Fatalf("type %v", gotArgs["type"])
|
|
}
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestTools(t *testing.T) {
|
|
t.Parallel()
|
|
want := []string{
|
|
"cancel_crypto_order",
|
|
"get_crypto_orders",
|
|
"get_crypto_positions",
|
|
"get_crypto_quotes",
|
|
"get_currency_pairs",
|
|
"place_crypto_order",
|
|
"preview_crypto_order",
|
|
}
|
|
got := append([]string(nil), crypto.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_crypto_quotes", json.RawMessage(`{}`))
|
|
c := crypto.New(&client.Client{URL: s.URL})
|
|
_, err := c.Quotes(context.Background(), crypto.QuotesRequest{
|
|
Symbols: []string{"BTC-USD", "ETH-USD"},
|
|
RHSAccountNumber: "123456789",
|
|
})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if s.LastName() != "get_crypto_quotes" {
|
|
t.Fatalf("%s", s.LastName())
|
|
}
|
|
want := map[string]any{
|
|
"symbols": []any{"BTC-USD", "ETH-USD"},
|
|
"rhs_account_number": "123456789",
|
|
}
|
|
if diff := cmp.Diff(want, s.LastArgs()); diff != "" {
|
|
t.Fatal(diff)
|
|
}
|
|
}
|
|
|
|
func TestPlaceOrder_rhntest(t *testing.T) {
|
|
t.Parallel()
|
|
s := rhntest.New(t)
|
|
s.Set("place_crypto_order", json.RawMessage(`{"id":"o1"}`))
|
|
c := crypto.New(&client.Client{URL: s.URL})
|
|
qty := decimal.RequireFromString("0.001")
|
|
stop := decimal.RequireFromString("50000")
|
|
got, err := c.PlaceOrder(context.Background(), crypto.PlaceOrderRequest{
|
|
RHSAccountNumber: "123456789",
|
|
Symbol: "BTC-USD",
|
|
Side: client.Sell,
|
|
Type: client.StopLoss,
|
|
Qty: &qty,
|
|
StopPrice: &stop,
|
|
TimeInForce: client.GFW,
|
|
RefID: "sell:btc",
|
|
})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if got.ID != "o1" {
|
|
t.Fatalf("%+v", got)
|
|
}
|
|
if s.LastName() != "place_crypto_order" {
|
|
t.Fatalf("%s", s.LastName())
|
|
}
|
|
want := map[string]any{
|
|
"rhs_account_number": "123456789",
|
|
"symbol": "BTC-USD",
|
|
"side": "sell",
|
|
"type": "stop_loss",
|
|
"quantity": "0.001",
|
|
"stop_price": "50000",
|
|
"time_in_force": "gfw",
|
|
"ref_id": "sell:btc",
|
|
}
|
|
if diff := cmp.Diff(want, s.LastArgs()); diff != "" {
|
|
t.Fatal(diff)
|
|
}
|
|
}
|