feat: add options MCP methods
This commit is contained in:
@@ -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