feat: add options MCP methods
This commit is contained in:
@@ -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,
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
+271
@@ -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
|
||||||
|
}
|
||||||
@@ -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
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user