135 lines
3.4 KiB
Go
135 lines
3.4 KiB
Go
package equity
|
|
|
|
import (
|
|
"context"
|
|
|
|
decimal "github.com/alpacahq/alpacadecimal"
|
|
"s1d3sw1ped/robinhood-agentic-mcp/client"
|
|
"s1d3sw1ped/robinhood-agentic-mcp/internal/wire"
|
|
)
|
|
|
|
const (
|
|
toolReview = "review_equity_order"
|
|
toolPlace = "place_equity_order"
|
|
toolCancel = "cancel_equity_order"
|
|
)
|
|
|
|
// PlaceOrderRequest is the argument set for review_equity_order and place_equity_order.
|
|
type PlaceOrderRequest struct {
|
|
AccountNumber string
|
|
Symbol string
|
|
Side client.Side
|
|
Type client.OrderType
|
|
Qty *decimal.Decimal
|
|
DollarAmount *decimal.Decimal
|
|
LimitPrice *decimal.Decimal
|
|
StopPrice *decimal.Decimal
|
|
TimeInForce client.TimeInForce // empty → omit (Robinhood defaults gfd)
|
|
MarketHours client.MarketHours
|
|
TaxLots []TaxLot
|
|
RefID string
|
|
}
|
|
|
|
// TaxLot is a specified-lot selection for a sell order.
|
|
type TaxLot struct {
|
|
OpenLotID string
|
|
Quantity decimal.Decimal
|
|
}
|
|
|
|
// ReviewResult is the pre-trade check from review_equity_order.
|
|
type ReviewResult struct {
|
|
Errors []string `json:"errors"`
|
|
Warnings []string `json:"warnings"`
|
|
}
|
|
|
|
// Order is a placed equity order.
|
|
type Order struct {
|
|
ID string `json:"id"`
|
|
}
|
|
|
|
// CancelOrderRequest is the argument set for cancel_equity_order.
|
|
type CancelOrderRequest struct {
|
|
AccountNumber string
|
|
OrderID string
|
|
}
|
|
|
|
// ReviewOrder calls review_equity_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), &out); err != nil {
|
|
return ReviewResult{}, err
|
|
}
|
|
return out, nil
|
|
}
|
|
|
|
// PlaceOrder calls place_equity_order. RefID is sent as both ref_id and idempotency_key.
|
|
func (c *Client) PlaceOrder(ctx context.Context, req PlaceOrderRequest) (Order, error) {
|
|
var out Order
|
|
if err := c.parse(ctx, toolPlace, placeArgs(req, true), &out); err != nil {
|
|
return Order{}, err
|
|
}
|
|
return out, nil
|
|
}
|
|
|
|
// CancelOrder calls cancel_equity_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
|
|
}
|
|
|
|
func placeArgs(req PlaceOrderRequest, withRef bool) map[string]any {
|
|
args := map[string]any{}
|
|
if req.AccountNumber != "" {
|
|
args["account_number"] = req.AccountNumber
|
|
}
|
|
if req.Symbol != "" {
|
|
args["symbol"] = req.Symbol
|
|
}
|
|
if req.Side != "" {
|
|
args["side"] = string(req.Side)
|
|
}
|
|
if req.Type != "" {
|
|
args["type"] = string(req.Type)
|
|
}
|
|
if req.Qty != nil {
|
|
args["quantity"] = wire.Encode(*req.Qty)
|
|
}
|
|
if req.DollarAmount != nil {
|
|
args["dollar_amount"] = wire.Encode(*req.DollarAmount)
|
|
}
|
|
if req.LimitPrice != nil {
|
|
args["limit_price"] = wire.Encode(*req.LimitPrice)
|
|
}
|
|
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 len(req.TaxLots) > 0 {
|
|
lots := make([]map[string]any, len(req.TaxLots))
|
|
for i, lot := range req.TaxLots {
|
|
lots[i] = map[string]any{
|
|
"open_lot_id": lot.OpenLotID,
|
|
"quantity": wire.Encode(lot.Quantity),
|
|
}
|
|
}
|
|
args["tax_lots"] = lots
|
|
}
|
|
if withRef && req.RefID != "" {
|
|
args["ref_id"] = req.RefID
|
|
args["idempotency_key"] = req.RefID
|
|
}
|
|
return args
|
|
}
|