diff --git a/options/client.go b/options/client.go new file mode 100644 index 0000000..da22fc9 --- /dev/null +++ b/options/client.go @@ -0,0 +1,31 @@ +package options + +import "s1d3sw1ped/robinhood-agentic-mcp/client" + +// Client wraps Robinhood options MCP tools. +type Client struct { + c client.Caller +} + +// New returns an options 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{ + toolChains, + toolInstruments, + toolQuotes, + toolPositions, + toolOrders, + toolHistoricals, + toolReview, + toolPlace, + toolCancel, + toolReplace, + toolExercise, + toolCancelExercise, + } +} diff --git a/options/options_test.go b/options/options_test.go new file mode 100644 index 0000000..2cd4fbd --- /dev/null +++ b/options/options_test.go @@ -0,0 +1,420 @@ +package options_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/internal/rhntest" + "s1d3sw1ped/robinhood-agentic-mcp/options" +) + +func TestOptions_toolNames(t *testing.T) { + t.Parallel() + qty := decimal.NewFromInt(1) + px := decimal.RequireFromString("1.5") + stop := decimal.RequireFromString("1.4") + start := time.Date(2026, 8, 18, 13, 30, 0, 0, time.UTC) + end := time.Date(2026, 8, 18, 20, 0, 0, 0, time.UTC) + leg := options.Leg{ + OptionID: "opt-1", + Side: client.Buy, + PositionEffect: "open", + RatioQuantity: 1, + } + legsWire := []map[string]any{ + {"option_id": "opt-1", "side": "buy", "position_effect": "open", "ratio_quantity": 1}, + } + tests := []struct { + name string + call func(*options.Client) error + wantName string + wantArgs map[string]any + }{ + { + name: "Chains", + call: func(c *options.Client) error { + _, err := c.Chains(context.Background(), options.ChainsRequest{ + IDs: "chain-1", + UnderlyingSymbol: "AAPL", + }) + return err + }, + wantName: "get_option_chains", + wantArgs: map[string]any{"ids": "chain-1", "underlying_symbol": "AAPL"}, + }, + { + name: "Instruments", + call: func(c *options.Client) error { + _, err := c.Instruments(context.Background(), options.InstrumentsRequest{ + ChainID: "chain-1", + ChainSymbol: "AAPL", + ExpirationDates: "2026-09-18", + StrikePrice: "150.0000", + Type: "call", + State: "active", + Tradability: "tradable", + IDs: "opt-1", + Cursor: "c1", + }) + return err + }, + wantName: "get_option_instruments", + wantArgs: map[string]any{ + "chain_id": "chain-1", + "chain_symbol": "AAPL", + "expiration_dates": "2026-09-18", + "strike_price": "150.0000", + "type": "call", + "state": "active", + "tradability": "tradable", + "ids": "opt-1", + "cursor": "c1", + }, + }, + { + name: "Quotes", + call: func(c *options.Client) error { + _, err := c.Quotes(context.Background(), options.QuotesRequest{ + InstrumentIDs: []string{"opt-1", "opt-2"}, + }) + return err + }, + wantName: "get_option_quotes", + wantArgs: map[string]any{"instrument_ids": []string{"opt-1", "opt-2"}}, + }, + { + name: "Positions", + call: func(c *options.Client) error { + _, err := c.Positions(context.Background(), options.PositionsRequest{ + AccountNumber: "acct-1", + Nonzero: true, + ChainIDs: "chain-1", + OptionIDs: "opt-1", + Type: "long", + OptionType: "call", + ExpirationDate: "2026-09-18", + ExpirationDateLTE: "2026-12-31", + ExpirationDateGTE: "2026-01-01", + Cursor: "c1", + }) + return err + }, + wantName: "get_option_positions", + wantArgs: map[string]any{ + "account_number": "acct-1", + "nonzero": true, + "chain_ids": "chain-1", + "option_ids": "opt-1", + "type": "long", + "option_type": "call", + "expiration_date": "2026-09-18", + "expiration_date_lte": "2026-12-31", + "expiration_date_gte": "2026-01-01", + "cursor": "c1", + }, + }, + { + name: "Orders", + call: func(c *options.Client) error { + _, err := c.Orders(context.Background(), options.OrdersRequest{ + AccountNumber: "acct-1", + OrderID: "o1", + State: "filled", + CreatedAtGTE: "2026-08-18", + ChainIDs: "chain-1", + UnderlyingType: "equity", + PlacedAgent: "agentic", + Cursor: "c1", + }) + return err + }, + wantName: "get_option_orders", + wantArgs: map[string]any{ + "account_number": "acct-1", + "order_id": "o1", + "state": "filled", + "created_at_gte": "2026-08-18", + "chain_ids": "chain-1", + "underlying_type": "equity", + "placed_agent": "agentic", + "cursor": "c1", + }, + }, + { + name: "Historicals", + call: func(c *options.Client) error { + _, err := c.Historicals(context.Background(), options.HistoricalsRequest{ + InstrumentIDs: []string{"opt-1"}, + StartTime: start, + EndTime: end, + Interval: "minute", + Bounds: "regular", + }) + return err + }, + wantName: "get_option_historicals", + wantArgs: map[string]any{ + "instrument_ids": []string{"opt-1"}, + "start_time": "2026-08-18T13:30:00Z", + "end_time": "2026-08-18T20:00:00Z", + "interval": "minute", + "bounds": "regular", + }, + }, + { + name: "ReviewOrder", + call: func(c *options.Client) error { + _, err := c.ReviewOrder(context.Background(), options.PlaceOrderRequest{ + AccountNumber: "acct", + Legs: []options.Leg{leg}, + Type: client.Limit, + Quantity: &qty, + Price: &px, + TimeInForce: client.GFD, + MarketHours: client.RegularHours, + ChainSymbol: "AAPL", + UnderlyingType: "equity", + RefID: "buy:opt", + }) + return err + }, + wantName: "review_option_order", + wantArgs: map[string]any{ + "account_number": "acct", + "legs": legsWire, + "type": "limit", + "quantity": "1", + "price": "1.5", + "time_in_force": "gfd", + "market_hours": "regular_hours", + "chain_symbol": "AAPL", + "underlying_type": "equity", + }, + }, + { + name: "PlaceOrder", + call: func(c *options.Client) error { + _, err := c.PlaceOrder(context.Background(), options.PlaceOrderRequest{ + AccountNumber: "acct", + Legs: []options.Leg{leg}, + Direction: "debit", + Type: client.StopLimit, + Quantity: &qty, + Price: &px, + StopPrice: &stop, + TimeInForce: client.GTC, + MarketHours: client.RegularCurbHours, + RefID: "buy:opt", + }) + return err + }, + wantName: "place_option_order", + wantArgs: map[string]any{ + "account_number": "acct", + "legs": legsWire, + "direction": "debit", + "type": "stop_limit", + "quantity": "1", + "price": "1.5", + "stop_price": "1.4", + "time_in_force": "gtc", + "market_hours": "regular_curb_hours", + "ref_id": "buy:opt", + }, + }, + { + name: "CancelOrder", + call: func(c *options.Client) error { + return c.CancelOrder(context.Background(), options.CancelOrderRequest{ + AccountNumber: "acct", + OrderID: "o1", + }) + }, + wantName: "cancel_option_order", + wantArgs: map[string]any{"account_number": "acct", "order_id": "o1"}, + }, + { + name: "ReplaceOrder", + call: func(c *options.Client) error { + _, err := c.ReplaceOrder(context.Background(), options.ReplaceOrderRequest{ + AccountNumber: "acct", + OrderID: "o1", + Legs: []options.Leg{leg}, + Type: client.Limit, + Quantity: &qty, + Price: &px, + RefID: "repl:opt", + }) + return err + }, + wantName: "replace_option_order", + wantArgs: map[string]any{ + "account_number": "acct", + "order_id": "o1", + "legs": legsWire, + "type": "limit", + "quantity": "1", + "price": "1.5", + "ref_id": "repl:opt", + }, + }, + { + name: "Exercise", + call: func(c *options.Client) error { + _, err := c.Exercise(context.Background(), options.ExerciseRequest{ + AccountNumber: "acct", + OptionID: "opt-1", + Quantity: 2, + RefID: "ex:opt", + Reason: "buying_stocks", + AllowShorts: true, + }) + return err + }, + wantName: "exercise_option", + wantArgs: map[string]any{ + "account_number": "acct", + "option_id": "opt-1", + "quantity": 2, + "ref_id": "ex:opt", + "reason": "buying_stocks", + "allow_shorts": true, + }, + }, + { + name: "CancelExercise", + call: func(c *options.Client) error { + return c.CancelExercise(context.Background(), options.CancelExerciseRequest{ + AccountNumber: "acct", + OptionID: "opt-1", + }) + }, + wantName: "cancel_option_exercise", + wantArgs: map[string]any{"account_number": "acct", "option_id": "opt-1"}, + }, + } + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + t.Parallel() + var gotName string + var gotArgs map[string]any + c := options.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 == "ReviewOrder" { + if _, ok := gotArgs["ref_id"]; ok { + t.Fatalf("ref_id on review: %+v", gotArgs) + } + } + if tc.name == "PlaceOrder" { + if _, ok := gotArgs["idempotency_key"]; ok { + t.Fatalf("idempotency_key on place: %+v", gotArgs) + } + if _, ok := gotArgs["chain_symbol"]; ok { + t.Fatalf("chain_symbol on place: %+v", gotArgs) + } + } + }) + } +} + +func TestTools(t *testing.T) { + t.Parallel() + want := []string{ + "cancel_option_exercise", + "cancel_option_order", + "exercise_option", + "get_option_chains", + "get_option_historicals", + "get_option_instruments", + "get_option_orders", + "get_option_positions", + "get_option_quotes", + "place_option_order", + "replace_option_order", + "review_option_order", + } + got := append([]string(nil), options.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_option_quotes", json.RawMessage(`{}`)) + c := options.New(&client.Client{URL: s.URL}) + _, err := c.Quotes(context.Background(), options.QuotesRequest{ + InstrumentIDs: []string{"opt-1", "opt-2"}, + }) + if err != nil { + t.Fatal(err) + } + if s.LastName() != "get_option_quotes" { + t.Fatalf("%s", s.LastName()) + } + want := map[string]any{"instrument_ids": []any{"opt-1", "opt-2"}} + 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_option_order", json.RawMessage(`{"id":"o1"}`)) + c := options.New(&client.Client{URL: s.URL}) + qty := decimal.NewFromInt(1) + px := decimal.RequireFromString("1.5") + got, err := c.PlaceOrder(context.Background(), options.PlaceOrderRequest{ + AccountNumber: "acct", + Legs: []options.Leg{{ + OptionID: "opt-1", + Side: client.Buy, + PositionEffect: "open", + RatioQuantity: 1, + }}, + Type: client.Limit, + Quantity: &qty, + Price: &px, + RefID: "buy:opt", + }) + if err != nil { + t.Fatal(err) + } + if got.ID != "o1" { + t.Fatalf("%+v", got) + } + if s.LastName() != "place_option_order" { + t.Fatalf("%s", s.LastName()) + } + wantLegs := []any{ + map[string]any{ + "option_id": "opt-1", + "side": "buy", + "position_effect": "open", + "ratio_quantity": float64(1), + }, + } + if diff := cmp.Diff(wantLegs, s.LastArgs()["legs"]); diff != "" { + t.Fatal(diff) + } +} diff --git a/options/read.go b/options/read.go new file mode 100644 index 0000000..9c70d40 --- /dev/null +++ b/options/read.go @@ -0,0 +1,271 @@ +package options + +import ( + "context" + "encoding/json" + "time" + + "s1d3sw1ped/robinhood-agentic-mcp/client" + "s1d3sw1ped/robinhood-agentic-mcp/internal/wire" +) + +const ( + toolChains = "get_option_chains" + toolInstruments = "get_option_instruments" + toolQuotes = "get_option_quotes" + toolPositions = "get_option_positions" + toolOrders = "get_option_orders" + toolHistoricals = "get_option_historicals" +) + +// ChainsRequest is the argument set for get_option_chains. +type ChainsRequest struct { + IDs string + UnderlyingSymbol string +} + +// ChainsResult is the parsed get_option_chains payload. +type ChainsResult struct{} + +// InstrumentsRequest is the argument set for get_option_instruments. +type InstrumentsRequest struct { + ChainID string + ChainSymbol string + ExpirationDates string + StrikePrice string + Type string + State string + Tradability string + IDs string + Cursor string +} + +// InstrumentsResult is the parsed get_option_instruments payload. +type InstrumentsResult struct{} + +// QuotesRequest is the argument set for get_option_quotes. +type QuotesRequest struct { + InstrumentIDs []string +} + +// QuotesResult is the parsed get_option_quotes payload. +type QuotesResult struct{} + +// PositionsRequest is the argument set for get_option_positions. +type PositionsRequest struct { + AccountNumber string + Nonzero bool + ChainIDs string + OptionIDs string + Type string + OptionType string + ExpirationDate string + ExpirationDateLTE string + ExpirationDateGTE string + Cursor string +} + +// PositionsResult is the parsed get_option_positions payload. +type PositionsResult struct{} + +// OrdersRequest is the argument set for get_option_orders. +type OrdersRequest struct { + AccountNumber string + OrderID string + State string + CreatedAtGTE string + ChainIDs string + UnderlyingType string + PlacedAgent string + Cursor string +} + +// OrdersResult is the parsed get_option_orders payload. +type OrdersResult struct{} + +// HistoricalsRequest is the argument set for get_option_historicals. +type HistoricalsRequest struct { + InstrumentIDs []string + StartTime time.Time + EndTime time.Time + Interval string + Bounds string +} + +// HistoricalsResult is the parsed get_option_historicals payload. +type HistoricalsResult struct{} + +// Chains calls get_option_chains. +func (c *Client) Chains(ctx context.Context, req ChainsRequest) (ChainsResult, error) { + args := map[string]any{} + if req.IDs != "" { + args["ids"] = req.IDs + } + if req.UnderlyingSymbol != "" { + args["underlying_symbol"] = req.UnderlyingSymbol + } + var out ChainsResult + if err := c.parse(ctx, toolChains, args, &out); err != nil { + return ChainsResult{}, err + } + return out, nil +} + +// Instruments calls get_option_instruments. +func (c *Client) Instruments(ctx context.Context, req InstrumentsRequest) (InstrumentsResult, error) { + args := map[string]any{} + if req.ChainID != "" { + args["chain_id"] = req.ChainID + } + if req.ChainSymbol != "" { + args["chain_symbol"] = req.ChainSymbol + } + if req.ExpirationDates != "" { + args["expiration_dates"] = req.ExpirationDates + } + if req.StrikePrice != "" { + args["strike_price"] = req.StrikePrice + } + if req.Type != "" { + args["type"] = req.Type + } + if req.State != "" { + args["state"] = req.State + } + if req.Tradability != "" { + args["tradability"] = req.Tradability + } + if req.IDs != "" { + args["ids"] = req.IDs + } + if req.Cursor != "" { + args["cursor"] = req.Cursor + } + var out InstrumentsResult + if err := c.parse(ctx, toolInstruments, args, &out); err != nil { + return InstrumentsResult{}, err + } + return out, nil +} + +// Quotes calls get_option_quotes. +func (c *Client) Quotes(ctx context.Context, req QuotesRequest) (QuotesResult, error) { + args := map[string]any{} + if len(req.InstrumentIDs) > 0 { + args["instrument_ids"] = req.InstrumentIDs + } + var out QuotesResult + if err := c.parse(ctx, toolQuotes, args, &out); err != nil { + return QuotesResult{}, err + } + return out, nil +} + +// Positions calls get_option_positions. +func (c *Client) Positions(ctx context.Context, req PositionsRequest) (PositionsResult, error) { + args := map[string]any{} + if req.AccountNumber != "" { + args["account_number"] = req.AccountNumber + } + if req.Nonzero { + args["nonzero"] = true + } + if req.ChainIDs != "" { + args["chain_ids"] = req.ChainIDs + } + if req.OptionIDs != "" { + args["option_ids"] = req.OptionIDs + } + if req.Type != "" { + args["type"] = req.Type + } + if req.OptionType != "" { + args["option_type"] = req.OptionType + } + if req.ExpirationDate != "" { + args["expiration_date"] = req.ExpirationDate + } + if req.ExpirationDateLTE != "" { + args["expiration_date_lte"] = req.ExpirationDateLTE + } + if req.ExpirationDateGTE != "" { + args["expiration_date_gte"] = req.ExpirationDateGTE + } + if req.Cursor != "" { + args["cursor"] = req.Cursor + } + var out PositionsResult + if err := c.parse(ctx, toolPositions, args, &out); err != nil { + return PositionsResult{}, err + } + return out, nil +} + +// Orders calls get_option_orders. +func (c *Client) Orders(ctx context.Context, req OrdersRequest) (OrdersResult, error) { + args := map[string]any{} + if req.AccountNumber != "" { + args["account_number"] = req.AccountNumber + } + if req.OrderID != "" { + args["order_id"] = req.OrderID + } + if req.State != "" { + args["state"] = req.State + } + if req.CreatedAtGTE != "" { + args["created_at_gte"] = req.CreatedAtGTE + } + if req.ChainIDs != "" { + args["chain_ids"] = req.ChainIDs + } + if req.UnderlyingType != "" { + args["underlying_type"] = req.UnderlyingType + } + if req.PlacedAgent != "" { + args["placed_agent"] = req.PlacedAgent + } + if req.Cursor != "" { + args["cursor"] = req.Cursor + } + var out OrdersResult + if err := c.parse(ctx, toolOrders, args, &out); err != nil { + return OrdersResult{}, err + } + return out, nil +} + +// Historicals calls get_option_historicals. +func (c *Client) Historicals(ctx context.Context, req HistoricalsRequest) (HistoricalsResult, error) { + args := map[string]any{ + "start_time": req.StartTime.UTC().Format(time.RFC3339), + } + if len(req.InstrumentIDs) > 0 { + args["instrument_ids"] = req.InstrumentIDs + } + if !req.EndTime.IsZero() { + args["end_time"] = req.EndTime.UTC().Format(time.RFC3339) + } + if req.Interval != "" { + args["interval"] = req.Interval + } + if req.Bounds != "" { + args["bounds"] = req.Bounds + } + var out HistoricalsResult + if err := c.parse(ctx, toolHistoricals, args, &out); err != nil { + return HistoricalsResult{}, err + } + return out, nil +} + +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 +} diff --git a/options/write.go b/options/write.go new file mode 100644 index 0000000..f255b28 --- /dev/null +++ b/options/write.go @@ -0,0 +1,246 @@ +package options + +import ( + "context" + + decimal "github.com/alpacahq/alpacadecimal" + "s1d3sw1ped/robinhood-agentic-mcp/client" + "s1d3sw1ped/robinhood-agentic-mcp/internal/wire" +) + +const ( + toolReview = "review_option_order" + toolPlace = "place_option_order" + toolCancel = "cancel_option_order" + toolReplace = "replace_option_order" + toolExercise = "exercise_option" + toolCancelExercise = "cancel_option_exercise" +) + +// Leg is one contract in an option order. +type Leg struct { + OptionID string + Side client.Side + PositionEffect string // "open" | "close" + RatioQuantity int +} + +// PlaceOrderRequest is the argument set for review_option_order and place_option_order. +type PlaceOrderRequest struct { + AccountNumber string + Legs []Leg + Direction string + Type client.OrderType + Quantity *decimal.Decimal + Price *decimal.Decimal + StopPrice *decimal.Decimal + TimeInForce client.TimeInForce // empty → omit (Robinhood defaults gfd) + MarketHours client.MarketHours + RefID string + ChainSymbol string // review only + UnderlyingType string // review only +} + +// ReplaceOrderRequest is the argument set for replace_option_order. +type ReplaceOrderRequest struct { + AccountNumber string + OrderID string + Legs []Leg + Direction string + Type client.OrderType + Quantity *decimal.Decimal + Price *decimal.Decimal + StopPrice *decimal.Decimal + TimeInForce client.TimeInForce + MarketHours client.MarketHours + RefID string +} + +// ReviewResult is the pre-trade check from review_option_order. +type ReviewResult struct{} + +// Order is a placed or replaced option order. +type Order struct { + ID string `json:"id"` +} + +// CancelOrderRequest is the argument set for cancel_option_order. +type CancelOrderRequest struct { + AccountNumber string + OrderID string +} + +// ExerciseRequest is the argument set for exercise_option. +type ExerciseRequest struct { + AccountNumber string + OptionID string + Quantity int + RefID string + Reason string + AllowShorts bool +} + +// ExerciseResult is the parsed exercise_option payload. +type ExerciseResult struct{} + +// CancelExerciseRequest is the argument set for cancel_option_exercise. +type CancelExerciseRequest struct { + AccountNumber string + OptionID string +} + +// ReviewOrder calls review_option_order. RefID is not sent. +func (c *Client) ReviewOrder(ctx context.Context, req PlaceOrderRequest) (ReviewResult, error) { + var out ReviewResult + if err := c.parse(ctx, toolReview, placeArgs(req, false, true), &out); err != nil { + return ReviewResult{}, err + } + return out, nil +} + +// PlaceOrder calls place_option_order. RefID is sent as ref_id. +func (c *Client) PlaceOrder(ctx context.Context, req PlaceOrderRequest) (Order, error) { + var out Order + if err := c.parse(ctx, toolPlace, placeArgs(req, true, false), &out); err != nil { + return Order{}, err + } + return out, nil +} + +// CancelOrder calls cancel_option_order. +func (c *Client) CancelOrder(ctx context.Context, req CancelOrderRequest) error { + args := map[string]any{} + if req.AccountNumber != "" { + args["account_number"] = req.AccountNumber + } + if req.OrderID != "" { + args["order_id"] = req.OrderID + } + _, err := c.c.Call(ctx, toolCancel, args) + return err +} + +// ReplaceOrder calls replace_option_order. +func (c *Client) ReplaceOrder(ctx context.Context, req ReplaceOrderRequest) (Order, error) { + args := placeArgs(PlaceOrderRequest{ + AccountNumber: req.AccountNumber, + Legs: req.Legs, + Direction: req.Direction, + Type: req.Type, + Quantity: req.Quantity, + Price: req.Price, + StopPrice: req.StopPrice, + TimeInForce: req.TimeInForce, + MarketHours: req.MarketHours, + RefID: req.RefID, + }, true, false) + if req.OrderID != "" { + args["order_id"] = req.OrderID + } + var out Order + if err := c.parse(ctx, toolReplace, args, &out); err != nil { + return Order{}, err + } + return out, nil +} + +// Exercise calls exercise_option. +func (c *Client) Exercise(ctx context.Context, req ExerciseRequest) (ExerciseResult, error) { + args := map[string]any{} + if req.AccountNumber != "" { + args["account_number"] = req.AccountNumber + } + if req.OptionID != "" { + args["option_id"] = req.OptionID + } + if req.Quantity != 0 { + args["quantity"] = req.Quantity + } + if req.RefID != "" { + args["ref_id"] = req.RefID + } + if req.Reason != "" { + args["reason"] = req.Reason + } + if req.AllowShorts { + args["allow_shorts"] = true + } + var out ExerciseResult + if err := c.parse(ctx, toolExercise, args, &out); err != nil { + return ExerciseResult{}, err + } + return out, nil +} + +// CancelExercise calls cancel_option_exercise. +func (c *Client) CancelExercise(ctx context.Context, req CancelExerciseRequest) error { + args := map[string]any{} + if req.AccountNumber != "" { + args["account_number"] = req.AccountNumber + } + if req.OptionID != "" { + args["option_id"] = req.OptionID + } + _, err := c.c.Call(ctx, toolCancelExercise, args) + return err +} + +func placeArgs(req PlaceOrderRequest, withRef, review bool) map[string]any { + args := map[string]any{} + if req.AccountNumber != "" { + args["account_number"] = req.AccountNumber + } + if len(req.Legs) > 0 { + legs := make([]map[string]any, len(req.Legs)) + for i, leg := range req.Legs { + m := map[string]any{} + if leg.OptionID != "" { + m["option_id"] = leg.OptionID + } + if leg.Side != "" { + m["side"] = string(leg.Side) + } + if leg.PositionEffect != "" { + m["position_effect"] = leg.PositionEffect + } + if leg.RatioQuantity != 0 { + m["ratio_quantity"] = leg.RatioQuantity + } + legs[i] = m + } + args["legs"] = legs + } + if req.Direction != "" { + args["direction"] = req.Direction + } + if req.Type != "" { + args["type"] = string(req.Type) + } + if req.Quantity != nil { + args["quantity"] = wire.Encode(*req.Quantity) + } + if req.Price != nil { + args["price"] = wire.Encode(*req.Price) + } + if req.StopPrice != nil { + args["stop_price"] = wire.Encode(*req.StopPrice) + } + if req.TimeInForce != "" { + args["time_in_force"] = string(req.TimeInForce) + } + if req.MarketHours != "" { + args["market_hours"] = string(req.MarketHours) + } + if withRef && req.RefID != "" { + args["ref_id"] = req.RefID + } + if review { + if req.ChainSymbol != "" { + args["chain_symbol"] = req.ChainSymbol + } + if req.UnderlyingType != "" { + args["underlying_type"] = req.UnderlyingType + } + } + return args +}