feat: add watchlist MCP methods
This commit is contained in:
@@ -0,0 +1,31 @@
|
|||||||
|
package watchlists
|
||||||
|
|
||||||
|
import "s1d3sw1ped/robinhood-agentic-mcp/client"
|
||||||
|
|
||||||
|
// Client wraps Robinhood watchlist MCP tools.
|
||||||
|
type Client struct {
|
||||||
|
c client.Caller
|
||||||
|
}
|
||||||
|
|
||||||
|
// New returns a watchlists 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{
|
||||||
|
toolLists,
|
||||||
|
toolItems,
|
||||||
|
toolOptionList,
|
||||||
|
toolPopular,
|
||||||
|
toolCreate,
|
||||||
|
toolUpdate,
|
||||||
|
toolFollow,
|
||||||
|
toolUnfollow,
|
||||||
|
toolAdd,
|
||||||
|
toolRemove,
|
||||||
|
toolAddOption,
|
||||||
|
toolRemoveOption,
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,324 @@
|
|||||||
|
package watchlists
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"encoding/json"
|
||||||
|
|
||||||
|
"s1d3sw1ped/robinhood-agentic-mcp/client"
|
||||||
|
"s1d3sw1ped/robinhood-agentic-mcp/internal/wire"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
toolLists = "get_watchlists"
|
||||||
|
toolItems = "get_watchlist_items"
|
||||||
|
toolOptionList = "get_option_watchlist"
|
||||||
|
toolPopular = "get_popular_watchlists"
|
||||||
|
toolCreate = "create_watchlist"
|
||||||
|
toolUpdate = "update_watchlist"
|
||||||
|
toolFollow = "follow_watchlist"
|
||||||
|
toolUnfollow = "unfollow_watchlist"
|
||||||
|
toolAdd = "add_to_watchlist"
|
||||||
|
toolRemove = "remove_from_watchlist"
|
||||||
|
toolAddOption = "add_option_to_watchlist"
|
||||||
|
toolRemoveOption = "remove_option_from_watchlist"
|
||||||
|
)
|
||||||
|
|
||||||
|
// ListsRequest is the argument set for get_watchlists (none).
|
||||||
|
type ListsRequest struct{}
|
||||||
|
|
||||||
|
// Watchlist is one list from get_watchlists. Title falls back to name.
|
||||||
|
type Watchlist struct {
|
||||||
|
ID string
|
||||||
|
Title string
|
||||||
|
}
|
||||||
|
|
||||||
|
// ListsResult is the parsed get_watchlists payload.
|
||||||
|
type ListsResult struct {
|
||||||
|
Watchlists []Watchlist
|
||||||
|
}
|
||||||
|
|
||||||
|
// ItemsRequest is the argument set for get_watchlist_items.
|
||||||
|
type ItemsRequest struct {
|
||||||
|
ListID string
|
||||||
|
}
|
||||||
|
|
||||||
|
// Instrument is the nested instrument object on a watchlist item.
|
||||||
|
type Instrument struct {
|
||||||
|
Symbol string
|
||||||
|
Type string
|
||||||
|
}
|
||||||
|
|
||||||
|
// Item is one row from get_watchlist_items. Object types are not filtered.
|
||||||
|
type Item struct {
|
||||||
|
Symbol string
|
||||||
|
ObjectType string
|
||||||
|
Instrument *Instrument
|
||||||
|
}
|
||||||
|
|
||||||
|
// ItemsResult is the parsed get_watchlist_items payload.
|
||||||
|
type ItemsResult struct {
|
||||||
|
Items []Item
|
||||||
|
}
|
||||||
|
|
||||||
|
// OptionListRequest is the argument set for get_option_watchlist (none).
|
||||||
|
type OptionListRequest struct{}
|
||||||
|
|
||||||
|
// OptionListResult is the parsed get_option_watchlist payload.
|
||||||
|
type OptionListResult struct{}
|
||||||
|
|
||||||
|
// PopularRequest is the argument set for get_popular_watchlists (none).
|
||||||
|
type PopularRequest struct{}
|
||||||
|
|
||||||
|
// PopularResult is the parsed get_popular_watchlists payload.
|
||||||
|
type PopularResult struct{}
|
||||||
|
|
||||||
|
// CreateRequest is the argument set for create_watchlist.
|
||||||
|
type CreateRequest struct {
|
||||||
|
DisplayName string
|
||||||
|
IconEmoji string
|
||||||
|
DisplayDescription string
|
||||||
|
}
|
||||||
|
|
||||||
|
// UpdateRequest is the argument set for update_watchlist.
|
||||||
|
type UpdateRequest struct {
|
||||||
|
ListID string
|
||||||
|
DisplayName string
|
||||||
|
IconEmoji string
|
||||||
|
DisplayDescription string
|
||||||
|
}
|
||||||
|
|
||||||
|
// ListIDRequest is a single list_id argument.
|
||||||
|
type ListIDRequest struct {
|
||||||
|
ListID string
|
||||||
|
}
|
||||||
|
|
||||||
|
// AddRequest is the argument set for add_to_watchlist.
|
||||||
|
type AddRequest struct {
|
||||||
|
ListID string
|
||||||
|
Symbols []string
|
||||||
|
CurrencyPairIDs []string
|
||||||
|
IndexIDs []string
|
||||||
|
}
|
||||||
|
|
||||||
|
// RemoveRequest is the argument set for remove_from_watchlist.
|
||||||
|
type RemoveRequest struct {
|
||||||
|
ListID string
|
||||||
|
Symbols []string
|
||||||
|
CurrencyPairIDs []string
|
||||||
|
IndexIDs []string
|
||||||
|
}
|
||||||
|
|
||||||
|
// OptionMutateRequest is the argument set for add/remove option watchlist tools.
|
||||||
|
type OptionMutateRequest struct {
|
||||||
|
OptionIDs []string
|
||||||
|
PositionType string
|
||||||
|
}
|
||||||
|
|
||||||
|
// Lists calls get_watchlists.
|
||||||
|
func (c *Client) Lists(ctx context.Context, req ListsRequest) (ListsResult, error) {
|
||||||
|
raw, err := c.c.Call(ctx, toolLists, map[string]any{})
|
||||||
|
if err != nil {
|
||||||
|
return ListsResult{}, err
|
||||||
|
}
|
||||||
|
data := wire.Unwrap(raw)
|
||||||
|
var wrap struct {
|
||||||
|
Watchlists []watchlistJSON `json:"watchlists"`
|
||||||
|
}
|
||||||
|
if err := json.Unmarshal(data, &wrap); err != nil {
|
||||||
|
var list []watchlistJSON
|
||||||
|
if err := json.Unmarshal(data, &list); err != nil {
|
||||||
|
return ListsResult{}, client.ToolErrorf(toolLists, "parse: %w", err)
|
||||||
|
}
|
||||||
|
wrap.Watchlists = list
|
||||||
|
}
|
||||||
|
out := make([]Watchlist, 0, len(wrap.Watchlists))
|
||||||
|
for _, w := range wrap.Watchlists {
|
||||||
|
title := w.Title
|
||||||
|
if title == "" {
|
||||||
|
title = w.Name
|
||||||
|
}
|
||||||
|
out = append(out, Watchlist{ID: w.ID, Title: title})
|
||||||
|
}
|
||||||
|
return ListsResult{Watchlists: out}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Items calls get_watchlist_items. Every object_type is kept.
|
||||||
|
func (c *Client) Items(ctx context.Context, req ItemsRequest) (ItemsResult, error) {
|
||||||
|
raw, err := c.c.Call(ctx, toolItems, listIDArgs(req.ListID))
|
||||||
|
if err != nil {
|
||||||
|
return ItemsResult{}, err
|
||||||
|
}
|
||||||
|
data := wire.Unwrap(raw)
|
||||||
|
var wrap struct {
|
||||||
|
Items []itemJSON `json:"items"`
|
||||||
|
}
|
||||||
|
if err := json.Unmarshal(data, &wrap); err != nil {
|
||||||
|
var list []itemJSON
|
||||||
|
if err := json.Unmarshal(data, &list); err != nil {
|
||||||
|
return ItemsResult{}, client.ToolErrorf(toolItems, "parse: %w", err)
|
||||||
|
}
|
||||||
|
wrap.Items = list
|
||||||
|
}
|
||||||
|
out := make([]Item, 0, len(wrap.Items))
|
||||||
|
for _, it := range wrap.Items {
|
||||||
|
sym := it.Symbol
|
||||||
|
typ := it.ObjectType
|
||||||
|
var inst *Instrument
|
||||||
|
if it.Instrument != nil {
|
||||||
|
inst = &Instrument{Symbol: it.Instrument.Symbol, Type: it.Instrument.Type}
|
||||||
|
if typ == "" {
|
||||||
|
typ = it.Instrument.Type
|
||||||
|
}
|
||||||
|
if sym == "" {
|
||||||
|
sym = it.Instrument.Symbol
|
||||||
|
}
|
||||||
|
}
|
||||||
|
out = append(out, Item{Symbol: sym, ObjectType: typ, Instrument: inst})
|
||||||
|
}
|
||||||
|
return ItemsResult{Items: out}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// OptionList calls get_option_watchlist.
|
||||||
|
func (c *Client) OptionList(ctx context.Context, req OptionListRequest) (OptionListResult, error) {
|
||||||
|
var out OptionListResult
|
||||||
|
if err := c.parse(ctx, toolOptionList, map[string]any{}, &out); err != nil {
|
||||||
|
return OptionListResult{}, err
|
||||||
|
}
|
||||||
|
return out, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Popular calls get_popular_watchlists.
|
||||||
|
func (c *Client) Popular(ctx context.Context, req PopularRequest) (PopularResult, error) {
|
||||||
|
var out PopularResult
|
||||||
|
if err := c.parse(ctx, toolPopular, map[string]any{}, &out); err != nil {
|
||||||
|
return PopularResult{}, err
|
||||||
|
}
|
||||||
|
return out, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create calls create_watchlist.
|
||||||
|
func (c *Client) Create(ctx context.Context, req CreateRequest) error {
|
||||||
|
args := map[string]any{}
|
||||||
|
if req.DisplayName != "" {
|
||||||
|
args["display_name"] = req.DisplayName
|
||||||
|
}
|
||||||
|
if req.IconEmoji != "" {
|
||||||
|
args["icon_emoji"] = req.IconEmoji
|
||||||
|
}
|
||||||
|
if req.DisplayDescription != "" {
|
||||||
|
args["display_description"] = req.DisplayDescription
|
||||||
|
}
|
||||||
|
_, err := c.c.Call(ctx, toolCreate, args)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update calls update_watchlist.
|
||||||
|
func (c *Client) Update(ctx context.Context, req UpdateRequest) error {
|
||||||
|
args := listIDArgs(req.ListID)
|
||||||
|
if req.DisplayName != "" {
|
||||||
|
args["display_name"] = req.DisplayName
|
||||||
|
}
|
||||||
|
if req.IconEmoji != "" {
|
||||||
|
args["icon_emoji"] = req.IconEmoji
|
||||||
|
}
|
||||||
|
if req.DisplayDescription != "" {
|
||||||
|
args["display_description"] = req.DisplayDescription
|
||||||
|
}
|
||||||
|
_, err := c.c.Call(ctx, toolUpdate, args)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Follow calls follow_watchlist.
|
||||||
|
func (c *Client) Follow(ctx context.Context, req ListIDRequest) error {
|
||||||
|
_, err := c.c.Call(ctx, toolFollow, listIDArgs(req.ListID))
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Unfollow calls unfollow_watchlist.
|
||||||
|
func (c *Client) Unfollow(ctx context.Context, req ListIDRequest) error {
|
||||||
|
_, err := c.c.Call(ctx, toolUnfollow, listIDArgs(req.ListID))
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add calls add_to_watchlist.
|
||||||
|
func (c *Client) Add(ctx context.Context, req AddRequest) error {
|
||||||
|
_, err := c.c.Call(ctx, toolAdd, mutateArgs(req.ListID, req.Symbols, req.CurrencyPairIDs, req.IndexIDs))
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove calls remove_from_watchlist.
|
||||||
|
func (c *Client) Remove(ctx context.Context, req RemoveRequest) error {
|
||||||
|
_, err := c.c.Call(ctx, toolRemove, mutateArgs(req.ListID, req.Symbols, req.CurrencyPairIDs, req.IndexIDs))
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// AddOption calls add_option_to_watchlist.
|
||||||
|
func (c *Client) AddOption(ctx context.Context, req OptionMutateRequest) error {
|
||||||
|
_, err := c.c.Call(ctx, toolAddOption, optionArgs(req))
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// RemoveOption calls remove_option_from_watchlist.
|
||||||
|
func (c *Client) RemoveOption(ctx context.Context, req OptionMutateRequest) error {
|
||||||
|
_, err := c.c.Call(ctx, toolRemoveOption, optionArgs(req))
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
type watchlistJSON struct {
|
||||||
|
ID string `json:"id"`
|
||||||
|
Title string `json:"title"`
|
||||||
|
Name string `json:"name"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type itemJSON struct {
|
||||||
|
Symbol string `json:"symbol"`
|
||||||
|
ObjectType string `json:"object_type"`
|
||||||
|
Instrument *struct {
|
||||||
|
Symbol string `json:"symbol"`
|
||||||
|
Type string `json:"type"`
|
||||||
|
} `json:"instrument"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func listIDArgs(id string) map[string]any {
|
||||||
|
args := map[string]any{}
|
||||||
|
if id != "" {
|
||||||
|
args["list_id"] = id
|
||||||
|
}
|
||||||
|
return args
|
||||||
|
}
|
||||||
|
|
||||||
|
func mutateArgs(listID string, symbols, pairIDs, indexIDs []string) map[string]any {
|
||||||
|
args := listIDArgs(listID)
|
||||||
|
if len(symbols) > 0 {
|
||||||
|
args["symbols"] = symbols
|
||||||
|
}
|
||||||
|
if len(pairIDs) > 0 {
|
||||||
|
args["currency_pair_ids"] = pairIDs
|
||||||
|
}
|
||||||
|
if len(indexIDs) > 0 {
|
||||||
|
args["index_ids"] = indexIDs
|
||||||
|
}
|
||||||
|
return args
|
||||||
|
}
|
||||||
|
|
||||||
|
func optionArgs(req OptionMutateRequest) map[string]any {
|
||||||
|
args := map[string]any{}
|
||||||
|
if len(req.OptionIDs) > 0 {
|
||||||
|
args["option_ids"] = req.OptionIDs
|
||||||
|
}
|
||||||
|
if req.PositionType != "" {
|
||||||
|
args["position_type"] = req.PositionType
|
||||||
|
}
|
||||||
|
return args
|
||||||
|
}
|
||||||
|
|
||||||
|
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
|
||||||
|
}
|
||||||
@@ -0,0 +1,307 @@
|
|||||||
|
package watchlists_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"encoding/json"
|
||||||
|
"sort"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/google/go-cmp/cmp"
|
||||||
|
"s1d3sw1ped/robinhood-agentic-mcp/client"
|
||||||
|
"s1d3sw1ped/robinhood-agentic-mcp/internal/rhntest"
|
||||||
|
"s1d3sw1ped/robinhood-agentic-mcp/watchlists"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestWatchlists_toolNames(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
call func(*watchlists.Client) error
|
||||||
|
wantName string
|
||||||
|
wantArgs map[string]any
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "Lists",
|
||||||
|
call: func(c *watchlists.Client) error {
|
||||||
|
_, err := c.Lists(context.Background(), watchlists.ListsRequest{})
|
||||||
|
return err
|
||||||
|
},
|
||||||
|
wantName: "get_watchlists",
|
||||||
|
wantArgs: map[string]any{},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Items",
|
||||||
|
call: func(c *watchlists.Client) error {
|
||||||
|
_, err := c.Items(context.Background(), watchlists.ItemsRequest{ListID: "wl-1"})
|
||||||
|
return err
|
||||||
|
},
|
||||||
|
wantName: "get_watchlist_items",
|
||||||
|
wantArgs: map[string]any{"list_id": "wl-1"},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "OptionList",
|
||||||
|
call: func(c *watchlists.Client) error {
|
||||||
|
_, err := c.OptionList(context.Background(), watchlists.OptionListRequest{})
|
||||||
|
return err
|
||||||
|
},
|
||||||
|
wantName: "get_option_watchlist",
|
||||||
|
wantArgs: map[string]any{},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Popular",
|
||||||
|
call: func(c *watchlists.Client) error {
|
||||||
|
_, err := c.Popular(context.Background(), watchlists.PopularRequest{})
|
||||||
|
return err
|
||||||
|
},
|
||||||
|
wantName: "get_popular_watchlists",
|
||||||
|
wantArgs: map[string]any{},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Create",
|
||||||
|
call: func(c *watchlists.Client) error {
|
||||||
|
return c.Create(context.Background(), watchlists.CreateRequest{
|
||||||
|
DisplayName: "TRADEY",
|
||||||
|
IconEmoji: "📈",
|
||||||
|
DisplayDescription: "vwap book",
|
||||||
|
})
|
||||||
|
},
|
||||||
|
wantName: "create_watchlist",
|
||||||
|
wantArgs: map[string]any{
|
||||||
|
"display_name": "TRADEY",
|
||||||
|
"icon_emoji": "📈",
|
||||||
|
"display_description": "vwap book",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Update",
|
||||||
|
call: func(c *watchlists.Client) error {
|
||||||
|
return c.Update(context.Background(), watchlists.UpdateRequest{
|
||||||
|
ListID: "wl-1",
|
||||||
|
DisplayName: "TRADEY",
|
||||||
|
IconEmoji: "📈",
|
||||||
|
DisplayDescription: "vwap book",
|
||||||
|
})
|
||||||
|
},
|
||||||
|
wantName: "update_watchlist",
|
||||||
|
wantArgs: map[string]any{
|
||||||
|
"list_id": "wl-1",
|
||||||
|
"display_name": "TRADEY",
|
||||||
|
"icon_emoji": "📈",
|
||||||
|
"display_description": "vwap book",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Follow",
|
||||||
|
call: func(c *watchlists.Client) error {
|
||||||
|
return c.Follow(context.Background(), watchlists.ListIDRequest{ListID: "wl-pop"})
|
||||||
|
},
|
||||||
|
wantName: "follow_watchlist",
|
||||||
|
wantArgs: map[string]any{"list_id": "wl-pop"},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Unfollow",
|
||||||
|
call: func(c *watchlists.Client) error {
|
||||||
|
return c.Unfollow(context.Background(), watchlists.ListIDRequest{ListID: "wl-pop"})
|
||||||
|
},
|
||||||
|
wantName: "unfollow_watchlist",
|
||||||
|
wantArgs: map[string]any{"list_id": "wl-pop"},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Add",
|
||||||
|
call: func(c *watchlists.Client) error {
|
||||||
|
return c.Add(context.Background(), watchlists.AddRequest{
|
||||||
|
ListID: "wl-1",
|
||||||
|
Symbols: []string{"MU", "SPY"},
|
||||||
|
CurrencyPairIDs: []string{"btc-1"},
|
||||||
|
IndexIDs: []string{"idx-spx"},
|
||||||
|
})
|
||||||
|
},
|
||||||
|
wantName: "add_to_watchlist",
|
||||||
|
wantArgs: map[string]any{
|
||||||
|
"list_id": "wl-1",
|
||||||
|
"symbols": []string{"MU", "SPY"},
|
||||||
|
"currency_pair_ids": []string{"btc-1"},
|
||||||
|
"index_ids": []string{"idx-spx"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Remove",
|
||||||
|
call: func(c *watchlists.Client) error {
|
||||||
|
return c.Remove(context.Background(), watchlists.RemoveRequest{
|
||||||
|
ListID: "wl-1",
|
||||||
|
Symbols: []string{"MU"},
|
||||||
|
CurrencyPairIDs: []string{"btc-1"},
|
||||||
|
IndexIDs: []string{"idx-spx"},
|
||||||
|
})
|
||||||
|
},
|
||||||
|
wantName: "remove_from_watchlist",
|
||||||
|
wantArgs: map[string]any{
|
||||||
|
"list_id": "wl-1",
|
||||||
|
"symbols": []string{"MU"},
|
||||||
|
"currency_pair_ids": []string{"btc-1"},
|
||||||
|
"index_ids": []string{"idx-spx"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "AddOption",
|
||||||
|
call: func(c *watchlists.Client) error {
|
||||||
|
return c.AddOption(context.Background(), watchlists.OptionMutateRequest{
|
||||||
|
OptionIDs: []string{"opt-1", "opt-2"},
|
||||||
|
PositionType: "long",
|
||||||
|
})
|
||||||
|
},
|
||||||
|
wantName: "add_option_to_watchlist",
|
||||||
|
wantArgs: map[string]any{
|
||||||
|
"option_ids": []string{"opt-1", "opt-2"},
|
||||||
|
"position_type": "long",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "RemoveOption",
|
||||||
|
call: func(c *watchlists.Client) error {
|
||||||
|
return c.RemoveOption(context.Background(), watchlists.OptionMutateRequest{
|
||||||
|
OptionIDs: []string{"opt-1"},
|
||||||
|
PositionType: "short",
|
||||||
|
})
|
||||||
|
},
|
||||||
|
wantName: "remove_option_from_watchlist",
|
||||||
|
wantArgs: map[string]any{
|
||||||
|
"option_ids": []string{"opt-1"},
|
||||||
|
"position_type": "short",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
for _, tc := range tests {
|
||||||
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
var gotName string
|
||||||
|
var gotArgs map[string]any
|
||||||
|
c := watchlists.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)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestTools(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
want := []string{
|
||||||
|
"add_option_to_watchlist",
|
||||||
|
"add_to_watchlist",
|
||||||
|
"create_watchlist",
|
||||||
|
"follow_watchlist",
|
||||||
|
"get_option_watchlist",
|
||||||
|
"get_popular_watchlists",
|
||||||
|
"get_watchlist_items",
|
||||||
|
"get_watchlists",
|
||||||
|
"remove_from_watchlist",
|
||||||
|
"remove_option_from_watchlist",
|
||||||
|
"unfollow_watchlist",
|
||||||
|
"update_watchlist",
|
||||||
|
}
|
||||||
|
got := append([]string(nil), watchlists.Tools()...)
|
||||||
|
sort.Strings(got)
|
||||||
|
if diff := cmp.Diff(want, got); diff != "" {
|
||||||
|
t.Fatal(diff)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestLists_rhntest(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
s := rhntest.New(t)
|
||||||
|
s.Set("get_watchlists", json.RawMessage(`{"watchlists":[{"id":"wl-1","title":"TRADEY"}]}`))
|
||||||
|
c := watchlists.New(&client.Client{URL: s.URL})
|
||||||
|
got, err := c.Lists(context.Background(), watchlists.ListsRequest{})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if len(got.Watchlists) != 1 || got.Watchlists[0].ID != "wl-1" || got.Watchlists[0].Title != "TRADEY" {
|
||||||
|
t.Fatalf("%+v", got)
|
||||||
|
}
|
||||||
|
if s.LastName() != "get_watchlists" {
|
||||||
|
t.Fatalf("%s", s.LastName())
|
||||||
|
}
|
||||||
|
if diff := cmp.Diff(map[string]any{}, s.LastArgs()); diff != "" {
|
||||||
|
t.Fatal(diff)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestItems_rhntest(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
s := rhntest.New(t)
|
||||||
|
s.Set("get_watchlist_items", json.RawMessage(`{"items":[{"symbol":"MU","object_type":"equity"}]}`))
|
||||||
|
c := watchlists.New(&client.Client{URL: s.URL})
|
||||||
|
got, err := c.Items(context.Background(), watchlists.ItemsRequest{ListID: "wl-1"})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if len(got.Items) != 1 || got.Items[0].Symbol != "MU" || got.Items[0].ObjectType != "equity" {
|
||||||
|
t.Fatalf("%+v", got)
|
||||||
|
}
|
||||||
|
if s.LastName() != "get_watchlist_items" {
|
||||||
|
t.Fatalf("%s", s.LastName())
|
||||||
|
}
|
||||||
|
want := map[string]any{"list_id": "wl-1"}
|
||||||
|
if diff := cmp.Diff(want, s.LastArgs()); diff != "" {
|
||||||
|
t.Fatal(diff)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestLists_titleFallsBackToName(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
c := watchlists.New(client.Func(func(ctx context.Context, name string, args map[string]any) (json.RawMessage, error) {
|
||||||
|
return json.RawMessage(`{"watchlists":[{"id":"wl-1","title":"tradey"},{"id":"wl-2","name":"Other"}]}`), nil
|
||||||
|
}))
|
||||||
|
got, err := c.Lists(context.Background(), watchlists.ListsRequest{})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
want := watchlists.ListsResult{
|
||||||
|
Watchlists: []watchlists.Watchlist{
|
||||||
|
{ID: "wl-1", Title: "tradey"},
|
||||||
|
{ID: "wl-2", Title: "Other"},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
if diff := cmp.Diff(want, got); diff != "" {
|
||||||
|
t.Fatal(diff)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestItems_keepsObjectTypesAndNestedSymbol(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
c := watchlists.New(client.Func(func(ctx context.Context, name string, args map[string]any) (json.RawMessage, error) {
|
||||||
|
return json.RawMessage(`{"items":[
|
||||||
|
{"symbol":"MU","object_type":"equity"},
|
||||||
|
{"symbol":"BTC","object_type":"crypto"},
|
||||||
|
{"instrument":{"symbol":"SPY","type":"etf"}}
|
||||||
|
]}`), nil
|
||||||
|
}))
|
||||||
|
got, err := c.Items(context.Background(), watchlists.ItemsRequest{ListID: "wl-1"})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
want := watchlists.ItemsResult{
|
||||||
|
Items: []watchlists.Item{
|
||||||
|
{Symbol: "MU", ObjectType: "equity"},
|
||||||
|
{Symbol: "BTC", ObjectType: "crypto"},
|
||||||
|
{
|
||||||
|
Symbol: "SPY",
|
||||||
|
ObjectType: "etf",
|
||||||
|
Instrument: &watchlists.Instrument{Symbol: "SPY", Type: "etf"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
if diff := cmp.Diff(want, got); diff != "" {
|
||||||
|
t.Fatal(diff)
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user