27ca0a97c4
CI / Test and build (pull_request) Successful in 16s
Live Agentic get_equity_historicals returns data.results[].bars[] with open_price/high_price/low_price/close_price. parseHistoricals only read historicals[].data_points[] with short OHLC names, so CT113 saw bars_len=30 on the wire but 0 parsed bars and VWAP stayed 0 all day (insufficient_data mass skip). Accept results|historicals envelopes, bars|data_points arrays, and *_price aliases alongside short names. Fixture uses the live shape; legacy short-name envelope stays covered. Link: #18
682 lines
17 KiB
Go
682 lines
17 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
|
|
}
|
|
|
|
// Position is one holding from get_equity_positions.
|
|
type Position struct {
|
|
Symbol string
|
|
Qty decimal.Decimal
|
|
AvgCost decimal.Decimal
|
|
}
|
|
|
|
// PositionsResult is the parsed get_equity_positions payload.
|
|
type PositionsResult struct {
|
|
Positions []Position
|
|
NextCursor string
|
|
}
|
|
|
|
// TaxLotsRequest is the argument set for get_equity_tax_lots.
|
|
type TaxLotsRequest struct {
|
|
AccountNumber string
|
|
Symbol string
|
|
Cursor string
|
|
}
|
|
|
|
// TaxLotRow is one open tax lot from get_equity_tax_lots.
|
|
type TaxLotRow struct {
|
|
OpenLotID string
|
|
Quantity decimal.Decimal
|
|
CostBasis decimal.Decimal
|
|
AcquiredAt string
|
|
Term string
|
|
QuantityAvail decimal.Decimal
|
|
}
|
|
|
|
// TaxLotsResult is the parsed get_equity_tax_lots payload.
|
|
type TaxLotsResult struct {
|
|
Lots []TaxLotRow
|
|
NextCursor string
|
|
}
|
|
|
|
// 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 {
|
|
Orders []Order
|
|
NextCursor string
|
|
}
|
|
|
|
// TradabilityRequest is the argument set for get_equity_tradability.
|
|
type TradabilityRequest struct {
|
|
AccountNumber string
|
|
Symbols []string
|
|
}
|
|
|
|
// SymbolTradability is per-symbol eligibility from get_equity_tradability.
|
|
type SymbolTradability struct {
|
|
Symbol string
|
|
Tradable bool
|
|
Fractional bool
|
|
}
|
|
|
|
// TradabilityResult is the parsed get_equity_tradability payload.
|
|
type TradabilityResult struct {
|
|
Symbols []SymbolTradability
|
|
}
|
|
|
|
// 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
|
|
}
|
|
|
|
// Fundamentals is one symbol's facts from get_equity_fundamentals.
|
|
type Fundamentals struct {
|
|
Symbol string
|
|
AvgVolume decimal.Decimal
|
|
InstrumentKind string
|
|
MarketCap decimal.Decimal
|
|
PE decimal.Decimal
|
|
High52Week decimal.Decimal
|
|
Low52Week decimal.Decimal
|
|
}
|
|
|
|
// FundamentalsResult is the parsed get_equity_fundamentals payload.
|
|
type FundamentalsResult struct {
|
|
Fundamentals []Fundamentals
|
|
}
|
|
|
|
// PriceBookRequest is the argument set for get_equity_price_book.
|
|
type PriceBookRequest struct {
|
|
Symbols []string
|
|
}
|
|
|
|
// BookLevel is one bid or ask rung.
|
|
type BookLevel struct {
|
|
Price decimal.Decimal
|
|
Quantity decimal.Decimal
|
|
}
|
|
|
|
// PriceBook is a Level-2 snapshot for one symbol.
|
|
type PriceBook struct {
|
|
Symbol string
|
|
Bids []BookLevel
|
|
Asks []BookLevel
|
|
}
|
|
|
|
// PriceBookResult is the parsed get_equity_price_book payload.
|
|
type PriceBookResult struct {
|
|
Books []PriceBook
|
|
}
|
|
|
|
// 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
|
|
}
|
|
|
|
// IndicatorPoint is one computed indicator bar.
|
|
type IndicatorPoint struct {
|
|
Time time.Time
|
|
Value decimal.Decimal
|
|
Upper decimal.Decimal
|
|
Lower decimal.Decimal
|
|
MACD decimal.Decimal
|
|
Signal decimal.Decimal
|
|
}
|
|
|
|
// TechnicalIndicatorsResult is the parsed get_equity_technical_indicators payload.
|
|
type TechnicalIndicatorsResult struct {
|
|
Points []IndicatorPoint
|
|
}
|
|
|
|
// NewsRequest is the argument set for get_equity_news.
|
|
type NewsRequest struct {
|
|
Symbol string
|
|
Limit int
|
|
Cursor string
|
|
}
|
|
|
|
// NewsArticle is one article from get_equity_news.
|
|
type NewsArticle struct {
|
|
Title string
|
|
URL string
|
|
PublishedAt string
|
|
Source string
|
|
Summary string
|
|
}
|
|
|
|
// NewsResult is the parsed get_equity_news payload.
|
|
type NewsResult struct {
|
|
Articles []NewsArticle
|
|
NextCursor string
|
|
}
|
|
|
|
// 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"`
|
|
OpenPrice any `json:"open_price"`
|
|
High any `json:"high"`
|
|
HighPrice any `json:"high_price"`
|
|
Low any `json:"low"`
|
|
LowPrice any `json:"low_price"`
|
|
Close any `json:"close"`
|
|
ClosePrice any `json:"close_price"`
|
|
Volume any `json:"volume"`
|
|
Interpolated bool `json:"interpolated"`
|
|
}
|
|
|
|
type histSeriesJSON struct {
|
|
Symbol string `json:"symbol"`
|
|
DataPoints []histPointJSON `json:"data_points"`
|
|
Bars []histPointJSON `json:"bars"`
|
|
}
|
|
|
|
func parseHistoricals(raw json.RawMessage) ([]Bar, error) {
|
|
// Live Agentic wire (CT113): {"data":{"results":[{"symbol","bars":[{open_price,...}]}]}}.
|
|
// Fixture / older shapes: {"historicals":[{"symbol","data_points":[{open,...}]}]}.
|
|
rows, _, err := wire.UnmarshalRows[histSeriesJSON](raw, "results", "historicals")
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
var out []Bar
|
|
for _, series := range rows {
|
|
if series.Symbol == "" {
|
|
continue
|
|
}
|
|
points := series.Bars
|
|
if len(points) == 0 {
|
|
points = series.DataPoints
|
|
}
|
|
for _, p := range points {
|
|
ts, err := time.Parse(time.RFC3339, p.BeginsAt)
|
|
if err != nil {
|
|
continue
|
|
}
|
|
o, err := firstDec(p.OpenPrice, p.Open)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
h, err := firstDec(p.HighPrice, p.High)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
l, err := firstDec(p.LowPrice, p.Low)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
cl, err := firstDec(p.ClosePrice, 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
|
|
}
|