Files
robinhood-agentic-mcp/equity/read.go
T

586 lines
14 KiB
Go

package equity
import (
"context"
"encoding/json"
"time"
decimal "github.com/alpacahq/alpacadecimal"
"s1d3sw1ped/robinhood-agentic-mcp/client"
"s1d3sw1ped/robinhood-agentic-mcp/internal/wire"
)
const (
toolPositions = "get_equity_positions"
toolTaxLots = "get_equity_tax_lots"
toolQuotes = "get_equity_quotes"
toolOrders = "get_equity_orders"
toolTradability = "get_equity_tradability"
toolHistoricals = "get_equity_historicals"
toolFundamentals = "get_equity_fundamentals"
toolPriceBook = "get_equity_price_book"
toolTechnicalIndicators = "get_equity_technical_indicators"
toolNews = "get_equity_news"
)
// PositionsRequest is the argument set for get_equity_positions.
type PositionsRequest struct {
AccountNumber string
Cursor string
}
// PositionsResult is the parsed get_equity_positions payload.
type PositionsResult struct{}
// TaxLotsRequest is the argument set for get_equity_tax_lots.
type TaxLotsRequest struct {
AccountNumber string
Symbol string
Cursor string
}
// TaxLotsResult is the parsed get_equity_tax_lots payload.
type TaxLotsResult struct{}
// QuotesRequest is the argument set for get_equity_quotes.
type QuotesRequest struct {
Symbols []string
}
// Quote is a top-of-book print from get_equity_quotes.
type Quote struct {
Symbol string
Bid decimal.Decimal
Ask decimal.Decimal
Last decimal.Decimal
PrevClose decimal.Decimal
Volume decimal.Decimal
}
// QuotesResult is the parsed get_equity_quotes payload.
type QuotesResult struct {
Quotes []Quote
}
// OrdersRequest is the argument set for get_equity_orders.
type OrdersRequest struct {
AccountNumber string
OrderID string
State string
Symbol string
CreatedAtGTE string
PlacedAgent string
Cursor string
}
// OrdersResult is the parsed get_equity_orders payload.
type OrdersResult struct{}
// TradabilityRequest is the argument set for get_equity_tradability.
type TradabilityRequest struct {
AccountNumber string
Symbols []string
}
// TradabilityResult is the parsed get_equity_tradability payload.
type TradabilityResult struct{}
// HistoricalsRequest is the argument set for get_equity_historicals.
type HistoricalsRequest struct {
Symbols []string
StartTime time.Time
EndTime time.Time
Interval string
Bounds string
AdjustmentType string
}
// Bar is one equity historical candle from get_equity_historicals.
type Bar struct {
Symbol string
Time time.Time
Open, High, Low, Close decimal.Decimal
Volume decimal.Decimal
Interpolated bool
}
// HistoricalsResult is the parsed get_equity_historicals payload.
type HistoricalsResult struct {
Bars []Bar
}
// FundamentalsRequest is the argument set for get_equity_fundamentals.
type FundamentalsRequest struct {
Symbols []string
Bounds string
}
// FundamentalsResult is the parsed get_equity_fundamentals payload.
type FundamentalsResult struct{}
// PriceBookRequest is the argument set for get_equity_price_book.
type PriceBookRequest struct {
Symbols []string
}
// PriceBookResult is the parsed get_equity_price_book payload.
type PriceBookResult struct{}
// TechnicalIndicatorsRequest is the argument set for get_equity_technical_indicators.
type TechnicalIndicatorsRequest struct {
Symbol string
Type string
Interval string
StartTime time.Time
EndTime time.Time
Bounds string
AdjustmentType string
Output string
Period *int
NumStd *decimal.Decimal
FastPeriod *int
SlowPeriod *int
SignalPeriod *int
Multiplier *decimal.Decimal
Method string
}
// TechnicalIndicatorsResult is the parsed get_equity_technical_indicators payload.
type TechnicalIndicatorsResult struct{}
// NewsRequest is the argument set for get_equity_news.
type NewsRequest struct {
Symbol string
Limit int
Cursor string
}
// NewsResult is the parsed get_equity_news payload.
type NewsResult struct{}
// Positions calls get_equity_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.Cursor != "" {
args["cursor"] = req.Cursor
}
var out PositionsResult
if err := c.parse(ctx, toolPositions, args, &out); err != nil {
return PositionsResult{}, err
}
return out, nil
}
// TaxLots calls get_equity_tax_lots.
func (c *Client) TaxLots(ctx context.Context, req TaxLotsRequest) (TaxLotsResult, error) {
args := map[string]any{}
if req.AccountNumber != "" {
args["account_number"] = req.AccountNumber
}
if req.Symbol != "" {
args["symbol"] = req.Symbol
}
if req.Cursor != "" {
args["cursor"] = req.Cursor
}
var out TaxLotsResult
if err := c.parse(ctx, toolTaxLots, args, &out); err != nil {
return TaxLotsResult{}, err
}
return out, nil
}
// Quotes calls get_equity_quotes.
func (c *Client) Quotes(ctx context.Context, req QuotesRequest) (QuotesResult, error) {
args := map[string]any{}
if len(req.Symbols) > 0 {
args["symbols"] = req.Symbols
}
raw, err := c.c.Call(ctx, toolQuotes, args)
if err != nil {
return QuotesResult{}, err
}
quotes, err := parseQuotes(raw)
if err != nil {
return QuotesResult{}, client.ToolErrorf(toolQuotes, "parse: %w", err)
}
return QuotesResult{Quotes: quotes}, nil
}
// Orders calls get_equity_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.Symbol != "" {
args["symbol"] = req.Symbol
}
if req.CreatedAtGTE != "" {
args["created_at_gte"] = req.CreatedAtGTE
}
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
}
// Tradability calls get_equity_tradability.
func (c *Client) Tradability(ctx context.Context, req TradabilityRequest) (TradabilityResult, error) {
args := map[string]any{}
if req.AccountNumber != "" {
args["account_number"] = req.AccountNumber
}
if len(req.Symbols) > 0 {
args["symbols"] = req.Symbols
}
var out TradabilityResult
if err := c.parse(ctx, toolTradability, args, &out); err != nil {
return TradabilityResult{}, err
}
return out, nil
}
// Historicals calls get_equity_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.Symbols) > 0 {
args["symbols"] = req.Symbols
}
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
}
if req.AdjustmentType != "" {
args["adjustment_type"] = req.AdjustmentType
}
raw, err := c.c.Call(ctx, toolHistoricals, args)
if err != nil {
return HistoricalsResult{}, err
}
bars, err := parseHistoricals(raw)
if err != nil {
return HistoricalsResult{}, client.ToolErrorf(toolHistoricals, "parse: %w", err)
}
return HistoricalsResult{Bars: bars}, nil
}
// Fundamentals calls get_equity_fundamentals.
func (c *Client) Fundamentals(ctx context.Context, req FundamentalsRequest) (FundamentalsResult, error) {
args := map[string]any{}
if len(req.Symbols) > 0 {
args["symbols"] = req.Symbols
}
if req.Bounds != "" {
args["bounds"] = req.Bounds
}
var out FundamentalsResult
if err := c.parse(ctx, toolFundamentals, args, &out); err != nil {
return FundamentalsResult{}, err
}
return out, nil
}
// PriceBook calls get_equity_price_book.
func (c *Client) PriceBook(ctx context.Context, req PriceBookRequest) (PriceBookResult, error) {
args := map[string]any{}
if len(req.Symbols) > 0 {
args["symbols"] = req.Symbols
}
var out PriceBookResult
if err := c.parse(ctx, toolPriceBook, args, &out); err != nil {
return PriceBookResult{}, err
}
return out, nil
}
// TechnicalIndicators calls get_equity_technical_indicators.
func (c *Client) TechnicalIndicators(ctx context.Context, req TechnicalIndicatorsRequest) (TechnicalIndicatorsResult, error) {
args := map[string]any{
"start_time": req.StartTime.UTC().Format(time.RFC3339),
}
if req.Symbol != "" {
args["symbol"] = req.Symbol
}
if req.Type != "" {
args["type"] = req.Type
}
if req.Interval != "" {
args["interval"] = req.Interval
}
if !req.EndTime.IsZero() {
args["end_time"] = req.EndTime.UTC().Format(time.RFC3339)
}
if req.Bounds != "" {
args["bounds"] = req.Bounds
}
if req.AdjustmentType != "" {
args["adjustment_type"] = req.AdjustmentType
}
if req.Output != "" {
args["output"] = req.Output
}
if req.Period != nil {
args["period"] = *req.Period
}
if req.NumStd != nil {
args["num_std"] = json.Number(wire.Encode(*req.NumStd))
}
if req.FastPeriod != nil {
args["fast_period"] = *req.FastPeriod
}
if req.SlowPeriod != nil {
args["slow_period"] = *req.SlowPeriod
}
if req.SignalPeriod != nil {
args["signal_period"] = *req.SignalPeriod
}
if req.Multiplier != nil {
args["multiplier"] = json.Number(wire.Encode(*req.Multiplier))
}
if req.Method != "" {
args["method"] = req.Method
}
var out TechnicalIndicatorsResult
if err := c.parse(ctx, toolTechnicalIndicators, args, &out); err != nil {
return TechnicalIndicatorsResult{}, err
}
return out, nil
}
// News calls get_equity_news.
func (c *Client) News(ctx context.Context, req NewsRequest) (NewsResult, error) {
args := map[string]any{}
if req.Symbol != "" {
args["symbol"] = req.Symbol
}
if req.Limit != 0 {
args["limit"] = req.Limit
}
if req.Cursor != "" {
args["cursor"] = req.Cursor
}
var out NewsResult
if err := c.parse(ctx, toolNews, args, &out); err != nil {
return NewsResult{}, 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
}
type quoteFields struct {
Symbol string `json:"symbol"`
Last any `json:"last"`
LastTradePrice any `json:"last_trade_price"`
LastNonRegTradePrice any `json:"last_non_reg_trade_price"`
Bid any `json:"bid"`
BidPrice any `json:"bid_price"`
Ask any `json:"ask"`
AskPrice any `json:"ask_price"`
PreviousClose any `json:"previous_close"`
AdjustedPreviousClose any `json:"adjusted_previous_close"`
Volume any `json:"volume"`
}
type quoteRow struct {
Quote *quoteFields `json:"quote"`
Close *struct {
Symbol string `json:"symbol"`
Price any `json:"price"`
} `json:"close"`
quoteFields
}
func (r quoteRow) asQuote() (Quote, bool, error) {
f := r.quoteFields
if r.Quote != nil {
f = *r.Quote
}
if f.Symbol == "" && r.Close != nil {
f.Symbol = r.Close.Symbol
}
if f.Symbol == "" {
return Quote{}, false, nil
}
var closePx any
if r.Close != nil {
closePx = r.Close.Price
}
last, err := firstDec(f.LastTradePrice, f.Last, f.LastNonRegTradePrice)
if err != nil {
return Quote{}, false, err
}
prev, err := firstDec(closePx, f.PreviousClose, f.AdjustedPreviousClose)
if err != nil {
return Quote{}, false, err
}
bid, err := firstDec(f.BidPrice, f.Bid)
if err != nil {
return Quote{}, false, err
}
ask, err := firstDec(f.AskPrice, f.Ask)
if err != nil {
return Quote{}, false, err
}
vol, err := firstDec(f.Volume)
if err != nil {
return Quote{}, false, err
}
return Quote{
Symbol: f.Symbol,
Last: last,
PrevClose: prev,
Bid: bid,
Ask: ask,
Volume: vol,
}, true, nil
}
func parseQuotes(raw json.RawMessage) ([]Quote, error) {
var wrap struct {
Quotes []quoteRow `json:"quotes"`
Results []quoteRow `json:"results"`
}
if err := json.Unmarshal(wire.Unwrap(raw), &wrap); err != nil {
return nil, err
}
out := make([]Quote, 0, len(wrap.Quotes)+len(wrap.Results))
var err error
out, err = appendQuotes(out, wrap.Quotes)
if err != nil {
return nil, err
}
out, err = appendQuotes(out, wrap.Results)
if err != nil {
return nil, err
}
return out, nil
}
func appendQuotes(dst []Quote, rows []quoteRow) ([]Quote, error) {
for _, row := range rows {
q, ok, err := row.asQuote()
if err != nil {
return nil, err
}
if !ok {
continue
}
dst = append(dst, q)
}
return dst, nil
}
type histPointJSON struct {
BeginsAt string `json:"begins_at"`
Open any `json:"open"`
High any `json:"high"`
Low any `json:"low"`
Close any `json:"close"`
Volume any `json:"volume"`
Interpolated bool `json:"interpolated"`
}
type histSeriesJSON struct {
Symbol string `json:"symbol"`
DataPoints []histPointJSON `json:"data_points"`
}
func parseHistoricals(raw json.RawMessage) ([]Bar, error) {
var wrap struct {
Historicals []histSeriesJSON `json:"historicals"`
}
if err := json.Unmarshal(wire.Unwrap(raw), &wrap); err != nil {
return nil, err
}
var out []Bar
for _, series := range wrap.Historicals {
if series.Symbol == "" {
continue
}
for _, p := range series.DataPoints {
ts, err := time.Parse(time.RFC3339, p.BeginsAt)
if err != nil {
continue
}
o, err := firstDec(p.Open)
if err != nil {
return nil, err
}
h, err := firstDec(p.High)
if err != nil {
return nil, err
}
l, err := firstDec(p.Low)
if err != nil {
return nil, err
}
cl, err := firstDec(p.Close)
if err != nil {
return nil, err
}
vol, err := firstDec(p.Volume)
if err != nil {
return nil, err
}
out = append(out, Bar{
Symbol: series.Symbol,
Time: ts,
Open: o,
High: h,
Low: l,
Close: cl,
Volume: vol,
Interpolated: p.Interpolated,
})
}
}
return out, nil
}
func firstDec(vs ...any) (decimal.Decimal, error) {
for _, v := range vs {
if v == nil {
continue
}
if s, ok := v.(string); ok && s == "" {
continue
}
return wire.Dec(v)
}
return decimal.Zero, nil
}