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 { Errors []string `json:"errors"` Warnings []string `json:"warnings"` Alerts []string `json:"alerts"` } // Order is a placed, replaced, or listed option order. type Order struct { ID string `json:"id"` State string `json:"state"` Qty decimal.Decimal `json:"-"` Price decimal.Decimal `json:"-"` } // 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 { ID string `json:"id"` State string `json:"state"` } // 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) { raw, err := c.c.Call(ctx, toolPlace, placeArgs(req, true, false)) if err != nil { return Order{}, err } ords, err := parseOrders(raw) if err != nil { return Order{}, client.ToolErrorf(toolPlace, "parse: %w", err) } if len(ords) > 0 { return ords[0], nil } return Order{}, 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 } raw, err := c.c.Call(ctx, toolReplace, args) if err != nil { return Order{}, err } ords, err := parseOrders(raw) if err != nil { return Order{}, client.ToolErrorf(toolReplace, "parse: %w", err) } if len(ords) > 0 { return ords[0], nil } return Order{}, 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 }