├── .gitignore ├── doc.go ├── go.mod ├── log.go ├── go.sum ├── rate_limit.go ├── time.go ├── demux.go ├── tcli └── main.go ├── stream.go ├── README.md ├── interface.go ├── account.go ├── LICENSE ├── client.go └── fundamentals.go /.gitignore: -------------------------------------------------------------------------------- 1 | tcli/tcli 2 | -------------------------------------------------------------------------------- /doc.go: -------------------------------------------------------------------------------- 1 | // Package tradier implements a Client for using the Tradier API. 2 | package tradier 3 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/timpalpant/go-tradier 2 | 3 | go 1.15 4 | 5 | require ( 6 | github.com/cenkalti/backoff v2.0.0+incompatible 7 | github.com/pkg/errors v0.8.0 8 | golang.org/x/net v0.0.0-20180921000356-2f5d2388922f // indirect 9 | ) 10 | -------------------------------------------------------------------------------- /log.go: -------------------------------------------------------------------------------- 1 | package tradier 2 | 3 | import ( 4 | "io/ioutil" 5 | "log" 6 | ) 7 | 8 | type StdLogger interface { 9 | Print(v ...interface{}) 10 | Printf(format string, v ...interface{}) 11 | Println(v ...interface{}) 12 | } 13 | 14 | var Logger StdLogger = log.New(ioutil.Discard, "[go-tradier] ", log.LstdFlags) 15 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/cenkalti/backoff v2.0.0+incompatible h1:5IIPUHhlnUZbcHQsQou5k1Tn58nJkeJL9U+ig5CHJbY= 2 | github.com/cenkalti/backoff v2.0.0+incompatible/go.mod h1:90ReRw6GdpyfrHakVjL/QHaoyV4aDUVVkXQJJJ3NXXM= 3 | github.com/pkg/errors v0.8.0 h1:WdK/asTD0HN+q6hsWO3/vpuAkAr+tw6aNJNDFFf0+qw= 4 | github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 5 | golang.org/x/net v0.0.0-20180921000356-2f5d2388922f h1:QM2QVxvDoW9PFSPp/zy9FgxJLfaWTZlS61KEPtBwacM= 6 | golang.org/x/net v0.0.0-20180921000356-2f5d2388922f/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 7 | -------------------------------------------------------------------------------- /rate_limit.go: -------------------------------------------------------------------------------- 1 | package tradier 2 | 3 | import ( 4 | "net/http" 5 | "strconv" 6 | "strings" 7 | "time" 8 | ) 9 | 10 | // Extract quota violation expiration from body message. 11 | func parseQuotaViolationExpiration(body string) time.Time { 12 | if !strings.HasPrefix(body, "Quota Violation") { 13 | return time.Time{} 14 | } 15 | 16 | parts := strings.Fields(body) 17 | ms, err := strconv.ParseInt(parts[len(parts)-1], 10, 64) 18 | if err != nil { 19 | return time.Time{} 20 | } 21 | 22 | return time.Unix(ms/1000, 0) 23 | } 24 | 25 | // Attempt to extract the rate-limit expiry time if we have exceeded the rate limit available. 26 | func getRateLimitExpiration(h http.Header) time.Time { 27 | rateLimit, err := strconv.ParseInt(h.Get(rateLimitAvailable), 10, 64) 28 | if err != nil { 29 | return time.Time{} 30 | } 31 | 32 | rateLimitExpiry, err := strconv.ParseInt(h.Get(rateLimitExpiry), 10, 64) 33 | if err != nil { 34 | return time.Time{} 35 | } 36 | 37 | if rateLimit == 0 && rateLimitExpiry > 0 { 38 | return time.Unix(rateLimitExpiry, 0) 39 | } 40 | 41 | return time.Time{} 42 | } 43 | -------------------------------------------------------------------------------- /time.go: -------------------------------------------------------------------------------- 1 | package tradier 2 | 3 | import ( 4 | "strconv" 5 | "time" 6 | ) 7 | 8 | // DateTime wraps time.Time and adds flexible implementations for unmarshaling 9 | // JSON in the different forms it appears in the Tradier API. 10 | type DateTime struct { 11 | time.Time 12 | } 13 | 14 | func (d *DateTime) Set(s string) error { 15 | if s == "null" { 16 | return nil 17 | } 18 | 19 | t, err := time.Parse(time.RFC3339, s) 20 | if err == nil { 21 | *d = DateTime{t} 22 | return nil 23 | } 24 | 25 | // Date and time. 26 | t, err = time.Parse("2006-01-02T15:04:05", s) 27 | if err == nil { 28 | *d = DateTime{t} 29 | return nil 30 | } 31 | 32 | // Just the date 33 | t, err = time.Parse("2006-01-02", s) 34 | if err == nil { 35 | *d = DateTime{t} 36 | return nil 37 | } 38 | 39 | // Just the hour 40 | t, err = time.Parse("15:04", s) 41 | if err == nil { 42 | *d = DateTime{t} 43 | return nil 44 | } 45 | 46 | // Milliseconds since the Unix epoch. 47 | t, err = ParseTimeMs(s) 48 | if err == nil { 49 | *d = DateTime{t} 50 | return nil 51 | } 52 | 53 | return err 54 | } 55 | 56 | func (d *DateTime) UnmarshalJSON(b []byte) error { 57 | if b[0] == '"' && b[len(b)-1] == '"' { 58 | b = b[1 : len(b)-1] 59 | } 60 | s := string(b) 61 | 62 | return d.Set(s) 63 | } 64 | 65 | func ParseTimeMs(tsMs string) (time.Time, error) { 66 | msecs, err := strconv.ParseInt(tsMs, 10, 64) 67 | if err != nil { 68 | return time.Time{}, err 69 | } 70 | secs := msecs / 1000 71 | nsecs := 1000000 * (msecs % 1000) 72 | t := time.Unix(secs, nsecs) 73 | return t, nil 74 | } 75 | -------------------------------------------------------------------------------- /demux.go: -------------------------------------------------------------------------------- 1 | package tradier 2 | 3 | import ( 4 | "github.com/pkg/errors" 5 | ) 6 | 7 | // StreamDemuxer demuxes the different types of messages in a market events stream. 8 | type StreamDemuxer struct { 9 | Quotes func(quote *QuoteEvent) 10 | Trades func(trade *TradeEvent) 11 | Summaries func(summary *SummaryEvent) 12 | TimeSales func(timeSale *TimeSaleEvent) 13 | Errors func(err error) 14 | } 15 | 16 | func (sd *StreamDemuxer) Handle(event *StreamEvent) { 17 | switch { 18 | case event.Type == "quote": 19 | sd.handleQuote(event) 20 | case event.Type == "trade": 21 | sd.handleTrade(event) 22 | case event.Type == "timesale": 23 | sd.handleTimeSale(event) 24 | case event.Type == "summary": 25 | sd.handleSummary(event) 26 | } 27 | } 28 | 29 | func (sd *StreamDemuxer) HandleChan(events <-chan *StreamEvent) { 30 | for event := range events { 31 | sd.Handle(event) 32 | } 33 | } 34 | 35 | func (sd *StreamDemuxer) handleQuote(m *StreamEvent) { 36 | if sd.Quotes != nil { 37 | if q, err := DecodeQuote(m); err == nil { 38 | sd.Quotes(q) 39 | } else { 40 | sd.Errors(errors.Wrapf(err, "error decoding quote: %v", string(m.Message))) 41 | } 42 | } 43 | } 44 | 45 | func (sd *StreamDemuxer) handleTrade(m *StreamEvent) { 46 | if sd.Trades != nil { 47 | if t, err := DecodeTrade(m); err == nil { 48 | sd.Trades(t) 49 | } else { 50 | sd.Errors(errors.Wrapf(err, "error decoding trade: %v", string(m.Message))) 51 | } 52 | } 53 | } 54 | 55 | func (sd *StreamDemuxer) handleSummary(m *StreamEvent) { 56 | if sd.Summaries != nil { 57 | if s, err := DecodeSummary(m); err == nil { 58 | sd.Summaries(s) 59 | } else { 60 | sd.Errors(errors.Wrapf(err, "error decoding summary: %v", string(m.Message))) 61 | } 62 | } 63 | } 64 | 65 | func (sd *StreamDemuxer) handleTimeSale(m *StreamEvent) { 66 | if sd.TimeSales != nil { 67 | if ts, err := DecodeTimeSale(m); err == nil { 68 | sd.TimeSales(ts) 69 | } else { 70 | sd.Errors(errors.Wrapf(err, "error decoding time sale: %v", string(m.Message))) 71 | } 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /tcli/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "flag" 5 | "fmt" 6 | "log" 7 | 8 | "github.com/timpalpant/go-tradier" 9 | ) 10 | 11 | func showPositions(client *tradier.Client) { 12 | fmt.Println("Fetching current positions") 13 | positions, err := client.GetAccountPositions() 14 | if err != nil { 15 | log.Fatal(err) 16 | } 17 | 18 | if len(positions) == 0 { 19 | fmt.Println("No current positions") 20 | return 21 | } 22 | 23 | for _, p := range positions { 24 | fmt.Printf("%v (%v): %v shares, $ %.2f\n", 25 | p.Symbol, p.DateAcquired, p.Quantity, p.CostBasis) 26 | } 27 | } 28 | 29 | func gainLoss(client *tradier.Client) { 30 | fmt.Println("Fetching gain loss") 31 | cps, err := client.GetAccountCostBasis() 32 | if err != nil { 33 | log.Fatal(err) 34 | } 35 | 36 | if len(cps) == 0 { 37 | fmt.Println("No closed positions") 38 | return 39 | } 40 | 41 | for _, cp := range cps { 42 | fmt.Printf( 43 | "%v: open: %v, close: %v, quantity: %v, cost: $ %.2f, "+ 44 | "proceeds: $ %.2f, gain-loss: $ %.2f, gain-loss percent: %.2f %%\n", 45 | cp.Symbol, cp.OpenDate.Time, cp.CloseDate.Time, 46 | cp.Quantity, cp.Cost, cp.Proceeds, cp.GainLoss, cp.GainLossPercent) 47 | } 48 | } 49 | 50 | func openOrders(client *tradier.Client) { 51 | fmt.Println("Fetching open orders") 52 | openOrders, err := client.GetOpenOrders() 53 | if err != nil { 54 | log.Fatal(err) 55 | } 56 | if len(openOrders) == 0 { 57 | fmt.Println("No open orders") 58 | return 59 | } 60 | 61 | var bought, sold, fees float64 62 | for _, o := range openOrders { 63 | fmt.Printf( 64 | "id: %v, type: %v, symbol: %v, side: %v, quantity: %v, status: %v\n", 65 | o.Id, o.Type, o.Symbol, o.Side, o.Quantity, o.Status) 66 | 67 | costBasis := o.ExecutedQuantity * o.AverageFillPrice 68 | switch o.Side { 69 | case tradier.Buy: 70 | bought += costBasis 71 | case tradier.Sell: 72 | sold += costBasis 73 | } 74 | 75 | if o.ExecutedQuantity > 0 { 76 | fees += 1.0 77 | } 78 | } 79 | 80 | dailyPL := sold - bought - fees 81 | fmt.Printf( 82 | "Daily PL = $ %.2f, (%.1f %% of $ %.2f invested)\n", 83 | dailyPL, 100*dailyPL/bought, bought) 84 | } 85 | 86 | func history(client *tradier.Client) { 87 | fmt.Println("Fetching account history") 88 | events, err := client.GetAccountHistory(1000) 89 | if err != nil { 90 | log.Fatal(err) 91 | } 92 | 93 | for _, e := range events { 94 | fmt.Printf("%v - %v - %.2f\n", e.Date, e.Type, e.Amount) 95 | switch e.Type { 96 | case "trade": 97 | fmt.Printf("\t%v - %v - %v shares, $ %.2f, commission = $ %.2f, trade type = %v\n", 98 | e.Trade.Symbol, e.Trade.Description, e.Trade.Quantity, 99 | e.Trade.Price, e.Trade.Commission, e.Trade.TradeType) 100 | case "adjustment": 101 | default: 102 | fmt.Printf("unknown event type: %v\n", e.Type) 103 | } 104 | } 105 | } 106 | 107 | func main() { 108 | subcommand := flag.String("command", "positions", "Command to run (positions, gainloss, openorders)") 109 | apiKey := flag.String("tradier.apikey", "", "Tradier API key") 110 | account := flag.String("tradier.account", "", "Tradier account ID") 111 | flag.Parse() 112 | 113 | params := tradier.DefaultParams(*apiKey) 114 | client := tradier.NewClient(params) 115 | client.SelectAccount(*account) 116 | 117 | switch *subcommand { 118 | case "positions": 119 | showPositions(client) 120 | case "gainloss": 121 | gainLoss(client) 122 | case "openorders": 123 | openOrders(client) 124 | case "history": 125 | history(client) 126 | default: 127 | log.Fatal("unknown command: ", *subcommand) 128 | } 129 | } 130 | -------------------------------------------------------------------------------- /stream.go: -------------------------------------------------------------------------------- 1 | package tradier 2 | 3 | import ( 4 | "bufio" 5 | "encoding/json" 6 | "io" 7 | ) 8 | 9 | // StreamEvent is used to unmarshal stream events before they are demuxed. 10 | // Message contains the remainder of the type-specific message. 11 | // 12 | // StreamEvents can be demuxed into type-specific events using 13 | // the StreamDemuxer. 14 | type StreamEvent struct { 15 | Type string 16 | Symbol string 17 | Message json.RawMessage 18 | Error error 19 | } 20 | 21 | func UnmarshalStreamEvent(buf []byte, se *StreamEvent) error { 22 | se.Message = make([]byte, len(buf)) 23 | copy(se.Message, buf) 24 | se.Error = json.Unmarshal(buf, se) 25 | return se.Error 26 | } 27 | 28 | type QuoteEvent struct { 29 | Symbol string 30 | Bid float64 31 | BidSize int64 `json:"bidsz"` 32 | BidExchange string `json:"bidexch"` 33 | BidDateMs int64 `json:"biddate,string"` 34 | Ask float64 35 | AskSize int64 `json:"asksz"` 36 | AskExchange string `json:"askexch"` 37 | AskDateMs int64 `json:"askdate,string"` 38 | } 39 | 40 | type TimeSaleEvent struct { 41 | Symbol string 42 | Exchange string `json:"exch"` 43 | Bid float64 `json:",string"` 44 | Ask float64 `json:",string"` 45 | Last float64 `json:",string"` 46 | Size int64 `json:",string"` 47 | DateMs int64 `json:"date,string"` 48 | Seq int64 49 | Flag string 50 | Cancel bool 51 | Correction bool 52 | Session string 53 | } 54 | 55 | type TradeEvent struct { 56 | Symbol string 57 | Exchange string `json:"exch"` 58 | Price float64 `json:",string"` 59 | Last float64 `json:",string"` 60 | Size int64 `json:",string"` 61 | CumulativeVolume int64 `json:"cvol,string"` 62 | DateMs int64 `json:"date,string"` 63 | } 64 | 65 | type SummaryEvent struct { 66 | Symbol string 67 | Open float64 `json:",string"` 68 | High float64 `json:",string"` 69 | Low float64 `json:",string"` 70 | PreviousClose float64 `json:"prevClose,string"` 71 | } 72 | 73 | // MarketEventStream scans the newline-delimited market stream 74 | // sent by Tradier and decodes each event into a StreamEvent. 75 | type MarketEventStream struct { 76 | // A message on this channel indicates to the http consumer to shutdown the stream. 77 | // All channels will be closed by the goroutine that owns this stream. 78 | closeChan chan struct{} 79 | } 80 | 81 | func NewMarketEventStream(input io.ReadCloser, output chan *StreamEvent) *MarketEventStream { 82 | mes := &MarketEventStream{ 83 | closeChan: make(chan struct{}), 84 | } 85 | go mes.consumeEvents(input, output) 86 | return mes 87 | } 88 | 89 | func (mes *MarketEventStream) Stop() { 90 | close(mes.closeChan) 91 | } 92 | 93 | func (mes *MarketEventStream) consumeEvents( 94 | input io.ReadCloser, 95 | output chan *StreamEvent) { 96 | scanner := bufio.NewScanner(input) 97 | defer input.Close() 98 | defer close(output) 99 | 100 | for scanner.Scan() { 101 | event := &StreamEvent{} 102 | if err := UnmarshalStreamEvent(scanner.Bytes(), event); err != nil { 103 | Logger.Println(err) 104 | } 105 | 106 | select { 107 | case output <- event: 108 | case <-mes.closeChan: 109 | return 110 | default: 111 | Logger.Println("stream output channel is full, dropping stream event") 112 | } 113 | } 114 | 115 | if err := scanner.Err(); err != nil { 116 | Logger.Println(err) 117 | } 118 | } 119 | 120 | func DecodeQuote(e *StreamEvent) (*QuoteEvent, error) { 121 | q := &QuoteEvent{Symbol: e.Symbol} 122 | err := json.Unmarshal(e.Message, q) 123 | return q, err 124 | } 125 | 126 | func DecodeTrade(e *StreamEvent) (*TradeEvent, error) { 127 | t := &TradeEvent{Symbol: e.Symbol} 128 | err := json.Unmarshal(e.Message, t) 129 | return t, err 130 | } 131 | 132 | func DecodeTimeSale(e *StreamEvent) (*TimeSaleEvent, error) { 133 | ts := &TimeSaleEvent{Symbol: e.Symbol} 134 | err := json.Unmarshal(e.Message, ts) 135 | return ts, err 136 | } 137 | 138 | func DecodeSummary(e *StreamEvent) (*SummaryEvent, error) { 139 | s := &SummaryEvent{Symbol: e.Symbol} 140 | err := json.Unmarshal(e.Message, s) 141 | return s, err 142 | } 143 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # go-tradier 2 | A Go library for accessing the Tradier Developer API. 3 | 4 | [![GoDoc](https://godoc.org/github.com/timpalpant/go-tradier?status.svg)](http://godoc.org/github.com/timpalpant/go-tradier) 5 | [![Build Status](https://travis-ci.org/timpalpant/go-tradier.svg?branch=master)](https://travis-ci.org/timpalpant/go-tradier) 6 | [![Coverage Status](https://coveralls.io/repos/timpalpant/go-tradier/badge.svg?branch=master&service=github)](https://coveralls.io/github/timpalpant/go-tradier?branch=master) 7 | 8 | go-tradier is a library to access the [Tradier Developer API](https://developer.tradier.com/documentation) from [Go](http://www.golang.org). 9 | It provides a thin wrapper for working with the JSON REST endpoints. 10 | 11 | [Tradier](https://tradier.com) is the first Brokerage API company, powering the world's leading 12 | trading, investing and digital advisor platforms. Tradier is not affiliated 13 | and does not endorse or recommend this library. 14 | 15 | ## Usage 16 | 17 | ### tcli 18 | 19 | The `tcli` tool is a small command-line interface for making requests. 20 | 21 | ```shell 22 | $ go install github.com/timpalpant/go-tradier/tcli 23 | $ tcli -tradier.account XXXXX -tradier.apikey XXXXX -command positions 24 | ``` 25 | 26 | ### Fetch real-time top-of-book quotes 27 | 28 | ```Go 29 | package main 30 | 31 | import ( 32 | "fmt" 33 | 34 | "github.com/timpalpant/go-tradier" 35 | ) 36 | 37 | func main() { 38 | params := tradier.DefaultParams("your-api-key-here") 39 | client := tradier.NewClient(params) 40 | 41 | quotes, err := client.GetQuotes([]string{"AAPL", "SPY"}) 42 | if err != nil { 43 | panic(err) 44 | } 45 | 46 | for _, quote := range quotes { 47 | fmt.Printf("%v: bid $%.02f (%v shares), ask $%.02f (%v shares)\n", 48 | quote.Symbol, quote.Bid, quote.BidSize, quote.Ask, quote.AskSize) 49 | } 50 | } 51 | ``` 52 | 53 | ### Stream real-time top-of-book trades and quotes (L1 TAQ) data. 54 | 55 | ```Go 56 | package main 57 | 58 | import ( 59 | "fmt" 60 | 61 | "github.com/timpalpant/go-tradier" 62 | ) 63 | 64 | func main() { 65 | params := tradier.DefaultParams("your-api-key-here") 66 | client := tradier.NewClient(params) 67 | 68 | eventsReader, err := client.StreamMarketEvents( 69 | []string{"AAPL", "SPY"}, 70 | []tradier.Filter{tradier.FilterQuote, tradier.FilterTrade}) 71 | if err != nil { 72 | panic(err) 73 | } 74 | 75 | eventsCh := make(chan *tradier.StreamEvent) 76 | eventStream := tradier.NewMarketEventStream(eventsReader, eventsCh) 77 | defer eventStream.Stop() 78 | 79 | demuxer := tradier.StreamDemuxer{ 80 | Quotes: func(quote *tradier.QuoteEvent) { 81 | fmt.Printf("QUOTE %v: bid $%.02f (%v shares), ask $%.02f (%v shares)\n", 82 | quote.Symbol, quote.Bid, quote.BidSize, quote.Ask, quote.AskSize) 83 | }, 84 | Trades: func(trade *tradier.TradeEvent) { 85 | fmt.Printf("TRADE %v: $%.02f (%v shares) at %v\n", 86 | trade.Symbol, trade.Price, trade.Size, trade.DateMs) 87 | }, 88 | } 89 | 90 | demuxer.HandleChan(eventsCh) 91 | } 92 | ``` 93 | 94 | ### Place and then cancel an order for SPY. 95 | 96 | ```Go 97 | package main 98 | 99 | import ( 100 | "fmt" 101 | "time" 102 | 103 | "github.com/timpalpant/go-tradier" 104 | ) 105 | 106 | func main() { 107 | params := tradier.DefaultParams("your-api-key-here") 108 | client := tradier.NewClient(params) 109 | client.SelectAccount("your-account-id-here") 110 | 111 | // Place a limit order for 1 share of SPY at $1.00. 112 | orderId, err := client.PlaceOrder(tradier.Order{ 113 | Class: tradier.Equity, 114 | Type: tradier.LimitOrder, 115 | Symbol: "SPY", 116 | Side: tradier.Buy, 117 | Quantity: 1, 118 | Price: 1.00, 119 | Duration: tradier.Day, 120 | }) 121 | if err != nil { 122 | panic(err) 123 | } 124 | fmt.Printf("Placed order: %v\n", orderId) 125 | 126 | time.Sleep(2 * time.Second) 127 | order, err := client.GetOrderStatus(orderId) 128 | if err != nil { 129 | panic(err) 130 | } 131 | fmt.Printf("Order status: %v\n", order.Status) 132 | 133 | // Cancel the order. 134 | fmt.Printf("Canceling order: %v\n", orderId) 135 | if err := client.CancelOrder(orderId); err != nil { 136 | panic(err) 137 | } 138 | } 139 | ``` 140 | 141 | ## Contributing 142 | 143 | Pull requests and issues are welcomed! 144 | 145 | ## License 146 | 147 | go-tradier is released under the [GNU Lesser General Public License, Version 3.0](https://www.gnu.org/licenses/lgpl-3.0.en.html) 148 | -------------------------------------------------------------------------------- /interface.go: -------------------------------------------------------------------------------- 1 | package tradier 2 | 3 | import ( 4 | "database/sql/driver" 5 | "encoding/json" 6 | "fmt" 7 | "math" 8 | "strconv" 9 | "time" 10 | ) 11 | 12 | const ( 13 | SandboxEndpoint = "https://sandbox.tradier.com" 14 | APIEndpoint = "https://api.tradier.com" 15 | StreamEndpoint = "https://stream.tradier.com" 16 | ) 17 | 18 | type MarketState string 19 | 20 | const ( 21 | MarketPremarket MarketState = "premarket" 22 | MarketOpen MarketState = "open" 23 | MarketPostmarket MarketState = "postmarket" 24 | MarketClosed MarketState = "closed" 25 | ) 26 | 27 | type Interval string 28 | 29 | const ( 30 | IntervalTick Interval = "tick" 31 | IntervalMinute Interval = "1min" 32 | Interval5Min Interval = "5min" 33 | Interval15Min Interval = "15min" 34 | IntervalDaily Interval = "daily" 35 | IntervalWeekly Interval = "weekly" 36 | IntervalMonthly Interval = "monthly" 37 | ) 38 | 39 | type Filter string 40 | 41 | const ( 42 | FilterTrade Filter = "trade" 43 | FilterQuote Filter = "quote" 44 | FilterTimeSale Filter = "timesale" 45 | FilterSummary Filter = "summary" 46 | ) 47 | 48 | type SecurityType string 49 | 50 | const ( 51 | SecurityTypeStock SecurityType = "stock" 52 | SecurityTypeIndex SecurityType = "index" 53 | SecurityTypeETF SecurityType = "etf" 54 | SecurityTypeMutualFund SecurityType = "mutual_fund" 55 | ) 56 | 57 | var OldestDailyDate = time.Date(1980, time.January, 1, 0, 0, 0, 0, time.UTC) 58 | 59 | type TradierError struct { 60 | Fault struct { 61 | FaultString string 62 | Detail struct { 63 | ErrorCode string 64 | } 65 | } 66 | HttpStatusCode int 67 | Message string 68 | } 69 | 70 | func (te TradierError) Error() string { 71 | return fmt.Sprintf("%d: %s - %s", te.HttpStatusCode, te.Fault.FaultString, te.Message) 72 | } 73 | 74 | type Security struct { 75 | Symbol string 76 | Exchange string 77 | Type string 78 | Description string 79 | } 80 | 81 | type FloatOrNaN float64 82 | 83 | func (f *FloatOrNaN) UnmarshalJSON(data []byte) error { 84 | var x float64 85 | var err error 86 | if err = json.Unmarshal(data, &x); err == nil { 87 | *f = FloatOrNaN(x) 88 | return nil 89 | } 90 | 91 | // Fallback for "NaN" values. 92 | var s string 93 | if strErr := json.Unmarshal(data, &s); strErr == nil { 94 | x, err = strconv.ParseFloat(s, 64) 95 | *f = FloatOrNaN(x) 96 | } 97 | 98 | return err 99 | } 100 | 101 | func (f FloatOrNaN) Value() (driver.Value, error) { 102 | if math.IsNaN(float64(f)) { 103 | return nil, nil 104 | } 105 | 106 | return float64(f), nil 107 | } 108 | 109 | type Quote struct { 110 | Symbol string 111 | Description string 112 | Exchange string `json:"exch"` 113 | Type string 114 | Change float64 115 | ChangePercentage float64 `json:"change_percentage"` 116 | Volume int 117 | AverageVolume int 118 | Last float64 119 | LastVolume int 120 | TradeDate DateTime `json:"trade_date"` 121 | Open float64 122 | High float64 123 | Low float64 124 | Close float64 125 | PreviousClose float64 `json:"prevclose"` 126 | Week52High float64 `json:"week_52_high"` 127 | Week52Low float64 `json:"week_52_low"` 128 | Bid float64 129 | BidSize int 130 | BidExchange string `json:"bidexch"` 131 | BidDate DateTime `json:"bid_date"` 132 | Ask float64 133 | AskSize int 134 | AskExchange string `json:"askexch"` 135 | AskDate DateTime `json:"ask_date"` 136 | OpenInterest float64 `json:"open_interest"` 137 | Underlying string 138 | Strike float64 139 | ContractSize int 140 | ExpirationDate DateTime `json:"expiration_date"` 141 | ExpirationType string `json:"expiration_type"` 142 | OptionType string `json:"option_type"` 143 | RootSymbol string `json:"root_symbol"` 144 | Greeks Greeks 145 | } 146 | 147 | type Greeks struct { 148 | Delta float64 149 | Gamma float64 150 | Theta float64 151 | Vega float64 152 | Rho float64 153 | BidIV float64 `json:"bid_iv"` 154 | MidIV float64 `json:"mid_iv"` 155 | AskIV float64 `json:"ask_iv"` 156 | SmvVol float64 `json:"smv_vol"` 157 | } 158 | 159 | type TimeSale struct { 160 | Date DateTime 161 | Time DateTime 162 | Timestamp int64 163 | Open FloatOrNaN 164 | Close FloatOrNaN 165 | High FloatOrNaN 166 | Low FloatOrNaN 167 | Price FloatOrNaN 168 | Vwap FloatOrNaN 169 | Volume int64 170 | } 171 | 172 | type MarketCalendar struct { 173 | Date DateTime 174 | Status string 175 | Description string 176 | Open struct { 177 | Start string 178 | End string 179 | } 180 | Premarket struct { 181 | Start string 182 | End string 183 | } 184 | Postmarket struct { 185 | Start string 186 | End string 187 | } 188 | } 189 | 190 | type MarketStatus struct { 191 | Time DateTime `json:"date"` 192 | State string 193 | Description string 194 | NextChange DateTime `json:"next_change"` 195 | NextState string `json:"next_state"` 196 | } 197 | -------------------------------------------------------------------------------- /account.go: -------------------------------------------------------------------------------- 1 | package tradier 2 | 3 | import ( 4 | "encoding/json" 5 | ) 6 | 7 | type Margin struct { 8 | FedCall int `json:"fed_call"` 9 | MaintenanceCall int `json:"maintenance_call"` 10 | OptionBuyingPower float64 `json:"option_buying_power"` 11 | StockBuyingPower float64 `json:"stock_buying_power"` 12 | StockShortValue float64 `json:"stock_short_value"` 13 | Sweep int 14 | } 15 | 16 | type Cash struct { 17 | CashAvailable float64 `json:"cash_available"` 18 | Sweep int 19 | UnsettledFunds float64 `json:"unsettled_funds"` 20 | } 21 | 22 | type PDT struct { 23 | DayTradeBuyingPower float64 `json:"day_trade_buying_power"` 24 | FedCall int `json:"fed_call"` 25 | MaintenanceCall int `json:"maintenance_call"` 26 | OptionBuyingPower float64 `json:"option_buying_power"` 27 | StockBuyingPower float64 `json:"stock_buying_power"` 28 | StockShortValue float64 `json:"stock_short_value"` 29 | } 30 | 31 | type AccountBalances struct { 32 | AccountNumber string `json:"account_number"` 33 | AccountType string `json:"account_type"` 34 | ClosePL float64 `json:"close_pl"` 35 | CurrentRequirement float64 `json:"current_requirement"` 36 | Equity float64 37 | LongMarketValue float64 `json:"long_market_value"` 38 | MarketValue float64 `json:"market_value"` 39 | OpenPL float64 `json:"open_pl"` 40 | OptionLongValue float64 `json:"option_long_value"` 41 | OptionRequirement float64 `json:"option_requirement"` 42 | OptionShortValue float64 `json:"option_short_value"` 43 | PendingOrdersCount int `json:"pending_orders_count"` 44 | ShortMarketValue float64 `json:"short_market_value"` 45 | StockLongValue float64 `json:"stock_long_value"` 46 | TotalCash float64 `json:"total_cash"` 47 | TotalEquity float64 `json:"total_equity"` 48 | UnclearedFunds float64 `json:"uncleared_funds"` 49 | Margin Margin 50 | Cash Cash 51 | PDT PDT 52 | } 53 | 54 | type Position struct { 55 | CostBasis float64 `json:"cost_basis"` 56 | DateAcquired DateTime `json:"date_acquired"` 57 | Id int 58 | Quantity float64 59 | Symbol string 60 | } 61 | 62 | type Trade struct { 63 | Commission float64 64 | Description string 65 | Price float64 66 | Quantity float64 67 | Symbol string 68 | TradeType string `json:"trade_type"` 69 | } 70 | 71 | type Adjustment struct { 72 | Description string 73 | Quantity float64 74 | } 75 | 76 | type Event struct { 77 | Amount float64 78 | Date DateTime 79 | Type string 80 | Trade Trade 81 | Adjustment Adjustment 82 | } 83 | 84 | type ClosedPosition struct { 85 | CloseDate DateTime `json:"close_date"` 86 | Cost float64 87 | GainLoss float64 `json:"gain_loss"` 88 | GainLossPercent float64 `json:"gain_loss_percent"` 89 | OpenDate DateTime `json:"open_date"` 90 | Proceeds float64 91 | Quantity float64 92 | Symbol string 93 | Term int 94 | } 95 | 96 | const ( 97 | // Order classes. 98 | Equity = "equity" 99 | Option = "option" 100 | Multileg = "multileg" 101 | Combo = "combo" 102 | OneTriggersOther = "oto" 103 | OneCancelsOther = "oco" 104 | OneTriggersOneCancelsOther = "otoco" 105 | 106 | // Order sides. 107 | Buy = "buy" 108 | BuyToCover = "buy_to_cover" 109 | BuyToOpen = "buy_to_open" 110 | BuyToClose = "buy_to_close" 111 | Sell = "sell" 112 | SellShort = "sell_short" 113 | SellToOpen = "sell_to_open" 114 | SellToClose = "sell_to_close" 115 | 116 | // Order types. 117 | MarketOrder = "market" 118 | LimitOrder = "limit" 119 | StopOrder = "stop" 120 | StopLimitOrder = "stop_limit" 121 | Credit = "credit" 122 | Debit = "debit" 123 | Even = "even" 124 | 125 | // Order durations. 126 | Day = "day" 127 | GTC = "gtc" 128 | PreMarket = "pre" 129 | PostMarket = "post" 130 | 131 | // Option types. 132 | Put = "put" 133 | Call = "call" 134 | 135 | // Order statuses. 136 | StatusOK = "ok" 137 | Filled = "filled" 138 | Canceled = "canceled" 139 | Open = "open" 140 | Expired = "expired" 141 | Rejected = "rejected" 142 | Pending = "pending" 143 | PartiallyFilled = "partially_filled" 144 | Submitted = "submitted" 145 | ) 146 | 147 | type Order struct { 148 | Id int 149 | Type string 150 | Symbol string 151 | OptionSymbol string `json:"option_symbol"` 152 | Side string 153 | Quantity float64 154 | Status string 155 | Duration string 156 | Price float64 157 | StopPrice float64 `json:"stop_price"` 158 | OptionType string `json:"option_type"` 159 | ExpirationDate DateTime `json:"expiration_date"` 160 | Exchange string `json:"exch"` 161 | AverageFillPrice float64 `json:"avg_fill_price"` 162 | ExecutedQuantity float64 `json:"exec_quantity"` 163 | ExecutionExchange string `json:"exec_exch"` 164 | LastFillPrice float64 `json:"last_fill_price"` 165 | LastFillQuantity float64 `json:"last_fill_quantity"` 166 | RemainingQuantity float64 `json:"remaining_quantity"` 167 | CreateDate DateTime `json:"create_date"` 168 | TransactionDate DateTime `json:"transaction_date"` 169 | Class string 170 | NumLegs int `json:"num_legs"` 171 | Legs []Order 172 | Strategy string 173 | } 174 | 175 | // If there is only a single event, then tradier sends back 176 | // an object, but if there are multiple events, then it sends 177 | // a list of objects... 178 | type OpenOrders []*Order 179 | 180 | func (oo *OpenOrders) UnmarshalJSON(data []byte) error { 181 | orders := make([]*Order, 0) 182 | if err := json.Unmarshal(data, &orders); err == nil { 183 | *oo = orders 184 | return nil 185 | } 186 | 187 | order := Order{} 188 | err := json.Unmarshal(data, &order) 189 | if err == nil { 190 | *oo = []*Order{&order} 191 | } 192 | return err 193 | } 194 | 195 | // Helper struct for decoding Tradier's response to open orders request, 196 | // which returns null if there are no open orders, a list if there are 197 | // multiple open orders, or a single object if there is just one. 198 | type openOrdersResponse struct { 199 | Orders struct { 200 | Order OpenOrders 201 | } 202 | } 203 | 204 | func (oor *openOrdersResponse) UnmarshalJSON(data []byte) error { 205 | // If there are no open orders, then Tradier returns "null". 206 | var noOrders struct { 207 | Orders string 208 | } 209 | 210 | err := json.Unmarshal(data, &noOrders) 211 | if err == nil { 212 | return nil 213 | } 214 | 215 | // Otherwise, unmarshal the results using the default unmarshaler. 216 | var result struct { 217 | Orders struct { 218 | Order OpenOrders 219 | } 220 | } 221 | err = json.Unmarshal(data, &result) 222 | oor.Orders = result.Orders 223 | return err 224 | } 225 | 226 | type OrderPreview struct { 227 | Commission float64 228 | Cost float64 229 | ExtendedHours bool `json:"extended_hours"` 230 | Fees float64 231 | MarginChange float64 `json:"margin_change"` 232 | Quantity float64 233 | Status string 234 | } 235 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | GNU LESSER GENERAL PUBLIC LICENSE 2 | Version 3, 29 June 2007 3 | 4 | Copyright (C) 2007 Free Software Foundation, Inc. 5 | Everyone is permitted to copy and distribute verbatim copies 6 | of this license document, but changing it is not allowed. 7 | 8 | 9 | This version of the GNU Lesser General Public License incorporates 10 | the terms and conditions of version 3 of the GNU General Public 11 | License, supplemented by the additional permissions listed below. 12 | 13 | 0. Additional Definitions. 14 | 15 | As used herein, "this License" refers to version 3 of the GNU Lesser 16 | General Public License, and the "GNU GPL" refers to version 3 of the GNU 17 | General Public License. 18 | 19 | "The Library" refers to a covered work governed by this License, 20 | other than an Application or a Combined Work as defined below. 21 | 22 | An "Application" is any work that makes use of an interface provided 23 | by the Library, but which is not otherwise based on the Library. 24 | Defining a subclass of a class defined by the Library is deemed a mode 25 | of using an interface provided by the Library. 26 | 27 | A "Combined Work" is a work produced by combining or linking an 28 | Application with the Library. The particular version of the Library 29 | with which the Combined Work was made is also called the "Linked 30 | Version". 31 | 32 | The "Minimal Corresponding Source" for a Combined Work means the 33 | Corresponding Source for the Combined Work, excluding any source code 34 | for portions of the Combined Work that, considered in isolation, are 35 | based on the Application, and not on the Linked Version. 36 | 37 | The "Corresponding Application Code" for a Combined Work means the 38 | object code and/or source code for the Application, including any data 39 | and utility programs needed for reproducing the Combined Work from the 40 | Application, but excluding the System Libraries of the Combined Work. 41 | 42 | 1. Exception to Section 3 of the GNU GPL. 43 | 44 | You may convey a covered work under sections 3 and 4 of this License 45 | without being bound by section 3 of the GNU GPL. 46 | 47 | 2. Conveying Modified Versions. 48 | 49 | If you modify a copy of the Library, and, in your modifications, a 50 | facility refers to a function or data to be supplied by an Application 51 | that uses the facility (other than as an argument passed when the 52 | facility is invoked), then you may convey a copy of the modified 53 | version: 54 | 55 | a) under this License, provided that you make a good faith effort to 56 | ensure that, in the event an Application does not supply the 57 | function or data, the facility still operates, and performs 58 | whatever part of its purpose remains meaningful, or 59 | 60 | b) under the GNU GPL, with none of the additional permissions of 61 | this License applicable to that copy. 62 | 63 | 3. Object Code Incorporating Material from Library Header Files. 64 | 65 | The object code form of an Application may incorporate material from 66 | a header file that is part of the Library. You may convey such object 67 | code under terms of your choice, provided that, if the incorporated 68 | material is not limited to numerical parameters, data structure 69 | layouts and accessors, or small macros, inline functions and templates 70 | (ten or fewer lines in length), you do both of the following: 71 | 72 | a) Give prominent notice with each copy of the object code that the 73 | Library is used in it and that the Library and its use are 74 | covered by this License. 75 | 76 | b) Accompany the object code with a copy of the GNU GPL and this license 77 | document. 78 | 79 | 4. Combined Works. 80 | 81 | You may convey a Combined Work under terms of your choice that, 82 | taken together, effectively do not restrict modification of the 83 | portions of the Library contained in the Combined Work and reverse 84 | engineering for debugging such modifications, if you also do each of 85 | the following: 86 | 87 | a) Give prominent notice with each copy of the Combined Work that 88 | the Library is used in it and that the Library and its use are 89 | covered by this License. 90 | 91 | b) Accompany the Combined Work with a copy of the GNU GPL and this license 92 | document. 93 | 94 | c) For a Combined Work that displays copyright notices during 95 | execution, include the copyright notice for the Library among 96 | these notices, as well as a reference directing the user to the 97 | copies of the GNU GPL and this license document. 98 | 99 | d) Do one of the following: 100 | 101 | 0) Convey the Minimal Corresponding Source under the terms of this 102 | License, and the Corresponding Application Code in a form 103 | suitable for, and under terms that permit, the user to 104 | recombine or relink the Application with a modified version of 105 | the Linked Version to produce a modified Combined Work, in the 106 | manner specified by section 6 of the GNU GPL for conveying 107 | Corresponding Source. 108 | 109 | 1) Use a suitable shared library mechanism for linking with the 110 | Library. A suitable mechanism is one that (a) uses at run time 111 | a copy of the Library already present on the user's computer 112 | system, and (b) will operate properly with a modified version 113 | of the Library that is interface-compatible with the Linked 114 | Version. 115 | 116 | e) Provide Installation Information, but only if you would otherwise 117 | be required to provide such information under section 6 of the 118 | GNU GPL, and only to the extent that such information is 119 | necessary to install and execute a modified version of the 120 | Combined Work produced by recombining or relinking the 121 | Application with a modified version of the Linked Version. (If 122 | you use option 4d0, the Installation Information must accompany 123 | the Minimal Corresponding Source and Corresponding Application 124 | Code. If you use option 4d1, you must provide the Installation 125 | Information in the manner specified by section 6 of the GNU GPL 126 | for conveying Corresponding Source.) 127 | 128 | 5. Combined Libraries. 129 | 130 | You may place library facilities that are a work based on the 131 | Library side by side in a single library together with other library 132 | facilities that are not Applications and are not covered by this 133 | License, and convey such a combined library under terms of your 134 | choice, if you do both of the following: 135 | 136 | a) Accompany the combined library with a copy of the same work based 137 | on the Library, uncombined with any other library facilities, 138 | conveyed under the terms of this License. 139 | 140 | b) Give prominent notice with the combined library that part of it 141 | is a work based on the Library, and explaining where to find the 142 | accompanying uncombined form of the same work. 143 | 144 | 6. Revised Versions of the GNU Lesser General Public License. 145 | 146 | The Free Software Foundation may publish revised and/or new versions 147 | of the GNU Lesser General Public License from time to time. Such new 148 | versions will be similar in spirit to the present version, but may 149 | differ in detail to address new problems or concerns. 150 | 151 | Each version is given a distinguishing version number. If the 152 | Library as you received it specifies that a certain numbered version 153 | of the GNU Lesser General Public License "or any later version" 154 | applies to it, you have the option of following the terms and 155 | conditions either of that published version or of any later version 156 | published by the Free Software Foundation. If the Library as you 157 | received it does not specify a version number of the GNU Lesser 158 | General Public License, you may choose any version of the GNU Lesser 159 | General Public License ever published by the Free Software Foundation. 160 | 161 | If the Library as you received it specifies that a proxy can decide 162 | whether future versions of the GNU Lesser General Public License shall 163 | apply, that proxy's public statement of acceptance of any version is 164 | permanent authorization for you to choose that version for the 165 | Library. 166 | -------------------------------------------------------------------------------- /client.go: -------------------------------------------------------------------------------- 1 | package tradier 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "io" 7 | "io/ioutil" 8 | "net/http" 9 | "net/url" 10 | "strconv" 11 | "strings" 12 | "time" 13 | 14 | "github.com/cenkalti/backoff" 15 | "github.com/pkg/errors" 16 | ) 17 | 18 | const ( 19 | defaultRetries = 3 20 | 21 | // Header indicating the number of requests remaining. 22 | rateLimitAvailable = "X-Ratelimit-Available" 23 | // Header indicating the time at which our rate limit will renew. 24 | rateLimitExpiry = "X-Ratelimit-Expiry" 25 | 26 | // Error returned by Tradier if we make too big of a request. 27 | ErrBodyBufferOverflow = "protocol.http.TooBigBody" 28 | ) 29 | 30 | var ( 31 | // ErrNoAccountSelected is returned if account-specific methods 32 | // are attempted to be used without selecting an account first. 33 | ErrNoAccountSelected = errors.New("no account selected") 34 | ) 35 | 36 | type ClientParams struct { 37 | Endpoint string 38 | AuthToken string 39 | Client *http.Client 40 | Backoff backoff.BackOff 41 | RetryLimit int 42 | Account string 43 | } 44 | 45 | // DefaultParams returns ClientParams initialized with default values. 46 | func DefaultParams(authToken string) ClientParams { 47 | return ClientParams{ 48 | Endpoint: APIEndpoint, 49 | AuthToken: authToken, 50 | Client: &http.Client{}, 51 | Backoff: backoff.NewExponentialBackOff(), 52 | RetryLimit: defaultRetries, 53 | } 54 | } 55 | 56 | // Client provides methods for making requests to the Tradier API. 57 | type Client struct { 58 | client *http.Client 59 | endpoint string 60 | authHeader string 61 | backoff backoff.BackOff 62 | retryLimit int 63 | 64 | account string 65 | } 66 | 67 | func NewClient(params ClientParams) *Client { 68 | return &Client{ 69 | client: params.Client, 70 | endpoint: params.Endpoint, 71 | authHeader: fmt.Sprintf("Bearer %s", params.AuthToken), 72 | backoff: params.Backoff, 73 | retryLimit: params.RetryLimit, 74 | account: params.Account, 75 | } 76 | } 77 | 78 | func (tc *Client) SelectAccount(account string) { 79 | tc.account = account 80 | } 81 | 82 | func (tc *Client) GetAccountBalances() (*AccountBalances, error) { 83 | if tc.account == "" { 84 | return nil, ErrNoAccountSelected 85 | } 86 | 87 | url := tc.endpoint + "/v1/accounts/" + tc.account + "/balances" 88 | var result struct { 89 | Balances *AccountBalances 90 | } 91 | 92 | err := tc.getJSON(url, &result) 93 | return result.Balances, err 94 | } 95 | 96 | func (tc *Client) GetAccountPositions() ([]*Position, error) { 97 | if tc.account == "" { 98 | return nil, ErrNoAccountSelected 99 | } 100 | 101 | url := tc.endpoint + "/v1/accounts/" + tc.account + "/positions" 102 | var result struct { 103 | Positions struct { 104 | Position []*Position 105 | } 106 | } 107 | err := tc.getJSON(url, &result) 108 | return result.Positions.Position, err 109 | } 110 | 111 | func (tc *Client) GetAccountHistory(limit int) ([]*Event, error) { 112 | if tc.account == "" { 113 | return nil, ErrNoAccountSelected 114 | } 115 | 116 | url := tc.endpoint + "/v1/accounts/" + tc.account + "/history" 117 | if limit > 0 { 118 | url += fmt.Sprintf("?limit=%d", limit) 119 | } 120 | var result struct { 121 | History struct { 122 | Event []*Event 123 | } 124 | } 125 | err := tc.getJSON(url, &result) 126 | return result.History.Event, err 127 | } 128 | 129 | func (tc *Client) GetAccountCostBasis() ([]*ClosedPosition, error) { 130 | if tc.account == "" { 131 | return nil, ErrNoAccountSelected 132 | } 133 | 134 | url := tc.endpoint + "/v1/accounts/" + tc.account + "/gainloss" 135 | var result struct { 136 | GainLoss struct { 137 | ClosedPosition []*ClosedPosition `json:"closed_position"` 138 | } `json:"gainloss"` 139 | } 140 | err := tc.getJSON(url, &result) 141 | return result.GainLoss.ClosedPosition, err 142 | } 143 | 144 | func (tc *Client) GetOpenOrders() ([]*Order, error) { 145 | if tc.account == "" { 146 | return nil, ErrNoAccountSelected 147 | } 148 | 149 | url := tc.endpoint + "/v1/accounts/" + tc.account + "/orders" 150 | var result openOrdersResponse 151 | err := tc.getJSON(url, &result) 152 | return []*Order(result.Orders.Order), err 153 | } 154 | 155 | func (tc *Client) GetOrderStatus(orderId int) (*Order, error) { 156 | if tc.account == "" { 157 | return nil, ErrNoAccountSelected 158 | } 159 | 160 | url := tc.endpoint + "/v1/accounts/" + tc.account + "/orders/" + strconv.Itoa(orderId) 161 | var result struct { 162 | Order *Order 163 | } 164 | err := tc.getJSON(url, &result) 165 | return result.Order, err 166 | } 167 | 168 | func (tc *Client) PlaceOrder(order Order) (int, error) { 169 | if tc.account == "" { 170 | return 0, ErrNoAccountSelected 171 | } 172 | 173 | url := tc.endpoint + "/v1/accounts/" + tc.account + "/orders" 174 | form, err := orderToParams(order) 175 | if err != nil { 176 | return 0, err 177 | } 178 | 179 | resp, err := tc.do("POST", url, form, 0) 180 | if err != nil { 181 | return 0, err 182 | } 183 | defer resp.Body.Close() 184 | if resp.StatusCode != http.StatusOK { 185 | body, _ := ioutil.ReadAll(resp.Body) 186 | return 0, errors.New(resp.Status + ": " + string(body)) 187 | } 188 | 189 | var result struct { 190 | Order struct { 191 | Id int 192 | Status string 193 | } 194 | } 195 | dec := json.NewDecoder(resp.Body) 196 | err = dec.Decode(&result) 197 | if err != nil { 198 | return result.Order.Id, err 199 | } else if result.Order.Status != StatusOK { 200 | err = fmt.Errorf("received order status: %v", result.Order.Status) 201 | } 202 | return result.Order.Id, err 203 | } 204 | 205 | func (tc *Client) PreviewOrder(order Order) (*OrderPreview, error) { 206 | if tc.account == "" { 207 | return nil, ErrNoAccountSelected 208 | } 209 | 210 | url := tc.endpoint + "/v1/accounts/" + tc.account + "/orders" 211 | form, err := orderToParams(order) 212 | if err != nil { 213 | return nil, err 214 | } 215 | 216 | form.Add("preview", "true") 217 | resp, err := tc.do("POST", url, form, tc.retryLimit) 218 | if err != nil { 219 | return nil, err 220 | } 221 | defer resp.Body.Close() 222 | if resp.StatusCode != http.StatusOK { 223 | body, _ := ioutil.ReadAll(resp.Body) 224 | return nil, errors.New(resp.Status + ": " + string(body)) 225 | } 226 | 227 | var result struct { 228 | Order *OrderPreview 229 | } 230 | dec := json.NewDecoder(resp.Body) 231 | err = dec.Decode(&result) 232 | if err != nil { 233 | return result.Order, err 234 | } else if result.Order == nil { 235 | err = fmt.Errorf("didn't receive order preview") 236 | } else if result.Order.Status != StatusOK { 237 | err = fmt.Errorf("received order status: %v", result.Order.Status) 238 | } 239 | return result.Order, err 240 | } 241 | 242 | // Convert the given order to URL parameters for a create order request. 243 | // We also do some sanity checking to prevent placing orders with unset fields. 244 | func orderToParams(order Order) (url.Values, error) { 245 | form := url.Values{} 246 | form.Add("class", order.Class) 247 | form.Add("duration", order.Duration) 248 | 249 | switch order.Class { 250 | case Equity, Option: 251 | form.Add("symbol", order.Symbol) 252 | form.Add("side", order.Side) 253 | form.Add("quantity", strconv.FormatFloat(order.Quantity, 'f', 0, 64)) 254 | form.Add("type", order.Type) 255 | if order.Type == LimitOrder || order.Type == StopLimitOrder { 256 | form.Add("price", strconv.FormatFloat(order.Price, 'f', 2, 64)) 257 | } 258 | if order.Type == StopOrder || order.Type == StopLimitOrder { 259 | form.Add("stop", strconv.FormatFloat(order.StopPrice, 'f', 2, 64)) 260 | } 261 | case Multileg, Combo: 262 | form.Add("symbol", order.Symbol) 263 | form.Add("type", order.Type) 264 | if order.Type == LimitOrder || order.Type == StopLimitOrder { 265 | form.Add("price", strconv.FormatFloat(order.Price, 'f', 2, 64)) 266 | } 267 | if order.Type == StopOrder || order.Type == StopLimitOrder { 268 | form.Add("stop", strconv.FormatFloat(order.StopPrice, 'f', 2, 64)) 269 | } 270 | 271 | for i, leg := range order.Legs { 272 | form.Add(fmt.Sprintf("option_symbol[%d]", i), leg.OptionSymbol) 273 | form.Add(fmt.Sprintf("side[%d]", i), leg.Side) 274 | form.Add(fmt.Sprintf("quantity[%dd]", i), strconv.FormatFloat(leg.Quantity, 'f', 0, 64)) 275 | } 276 | case OneTriggersOther, OneCancelsOther, OneTriggersOneCancelsOther: 277 | for i, leg := range order.Legs { 278 | form.Add(fmt.Sprintf("symbol[%d]", i), leg.Symbol) 279 | form.Add(fmt.Sprintf("quantity[%d]", i), strconv.FormatFloat(leg.Quantity, 'f', 0, 64)) 280 | form.Add(fmt.Sprintf("type[%d]", i), leg.Type) 281 | form.Add(fmt.Sprintf("side[%d]", i), leg.Side) 282 | if leg.OptionSymbol != "" { 283 | form.Add(fmt.Sprintf("option_symbol[%d]", i), leg.OptionSymbol) 284 | } 285 | if leg.Type == LimitOrder || leg.Type == StopLimitOrder { 286 | form.Add(fmt.Sprintf("price[%d]", i), strconv.FormatFloat(leg.Price, 'f', 2, 64)) 287 | } 288 | if leg.Type == StopOrder || leg.Type == StopLimitOrder { 289 | form.Add(fmt.Sprintf("stop[%d]", i), strconv.FormatFloat(leg.StopPrice, 'f', 2, 64)) 290 | } 291 | } 292 | default: 293 | return form, fmt.Errorf("unknown order class: %v", order.Class) 294 | } 295 | return form, nil 296 | } 297 | 298 | func (tc *Client) ChangeOrder(orderId int, order Order) error { 299 | if tc.account == "" { 300 | return ErrNoAccountSelected 301 | } 302 | 303 | url := tc.endpoint + "/v1/accounts/" + tc.account + "/orders/" + strconv.Itoa(orderId) 304 | form, err := updateOrderParams(order) 305 | if err != nil { 306 | return err 307 | } 308 | resp, err := tc.do("PUT", url, form, tc.retryLimit) 309 | if err != nil { 310 | return err 311 | } 312 | defer resp.Body.Close() 313 | if resp.StatusCode != http.StatusOK { 314 | body, _ := ioutil.ReadAll(resp.Body) 315 | return errors.New(resp.Status + ": " + string(body)) 316 | } 317 | 318 | var result struct { 319 | Order struct { 320 | Id int 321 | Status string 322 | } 323 | } 324 | dec := json.NewDecoder(resp.Body) 325 | err = dec.Decode(&result) 326 | if err != nil { 327 | return err 328 | } else if result.Order.Status != StatusOK { 329 | return fmt.Errorf("received order status: %v", result.Order.Status) 330 | } else if result.Order.Id != orderId { 331 | return fmt.Errorf("changed order %v but received %v in response", orderId, result.Order.Id) 332 | } 333 | return nil 334 | } 335 | 336 | func updateOrderParams(order Order) (url.Values, error) { 337 | form := url.Values{} 338 | if order.Type != MarketOrder && order.Type != LimitOrder && order.Type != StopOrder && order.Type != StopLimitOrder { 339 | return form, fmt.Errorf("unknown order type: %v", order.Type) 340 | } 341 | form.Add("type", order.Type) 342 | if order.Duration != GTC && order.Duration != Day { 343 | return form, fmt.Errorf("unknown order duration: %v", order.Duration) 344 | } 345 | form.Add("duration", order.Duration) 346 | if order.Type == LimitOrder || order.Type == StopLimitOrder { 347 | if order.Price <= 0 { 348 | return form, fmt.Errorf("cannot place limit order without limit price") 349 | } 350 | form.Add("price", strconv.FormatFloat(order.Price, 'f', 2, 64)) 351 | } 352 | if order.Type == StopOrder || order.Type == StopLimitOrder { 353 | if order.StopPrice <= 0 { 354 | return form, fmt.Errorf("cannot place stop order without stop price") 355 | } 356 | form.Add("stop", strconv.FormatFloat(order.StopPrice, 'f', 2, 64)) 357 | } 358 | return form, nil 359 | } 360 | 361 | func (tc *Client) CancelOrder(orderId int) error { 362 | if tc.account == "" { 363 | return ErrNoAccountSelected 364 | } 365 | 366 | url := tc.endpoint + "/v1/accounts/" + tc.account + "/orders/" + strconv.Itoa(orderId) 367 | resp, err := tc.do("DELETE", url, nil, tc.retryLimit) 368 | if err != nil { 369 | return err 370 | } 371 | defer resp.Body.Close() 372 | if resp.StatusCode != http.StatusOK { 373 | body, _ := ioutil.ReadAll(resp.Body) 374 | return errors.New(resp.Status + ": " + string(body)) 375 | } 376 | 377 | var result struct { 378 | Order struct { 379 | Id int 380 | Status string 381 | } 382 | } 383 | dec := json.NewDecoder(resp.Body) 384 | err = dec.Decode(&result) 385 | if err != nil { 386 | return err 387 | } else if result.Order.Status != StatusOK { 388 | return fmt.Errorf("received order status: %v", result.Order.Status) 389 | } else if result.Order.Id != orderId { 390 | return fmt.Errorf( 391 | "asked to cancel order %v but received %v in response", 392 | orderId, result.Order.Id) 393 | } 394 | return nil 395 | 396 | } 397 | 398 | // Get a list of symbols matching the given parameters. 399 | func (tc *Client) LookupSecurities( 400 | types []SecurityType, exchanges []string, query string) ( 401 | []Security, error) { 402 | url := tc.endpoint + "/v1/markets/lookup" 403 | if len(types) > 0 { 404 | strTypes := make([]string, len(types)) 405 | for i, t := range types { 406 | strTypes[i] = string(t) 407 | } 408 | url = url + "?types=" + strings.Join(strTypes, ",") 409 | } 410 | if exchanges != nil && len(exchanges) > 0 { 411 | url = url + "&exchanges=" + strings.Join(exchanges, ",") 412 | } 413 | if query != "" { 414 | url = url + "&q=" + query 415 | } 416 | 417 | var result struct { 418 | Securities struct { 419 | // TODO: If there is only one data point, then Tradier returns 420 | // a single object (not a list) and this fails to parse it. 421 | Security []Security 422 | } 423 | } 424 | err := tc.getJSON(url, &result) 425 | return result.Securities.Security, err 426 | } 427 | 428 | // Get the securities on the Easy-to-Borrow list. 429 | func (tc *Client) GetEasyToBorrow() ([]Security, error) { 430 | url := tc.endpoint + "/v1/markets/etb" 431 | var result struct { 432 | Securities struct { 433 | Security []Security 434 | } 435 | } 436 | err := tc.getJSON(url, &result) 437 | return result.Securities.Security, err 438 | } 439 | 440 | // Get an option's expiration dates. 441 | func (tc *Client) GetOptionExpirationDates(symbol string) ([]time.Time, error) { 442 | params := "?symbol=" + symbol 443 | url := tc.endpoint + "/v1/markets/options/expirations" + params 444 | var result struct { 445 | Expirations struct { 446 | Date []DateTime 447 | } 448 | } 449 | err := tc.getJSON(url, &result) 450 | 451 | times := make([]time.Time, len(result.Expirations.Date)) 452 | for i, dt := range result.Expirations.Date { 453 | times[i] = dt.Time 454 | } 455 | 456 | return times, err 457 | } 458 | 459 | // Get an option's expiration dates. 460 | func (tc *Client) GetOptionStrikes(symbol string, expiration time.Time) ([]float64, error) { 461 | params := "?symbol=" + symbol + "&expiration=" + expiration.Format("2006-01-02") 462 | url := tc.endpoint + "/v1/markets/options/strikes" + params 463 | var result struct { 464 | Strikes struct { 465 | Strike []float64 466 | } 467 | } 468 | err := tc.getJSON(url, &result) 469 | return result.Strikes.Strike, err 470 | } 471 | 472 | // Get an option chain. 473 | func (tc *Client) GetOptionChain(symbol string, expiration time.Time) ([]*Quote, error) { 474 | params := "?greeks=true&symbol=" + symbol + "&expiration=" + expiration.Format("2006-01-02") 475 | url := tc.endpoint + "/v1/markets/options/chains" + params 476 | var result struct { 477 | Options struct { 478 | Option []*Quote 479 | } 480 | } 481 | err := tc.getJSON(url, &result) 482 | return result.Options.Option, err 483 | } 484 | 485 | func (tc *Client) getTimeSalesUrl(symbol string, interval Interval, start, end time.Time) string { 486 | url := tc.endpoint 487 | timeFormat := "2006-01-02T15:04:05" 488 | tz := time.UTC 489 | var err error 490 | if interval == IntervalDaily || interval == IntervalWeekly || interval == IntervalMonthly { 491 | url = url + "/v1/markets/history" 492 | timeFormat = "2006-01-02" 493 | } else { 494 | url = url + "/v1/markets/timesales" 495 | tz, err = time.LoadLocation("America/New_York") 496 | if err != nil { 497 | panic(err) 498 | } 499 | } 500 | url = url + "?symbol=" + symbol 501 | if interval != "" { 502 | url = url + "&interval=" + string(interval) 503 | } 504 | if !start.IsZero() { 505 | url = url + "&start=" + start.In(tz).Format(timeFormat) 506 | } 507 | if !end.IsZero() { 508 | url = url + "&end=" + end.In(tz).Format(timeFormat) 509 | } 510 | return url 511 | } 512 | 513 | // NOTE: If there is only one data point, then Tradier returns 514 | // a single object (not a list). So first try list, and if parsing 515 | // fails then fall back to try parsing a single object. 516 | type timeSaleList []TimeSale 517 | 518 | func (tsl *timeSaleList) UnmarshalJSON(data []byte) error { 519 | tss := make([]TimeSale, 0) 520 | if err := json.Unmarshal(data, &tss); err == nil { 521 | *tsl = tss 522 | return nil 523 | } 524 | 525 | ts := TimeSale{} 526 | err := json.Unmarshal(data, &ts) 527 | if err == nil { 528 | *tsl = []TimeSale{ts} 529 | } 530 | return err 531 | } 532 | 533 | func decodeTimeSales(reader io.Reader, interval Interval) ([]TimeSale, error) { 534 | dec := json.NewDecoder(reader) 535 | var timeSales []TimeSale 536 | if interval == IntervalDaily || interval == IntervalWeekly || interval == IntervalMonthly { 537 | var result struct { 538 | History struct { 539 | Day timeSaleList 540 | } 541 | } 542 | err := dec.Decode(&result) 543 | if err != nil { 544 | return nil, err 545 | } 546 | timeSales = result.History.Day 547 | } else { 548 | var result struct { 549 | Series struct { 550 | Data timeSaleList 551 | } 552 | } 553 | err := dec.Decode(&result) 554 | if err != nil { 555 | return nil, err 556 | } 557 | timeSales = result.Series.Data 558 | } 559 | 560 | return timeSales, nil 561 | } 562 | 563 | func bisect(start, end time.Time) time.Time { 564 | if end.IsZero() { 565 | end = time.Now() 566 | } 567 | 568 | delta := end.Sub(start) 569 | middle := start.Add(delta / 2) 570 | return middle 571 | } 572 | 573 | // Return daily, minute, or tick price bars for the given symbol. 574 | // Tick data is available for the past 5 days, minute data for the past 20 days, 575 | // and daily data since 1980-01-01. 576 | // NOTE: The results are split, but not dividend-adjusted. 577 | // https://developer.tradier.com/documentation/markets/get-history 578 | // https://developer.tradier.com/documentation/markets/get-timesales 579 | func (tc *Client) GetTimeSales( 580 | symbol string, interval Interval, 581 | start, end time.Time) ([]TimeSale, error) { 582 | 583 | url := tc.getTimeSalesUrl(symbol, interval, start, end) 584 | 585 | resp, err := tc.do("GET", url, nil, tc.retryLimit) 586 | if err != nil { 587 | if err, ok := err.(TradierError); ok { 588 | if err.Fault.Detail.ErrorCode == ErrBodyBufferOverflow { 589 | // Too much data for a single request! 590 | // Split the requested time interval in half and recurse. 591 | middle := bisect(start, end) 592 | if end.Sub(middle) < time.Duration(1*time.Minute) { 593 | // Give up if the interval is < 1min to prevent infinite recursion. 594 | return nil, err 595 | } 596 | 597 | firstHalf, err := tc.GetTimeSales(symbol, interval, start, middle) 598 | if err != nil { 599 | return nil, err 600 | } 601 | secondHalf, err := tc.GetTimeSales(symbol, interval, middle, end) 602 | if err != nil { 603 | return nil, err 604 | } 605 | allResults := make([]TimeSale, 0, len(firstHalf)+len(secondHalf)) 606 | allResults = append(allResults, firstHalf...) 607 | allResults = append(allResults, secondHalf...) 608 | return allResults, nil 609 | } 610 | } 611 | 612 | // Some other error that we don't know how to handle. 613 | return nil, err 614 | } 615 | 616 | defer resp.Body.Close() 617 | return decodeTimeSales(resp.Body, interval) 618 | } 619 | 620 | // Subscribe to a stream of market events for the given symbols. 621 | // Filter restricts the type of events streamed and can include: 622 | // summary, trade, quote, timesale. If nil then all events are streamed. 623 | // https://developer.tradier.com/documentation/streaming/get-markets-events 624 | func (tc *Client) StreamMarketEvents( 625 | symbols []string, filter []Filter) (io.ReadCloser, error) { 626 | if len(symbols) == 0 { 627 | return nil, errors.New("list of symbols is required") 628 | } 629 | 630 | // First create a streaming session. 631 | createSessionUrl := tc.endpoint + "/v1/markets/events/session" 632 | 633 | createSessionResp, err := tc.do("POST", createSessionUrl, nil, tc.retryLimit) 634 | if err != nil { 635 | return nil, err 636 | } 637 | defer createSessionResp.Body.Close() 638 | if createSessionResp.StatusCode != http.StatusOK { 639 | body, _ := ioutil.ReadAll(createSessionResp.Body) 640 | return nil, errors.New(createSessionResp.Status + ": " + string(body)) 641 | } 642 | 643 | dec := json.NewDecoder(createSessionResp.Body) 644 | var sessionResp struct { 645 | Stream struct { 646 | SessionId string 647 | Url string 648 | } 649 | } 650 | err = dec.Decode(&sessionResp) 651 | if err != nil { 652 | return nil, err 653 | } 654 | 655 | // Now open the stream. 656 | form := url.Values{} 657 | form.Add("linebreak", "true") 658 | form.Add("sessionid", sessionResp.Stream.SessionId) 659 | form.Add("symbols", strings.Join(symbols, ",")) 660 | if len(filter) > 0 { 661 | strFilters := make([]string, len(filter)) 662 | for i, f := range filter { 663 | strFilters[i] = string(f) 664 | } 665 | form.Add("filter", strings.Join(strFilters, ",")) 666 | } 667 | // TODO: Make validOnly/flags configurable. 668 | form.Add("advancedDetails", "true") 669 | // If we fail here then just make a new session rather than retrying. 670 | // This prevents repeated failures to a session that doesn't exist for 671 | // some reason. 672 | resp, err := tc.do("POST", sessionResp.Stream.Url, form, 0) 673 | if err != nil { 674 | return nil, err 675 | } else if resp == nil { 676 | return nil, errors.New("nil response with no error") 677 | } else if resp.StatusCode != http.StatusOK { 678 | body, _ := ioutil.ReadAll(resp.Body) 679 | return nil, errors.New(resp.Status + ": " + string(body)) 680 | } 681 | 682 | return resp.Body, nil 683 | } 684 | 685 | // Get the market calendar for a given month. 686 | func (tc *Client) GetMarketCalendar(year int, month time.Month) ([]MarketCalendar, error) { 687 | params := fmt.Sprintf("?year=%d&month=%d", year, month) 688 | url := tc.endpoint + "/v1/markets/calendar" + params 689 | var result struct { 690 | Calendar struct { 691 | Days struct { 692 | Day []MarketCalendar 693 | } 694 | } 695 | } 696 | 697 | err := tc.getJSON(url, &result) 698 | return result.Calendar.Days.Day, err 699 | } 700 | 701 | // Get the current state of the market (open/closed/etc.) 702 | func (tc *Client) GetMarketState() (MarketStatus, error) { 703 | url := tc.endpoint + "/v1/markets/clock" 704 | var result struct { 705 | Clock MarketStatus 706 | } 707 | err := tc.getJSON(url, &result) 708 | return result.Clock, err 709 | } 710 | 711 | // Get corporate calendars. 712 | func (tc *Client) GetCorporateCalendars(symbols []string) ( 713 | GetCorporateCalendarsResponse, error) { 714 | params := "?symbols=" + strings.Join(symbols, ",") 715 | url := tc.endpoint + "/beta/markets/fundamentals/calendars" + params 716 | var result GetCorporateCalendarsResponse 717 | err := tc.getJSON(url, &result) 718 | return result, err 719 | } 720 | 721 | // Get company fundamentals. 722 | func (tc *Client) GetCompanyInfo(symbols []string) (GetCompanyInfoResponse, error) { 723 | params := "?symbols=" + strings.Join(symbols, ",") 724 | url := tc.endpoint + "/beta/markets/fundamentals/company" + params 725 | var result GetCompanyInfoResponse 726 | err := tc.getJSON(url, &result) 727 | return result, err 728 | } 729 | 730 | // Get corporate actions. 731 | func (tc *Client) GetCorporateActions(symbols []string) (GetCorporateActionsResponse, error) { 732 | params := "?symbols=" + strings.Join(symbols, ",") 733 | url := tc.endpoint + "/beta/markets/fundamentals/corporate_actions" + params 734 | var result GetCorporateActionsResponse 735 | err := tc.getJSON(url, &result) 736 | return result, err 737 | } 738 | 739 | // Get dividends. 740 | func (tc *Client) GetDividends(symbols []string) (GetDividendsResponse, error) { 741 | params := "?symbols=" + strings.Join(symbols, ",") 742 | url := tc.endpoint + "/beta/markets/fundamentals/dividends" + params 743 | var result GetDividendsResponse 744 | err := tc.getJSON(url, &result) 745 | return result, err 746 | } 747 | 748 | // Get corporate ratios. 749 | func (tc *Client) GetRatios(symbols []string) (GetRatiosResponse, error) { 750 | params := "?symbols=" + strings.Join(symbols, ",") 751 | url := tc.endpoint + "/beta/markets/fundamentals/ratios" + params 752 | var result GetRatiosResponse 753 | err := tc.getJSON(url, &result) 754 | return result, err 755 | } 756 | 757 | // Get financial reports. 758 | func (tc *Client) GetFinancials(symbols []string) (GetFinancialsResponse, error) { 759 | params := "?symbols=" + strings.Join(symbols, ",") 760 | url := tc.endpoint + "/beta/markets/fundamentals/financials" + params 761 | var result GetFinancialsResponse 762 | err := tc.getJSON(url, &result) 763 | return result, err 764 | } 765 | 766 | // Get price statistics. 767 | func (tc *Client) GetPriceStatistics(symbols []string) (GetPriceStatisticsResponse, error) { 768 | params := "?symbols=" + strings.Join(symbols, ",") 769 | url := tc.endpoint + "/beta/markets/fundamentals/statistics" + params 770 | var result GetPriceStatisticsResponse 771 | err := tc.getJSON(url, &result) 772 | return result, err 773 | } 774 | 775 | func (tc *Client) getJSON(url string, result interface{}) error { 776 | resp, err := tc.do("GET", url, nil, tc.retryLimit) 777 | if err != nil { 778 | return err 779 | } 780 | defer resp.Body.Close() 781 | if resp.StatusCode != http.StatusOK { 782 | body, _ := ioutil.ReadAll(resp.Body) 783 | return errors.New(resp.Status + ": " + string(body)) 784 | } 785 | 786 | dec := json.NewDecoder(resp.Body) 787 | return dec.Decode(result) 788 | } 789 | 790 | func (tc *Client) GetQuotes(symbols []string) ([]*Quote, error) { 791 | var result struct { 792 | Quotes struct { 793 | Quote []*Quote 794 | } 795 | } 796 | 797 | uri := tc.endpoint + "/v1/markets/quotes" 798 | data := url.Values{"symbols": {strings.Join(symbols, ",")}, "greeks": {"true"}} 799 | 800 | err := tc.postJSON(uri, data, &result) 801 | if err != nil { 802 | return nil, err 803 | } 804 | 805 | return result.Quotes.Quote, nil 806 | } 807 | 808 | func (tc *Client) postJSON(url string, data url.Values, result interface{}) error { 809 | resp, err := tc.do("POST", url, data, tc.retryLimit) 810 | if err != nil { 811 | return err 812 | } 813 | 814 | defer resp.Body.Close() 815 | if resp.StatusCode != http.StatusOK { 816 | body, _ := ioutil.ReadAll(resp.Body) 817 | return errors.New(resp.Status + ": " + string(body)) 818 | } 819 | 820 | dec := json.NewDecoder(resp.Body) 821 | return dec.Decode(result) 822 | } 823 | 824 | func (tc *Client) do(method, url string, body url.Values, maxRetries int) (*http.Response, error) { 825 | var req *http.Request 826 | var resp *http.Response 827 | var err error 828 | var sleep time.Duration 829 | for i := 0; i <= maxRetries; i++ { 830 | // Request must be made within retry loop, because body will be re-read each time. 831 | req, err = tc.makeSignedRequest(method, url, body) 832 | if err != nil { 833 | return nil, err 834 | } 835 | 836 | resp, err = tc.client.Do(req) 837 | if err == nil && resp.StatusCode == http.StatusOK { 838 | break // Successful request 839 | } 840 | 841 | if err != nil { 842 | Logger.Println(err) 843 | sleep = tc.backoff.NextBackOff() 844 | } else if resp.StatusCode != http.StatusOK { 845 | var respBody []byte 846 | respBody, err = ioutil.ReadAll(resp.Body) 847 | resp.Body.Close() 848 | tradierErr := TradierError{ 849 | HttpStatusCode: resp.StatusCode, 850 | } 851 | if jsonErr := json.Unmarshal(respBody, &tradierErr); jsonErr == nil { 852 | // We extracted an error message, don't retry. 853 | return resp, tradierErr 854 | } else { 855 | tradierErr.Fault.FaultString = string(respBody) 856 | } 857 | // Assign an error since we have read the body. If this is the last retry, 858 | // we need to return a non-nil error. 859 | err = tradierErr 860 | rateLimitExpiry := parseQuotaViolationExpiration(tradierErr.Fault.FaultString) 861 | if rateLimitExpiry.After(time.Now().Add(sleep)) { 862 | sleep = rateLimitExpiry.Sub(time.Now()) + (1 * time.Second) 863 | } else { 864 | sleep = tc.backoff.NextBackOff() 865 | } 866 | } 867 | 868 | if i+1 <= maxRetries && sleep != backoff.Stop { 869 | Logger.Printf("Retrying after %v\n", sleep) 870 | time.Sleep(sleep) 871 | } 872 | } 873 | return resp, err 874 | } 875 | 876 | func (tc *Client) makeSignedRequest(method, url string, body url.Values) (*http.Request, error) { 877 | var bodyReader io.Reader 878 | if body != nil { 879 | bodyReader = strings.NewReader(body.Encode()) 880 | } 881 | 882 | req, err := http.NewRequest(method, url, bodyReader) 883 | if err != nil { 884 | return nil, err 885 | } 886 | 887 | req.Header.Set("Accept", "application/json") 888 | req.Header.Set("Authorization", tc.authHeader) 889 | if method != http.MethodDelete { 890 | req.Header.Set("Content-Type", "application/x-www-form-urlencoded") 891 | } 892 | 893 | return req, nil 894 | } 895 | -------------------------------------------------------------------------------- /fundamentals.go: -------------------------------------------------------------------------------- 1 | package tradier 2 | 3 | import "encoding/json" 4 | 5 | type CorporateEvent struct { 6 | BeginDateTime *string `json:"begin_date_time"` 7 | CompanyID *string `json:"company_id"` 8 | EndDateTime *string `json:"end_date_time"` 9 | Event *string `json:"event"` 10 | EventType *int64 `json:"event_type"` 11 | TimeZone *string `json:"time_zone,omitempty"` 12 | } 13 | 14 | // If there is only a single event, then tradier sends back 15 | // an object, but if there are multiple events, then it sends 16 | // a list of objects... 17 | type CorporateCalendar []CorporateEvent 18 | 19 | func (cc *CorporateCalendar) UnmarshalJSON(data []byte) error { 20 | events := make([]CorporateEvent, 0) 21 | if err := json.Unmarshal(data, &events); err == nil { 22 | *cc = events 23 | return nil 24 | } 25 | 26 | event := CorporateEvent{} 27 | err := json.Unmarshal(data, &event) 28 | if err == nil { 29 | *cc = []CorporateEvent{event} 30 | } 31 | return err 32 | } 33 | 34 | type GetCorporateCalendarsResponse []struct { 35 | Error string 36 | Request string `json:"request"` 37 | Results []struct { 38 | ID string `json:"id"` 39 | Tables struct { 40 | CorporateCalendars *CorporateCalendar `json:"corporate_calendars"` 41 | } `json:"tables"` 42 | Type string `json:"type"` 43 | } `json:"results"` 44 | Type string `json:"type"` 45 | } 46 | 47 | type NAICS []int64 48 | 49 | func (n *NAICS) UnmarshalJSON(data []byte) error { 50 | ids := make([]int64, 0) 51 | if err := json.Unmarshal(data, &ids); err == nil { 52 | *n = ids 53 | return nil 54 | } 55 | 56 | var id int64 57 | err := json.Unmarshal(data, &id) 58 | if err == nil { 59 | *n = []int64{id} 60 | } 61 | return err 62 | } 63 | 64 | type SIC []int64 65 | 66 | func (n *SIC) UnmarshalJSON(data []byte) error { 67 | ids := make([]int64, 0) 68 | if err := json.Unmarshal(data, &ids); err == nil { 69 | *n = ids 70 | return nil 71 | } 72 | 73 | var id int64 74 | err := json.Unmarshal(data, &id) 75 | if err == nil { 76 | *n = []int64{id} 77 | } 78 | return err 79 | } 80 | 81 | type AssetClassification struct { 82 | FinancialHealthGradeAsOfDate *string `json:"FinancialHealthGrade.asOfDate"` 83 | GrowthGradeAsOfDate *string `json:"GrowthGrade.asOfDate"` 84 | ProfitabilityGradeAsOfDate *string `json:"ProfitabilityGrade.asOfDate"` 85 | StockTypeAsOfDate *string `json:"StockType.asOfDate"` 86 | StyleBoxAsOfDate *string `json:"StyleBox.asOfDate"` 87 | CANNAICS *int64 `json:"c_a_n_n_a_i_c_s"` 88 | CompanyID *string `json:"company_id"` 89 | FinancialHealthGrade *string `json:"financial_health_grade"` 90 | GrowthGrade *string `json:"growth_grade"` 91 | GrowthScore *float64 `json:"growth_score"` 92 | MorningstarEconomySphereCode *int64 `json:"morningstar_economy_sphere_code"` 93 | MorningstarIndustryCode *int64 `json:"morningstar_industry_code"` 94 | MorningstarIndustryGroupCode *int64 `json:"morningstar_industry_group_code"` 95 | MorningstarSectorCode *int64 `json:"morningstar_sector_code"` 96 | NACE *float64 `json:"n_a_c_e"` 97 | NAICS NAICS `json:"n_a_i_c_s"` 98 | ProfitabilityGrade *string `json:"profitability_grade"` 99 | SIC SIC `json:"s_i_c"` 100 | SizeScore *float64 `json:"size_score"` 101 | StockType *int64 `json:"stock_type"` 102 | StyleBox *int64 `json:"style_box"` 103 | StyleScore *float64 `json:"style_score"` 104 | ValueScore *float64 `json:"value_score"` 105 | } 106 | 107 | type CompanyHeadquarter struct { 108 | AddressLine1 *string `json:"address_line1"` 109 | City *string `json:"city"` 110 | Country *string `json:"country"` 111 | Fax *string `json:"fax"` 112 | Homepage *string `json:"homepage"` 113 | Phone *string `json:"phone"` 114 | PostalCode *string `json:"postal_code"` 115 | Province *string `json:"province"` 116 | } 117 | 118 | type CompanyProfile struct { 119 | TotalEmployeeNumberAsOfDate *string `json:"TotalEmployeeNumber.asOfDate"` 120 | CompanyID *string `json:"company_id"` 121 | ContactEmail *string `json:"contact_email"` 122 | Headquarter *CompanyHeadquarter `json:"headquarter"` 123 | ShortDescription *string `json:"short_description"` 124 | TotalEmployeeNumber *int64 `json:"total_employee_number"` 125 | } 126 | 127 | type HistoricalAssetClassification struct { 128 | AsOfDate *string `json:"as_of_date"` 129 | CompanyID *string `json:"company_id"` 130 | FinancialHealthGrade *string `json:"financial_health_grade"` 131 | GrowthScore *float64 `json:"growth_score"` 132 | MorningstarEconomySphereCode *int64 `json:"morningstar_economy_sphere_code"` 133 | MorningstarIndustryCode *int64 `json:"morningstar_industry_code"` 134 | MorningstarIndustryGroupCode *int64 `json:"morningstar_industry_group_code"` 135 | MorningstarSectorCode *int64 `json:"morningstar_sector_code"` 136 | ProfitabilityGrade *string `json:"profitability_grade"` 137 | SizeScore *float64 `json:"size_score"` 138 | StockType *int64 `json:"stock_type"` 139 | StyleBox *int64 `json:"style_box"` 140 | StyleScore *float64 `json:"style_score"` 141 | ValueScore *float64 `json:"value_score"` 142 | } 143 | 144 | type ShareClass struct { 145 | CompanyID *string `json:"company_id"` 146 | CUSIP *string `json:"c_u_s_i_p"` 147 | CurrencyID *string `json:"currency_id"` 148 | DelistingDate *string `json:"delisting_date"` 149 | ExchangeID *string `json:"exchange_id"` 150 | IPODate *string `json:"i_p_o_date"` 151 | ISIN *string `json:"i_s_i_n"` 152 | InvestmentID *string `json:"investment_id"` 153 | IsDepositaryReceipt *bool `json:"is_depositary_receipt"` 154 | IsDirectInvest *bool `json:"is_direct_invest"` 155 | IsDividendReinvest *bool `json:"is_dividend_reinvest"` 156 | IsPrimaryShare *bool `json:"is_primary_share"` 157 | MIC *string `json:"m_i_c"` 158 | SEDOL *string `json:"s_e_d_o_l"` 159 | SecurityType *string `json:"security_type"` 160 | ShareClassID *string `json:"share_class_id"` 161 | ShareClassStatus *string `json:"share_class_status"` 162 | Symbol *string `json:"symbol"` 163 | TradingStatus *bool `json:"trading_status"` 164 | Valoren *string `json:"valoren"` 165 | } 166 | 167 | type ShareClassProfile struct { 168 | EnterpriseValueAsOfDate *string `json:"EnterpriseValue.asOfDate"` 169 | MarketCapAsOfDate *string `json:"MarketCap.asOfDate"` 170 | SharesOutstandingAsOfDate *string `json:"SharesOutstanding.asOfDate"` 171 | EnterpriseValue *int64 `json:"enterprise_value"` 172 | MarketCap *int64 `json:"market_cap"` 173 | ShareClassID *string `json:"share_class_id"` 174 | ShareClassLevelSharesOutstanding *int64 `json:"share_class_level_shares_outstanding"` 175 | SharesOutstanding *int64 `json:"shares_outstanding"` 176 | SharesOutstandingWithBalanceSheetEndingDate *string `json:"shares_outstanding_with_balance_sheet_ending_date"` 177 | } 178 | 179 | type OwnershipDetail struct { 180 | AsOfDate *string `json:"as_of_date"` 181 | CurrencyOfMarketValue *string `json:"currencyof_market_value"` 182 | MarketValue *int64 `json:"market_value"` 183 | NumberOfShares *float64 `json:"number_of_shares"` 184 | OwnerCIK *int64 `json:"owner_c_i_k"` 185 | OwnerID *string `json:"owner_id"` 186 | OwnerName *string `json:"owner_name"` 187 | OwnerType *int64 `json:"owner_type,string"` 188 | PercentageInPortfolio *float64 `json:"percentage_in_portfolio"` 189 | PercentageOwnership *float64 `json:"percentage_ownership"` 190 | ShareChange *int64 `json:"share_change"` 191 | ShareClassID *string `json:"share_class_id"` 192 | } 193 | 194 | type OwnershipDetails []OwnershipDetail 195 | 196 | func (ods *OwnershipDetails) UnmarshalJSON(data []byte) error { 197 | details := make([]OwnershipDetail, 0) 198 | if err := json.Unmarshal(data, &details); err == nil { 199 | *ods = details 200 | return nil 201 | } 202 | 203 | detail := OwnershipDetail{} 204 | err := json.Unmarshal(data, &detail) 205 | if err == nil { 206 | *ods = []OwnershipDetail{detail} 207 | } 208 | return err 209 | } 210 | 211 | type OwnershipSummary struct { 212 | AsOfDate *string `json:"as_of_date"` 213 | DaysToCoverShort map[string]float64 `json:"days_to_cover_short"` 214 | Float *int64 `json:"float"` 215 | InsiderPercentOwned *float64 `json:"insider_percent_owned"` 216 | InsiderSharesBought *int64 `json:"insider_shares_bought"` 217 | InsiderSharesOwned *int64 `json:"insider_shares_owned"` 218 | InsiderSharesSold *int64 `json:"insider_shares_sold"` 219 | InstitutionHolderNumber *int64 `json:"institution_holder_number"` 220 | InstitutionSharesBought *int64 `json:"institution_shares_bought"` 221 | InstitutionSharesHeld *int64 `json:"institution_shares_held"` 222 | InstitutionSharesSold *int64 `json:"institution_shares_sold"` 223 | NumberOfInsiderBuys *int64 `json:"number_of_insider_buys"` 224 | NumberOfInsiderSellers *int64 `json:"number_of_insider_sellers"` 225 | ShareClassID *string `json:"share_class_id"` 226 | ShareClassLevelSharesOutstanding *int64 `json:"share_class_level_shares_outstanding"` 227 | ShareClassLevelSharesOutstandingBalanceSheet *int64 `json:"share_class_level_shares_outstanding_balance_sheet"` 228 | ShareClassLevelSharesOutstandingInterim *int64 `json:"share_class_level_shares_outstanding_interim"` 229 | ShareClassLevelTreasuryShareOutstanding *int64 `json:"share_class_level_treasury_share_outstanding"` 230 | SharesOutstanding *int64 `json:"shares_outstanding"` 231 | SharesOutstandingWithBalanceSheetEndingDate *string `json:"shares_outstanding_with_balance_sheet_ending_date"` 232 | ShortInterest *int64 `json:"short_interest"` 233 | ShortInterestsPercentageChange map[string]float64 `json:"short_interests_percentage_change"` 234 | ShortPercentageOfFloat *float64 `json:"short_percentage_of_float"` 235 | ShortPercentageOfSharesOutstanding *float64 `json:"short_percentage_of_shares_outstanding"` 236 | } 237 | 238 | type CompanyInfoResult struct { 239 | ID string `json:"id"` 240 | Tables struct { 241 | AssetClassification *AssetClassification `json:"asset_classification"` 242 | CompanyProfile *CompanyProfile `json:"company_profile"` 243 | HistoricalAssetClassification *HistoricalAssetClassification `json:"historical_asset_classification"` 244 | LongDescriptions *string `json:"long_descriptions"` 245 | OwnershipDetails OwnershipDetails `json:"ownership_details"` 246 | OwnershipSummary *OwnershipSummary `json:"ownership_summary"` 247 | ShareClass *ShareClass `json:"share_class"` 248 | ShareClassProfile *ShareClassProfile `json:"share_class_profile"` 249 | } `json:"tables"` 250 | Type string `json:"type"` 251 | } 252 | 253 | type GetCompanyInfoResponse []struct { 254 | Error string 255 | Request string `json:"request"` 256 | Results []CompanyInfoResult `json:"results"` 257 | Type string `json:"type"` 258 | } 259 | 260 | type MergerAndAcquisition struct { 261 | AcquiredCompanyID *string `json:"acquired_company_id"` 262 | CashAmount *float64 `json:"cash_amount"` 263 | CurrencyID *string `json:"currency_id"` 264 | EffectiveDate *string `json:"effective_date"` 265 | Notes *string `json:"notes"` 266 | ParentCompanyID *string `json:"parent_company_id"` 267 | } 268 | 269 | type MergersAndAcquisitions []MergerAndAcquisition 270 | 271 | func (maq *MergersAndAcquisitions) UnmarshalJSON(data []byte) error { 272 | events := make([]MergerAndAcquisition, 0) 273 | if err := json.Unmarshal(data, &events); err == nil { 274 | *maq = events 275 | return nil 276 | } 277 | 278 | event := MergerAndAcquisition{} 279 | err := json.Unmarshal(data, &event) 280 | if err == nil { 281 | *maq = []MergerAndAcquisition{event} 282 | } 283 | return err 284 | } 285 | 286 | type StockSplit struct { 287 | AdjustmentFactor *float64 `json:"adjustment_factor"` 288 | ExDate *string `json:"ex_date"` 289 | ShareClassID *string `json:"share_class_id"` 290 | SplitFrom *float64 `json:"split_from"` 291 | SplitTo *float64 `json:"split_to"` 292 | SplitType *string `json:"split_type"` 293 | } 294 | 295 | type StockSplits map[string]StockSplit 296 | 297 | type GetCorporateActionsResponse []struct { 298 | Error string 299 | Request string `json:"request"` 300 | Results []struct { 301 | ID string `json:"id"` 302 | Tables struct { 303 | MergersAndAcquisitions MergersAndAcquisitions `json:"mergers_and_acquisitions"` 304 | StockSplits StockSplits `json:"stock_splits"` 305 | } `json:"tables"` 306 | Type string `json:"type"` 307 | } `json:"results"` 308 | Type string `json:"type"` 309 | } 310 | 311 | type CashDividend struct { 312 | CashAmount *float64 `json:"cash_amount"` 313 | CurrencyID *string `json:"currency_i_d"` 314 | DeclarationDate *string `json:"declaration_date"` 315 | DividendType *string `json:"dividend_type"` 316 | ExDate *string `json:"ex_date"` 317 | Frequency *int64 `json:"frequency"` 318 | PayDate *string `json:"pay_date"` 319 | RecordDate *string `json:"record_date"` 320 | ShareClassID *string `json:"share_class_id"` 321 | } 322 | 323 | type CashDividends []CashDividend 324 | 325 | func (cds *CashDividends) UnmarshalJSON(data []byte) error { 326 | dividends := make([]CashDividend, 0) 327 | if err := json.Unmarshal(data, ÷nds); err == nil { 328 | *cds = dividends 329 | return nil 330 | } 331 | 332 | d := CashDividend{} 333 | err := json.Unmarshal(data, &d) 334 | if err == nil { 335 | *cds = []CashDividend{d} 336 | } 337 | return err 338 | } 339 | 340 | type GetDividendsResponse []struct { 341 | Error string 342 | Request string `json:"request"` 343 | Results []struct { 344 | ID string `json:"id"` 345 | Tables struct { 346 | CashDividends CashDividends `json:"cash_dividends"` 347 | } `json:"tables"` 348 | Type string `json:"type"` 349 | } `json:"results"` 350 | Type string `json:"type"` 351 | } 352 | 353 | type BalanceSheet struct { 354 | AccountsPayable *float64 `json:"accounts_payable"` 355 | AccountsReceivable *float64 `json:"accounts_receivable"` 356 | AccumulatedDepreciation *float64 `json:"accumulated_depreciation"` 357 | CapitalStock *float64 `json:"capital_stock"` 358 | CashAndCashEquivalents *float64 `json:"cash_and_cash_equivalents"` 359 | CashCashEquivalentsAndMarketableSecurities *float64 `json:"cash_cash_equivalents_and_marketable_securities"` 360 | CommercialPaper *float64 `json:"commercial_paper"` 361 | CommonStock *float64 `json:"common_stock"` 362 | CommonStockEquity *float64 `json:"common_stock_equity"` 363 | CurrencyID *string `json:"currency_id"` 364 | CurrentAccruedExpenses *float64 `json:"current_accrued_expenses"` 365 | CurrentAssets *float64 `json:"current_assets"` 366 | CurrentDebt *float64 `json:"current_debt"` 367 | CurrentDebtAndCapitalLeaseObligation *float64 `json:"current_debt_and_capital_lease_obligation"` 368 | CurrentDeferredLiabilities *float64 `json:"current_deferred_liabilities"` 369 | CurrentDeferredRevenue *float64 `json:"current_deferred_revenue"` 370 | CurrentLiabilities *float64 `json:"current_liabilities"` 371 | FileDate *string `json:"file_date"` 372 | FiscalYearEnd *string `json:"fiscal_year_end"` 373 | GainsLossesNotAffectingRetainedEarnings *float64 `json:"gains_losses_not_affecting_retained_earnings"` 374 | Goodwill *float64 `json:"goodwill"` 375 | GoodwillAndOtherIntangibleAssets *float64 `json:"goodwill_and_other_int64angible_assets"` 376 | GrossPPE *float64 `json:"gross_p_p_e"` 377 | Inventory *float64 `json:"inventory"` 378 | InvestedCapital *float64 `json:"invested_capital"` 379 | InvestmentsAndAdvances *float64 `json:"investments_and_advances"` 380 | LandAndImprovements *float64 `json:"land_and_improvements"` 381 | Leases *float64 `json:"leases"` 382 | LongTermDebt *float64 `json:"long_term_debt"` 383 | LongTermDebtAndCapitalLeaseObligation *float64 `json:"long_term_debt_and_capital_lease_obligation"` 384 | MachineryFurnitureEquipment *float64 `json:"machinery_furniture_equipment"` 385 | NetDebt *float64 `json:"net_debt"` 386 | NetPPE *float64 `json:"net_p_p_e"` 387 | NetTangibleAssets *float64 `json:"net_tangible_assets"` 388 | NonCurrentDeferredLiabilities *float64 `json:"non_current_deferred_liabilities"` 389 | NonCurrentDeferredRevenue *float64 `json:"non_current_deferred_revenue"` 390 | NonCurrentDeferredTaxesLiabilities *float64 `json:"non_current_deferred_taxes_liabilities"` 391 | NumberOfShareHolders *int64 `json:"number_of_share_holders"` 392 | OrdinarySharesNumber *float64 `json:"ordinary_shares_number"` 393 | OtherCurrentAssets *float64 `json:"other_current_assets"` 394 | OtherCurrentBorrowings *float64 `json:"other_current_borrowings"` 395 | OtherIntangibleAssets *float64 `json:"other_int64angible_assets"` 396 | OtherNonCurrentAssets *float64 `json:"other_non_current_assets"` 397 | OtherNonCurrentLiabilities *float64 `json:"other_non_current_liabilities"` 398 | OtherReceivables *float64 `json:"other_receivables"` 399 | OtherShortTermInvestments *float64 `json:"other_short_term_investments"` 400 | Payables *float64 `json:"payables"` 401 | PayablesAndAccruedExpenses *float64 `json:"payables_and_accrued_expenses"` 402 | Period *string `json:"period"` 403 | PeriodEndingDate *string `json:"period_ending_date"` 404 | Receivables *float64 `json:"receivables"` 405 | ReportType *string `json:"report_type"` 406 | RetainedEarnings *float64 `json:"retained_earnings"` 407 | ShareIssued *float64 `json:"share_issued"` 408 | StockholdersEquity *float64 `json:"stockholders_equity"` 409 | TangibleBookValue *float64 `json:"tangible_book_value"` 410 | TotalAssets *float64 `json:"total_assets"` 411 | TotalCapitalization *float64 `json:"total_capitalization"` 412 | TotalDebt *float64 `json:"total_debt"` 413 | TotalEquity *float64 `json:"total_equity"` 414 | TotalEquityGrossMinorityInterest *float64 `json:"total_equity_gross_minority_int64erest"` 415 | TotalLiabilities *float64 `json:"total_liabilities"` 416 | TotalLiabilitiesNetMinorityInterest *float64 `json:"total_liabilities_net_minority_int64erest"` 417 | TotalNonCurrentAssets *float64 `json:"total_non_current_assets"` 418 | TotalNonCurrentLiabilities *float64 `json:"total_non_current_liabilities"` 419 | TotalNonCurrentLiabilitiesNetMinorityInterest *float64 `json:"total_non_current_liabilities_net_minority_int64erest"` 420 | WorkingCapital *float64 `json:"working_capital"` 421 | } 422 | 423 | type CashFlowStatement struct { 424 | BeginningCashPosition *float64 `json:"beginning_cash_position"` 425 | CapitalExpenditure *float64 `json:"capital_expenditure"` 426 | CashDividendsPaid *float64 `json:"cash_dividends_paid"` 427 | ChangeInAccountPayable *float64 `json:"change_in_account_payable"` 428 | ChangeInInventory *float64 `json:"change_in_inventory"` 429 | ChangeInOtherWorkingCapital *float64 `json:"change_in_other_working_capital"` 430 | ChangeInPayable *float64 `json:"change_in_payable"` 431 | ChangeInPayablesAndAccruedExpense *float64 `json:"change_in_payables_and_accrued_expense"` 432 | ChangeInReceivables *float64 `json:"change_in_receivables"` 433 | ChangeInWorkingCapital *float64 `json:"change_in_working_capital"` 434 | ChangesInAccountReceivables *float64 `json:"changes_in_account_receivables"` 435 | ChangesInCash *float64 `json:"changes_in_cash"` 436 | CommonStockIssuance *float64 `json:"common_stock_issuance"` 437 | CommonStockPayments *float64 `json:"common_stock_payments"` 438 | CurrencyID *string `json:"currency_id"` 439 | DeferredIncomeTax *float64 `json:"deferred_income_tax"` 440 | DeferredTax *float64 `json:"deferred_tax"` 441 | DepreciationAmortizationDepletion *float64 `json:"depreciation_amortization_depletion"` 442 | DepreciationAndAmortization *float64 `json:"depreciation_and_amortization"` 443 | DomesticSales *float64 `json:"domestic_sales"` 444 | EndCashPosition *float64 `json:"end_cash_position"` 445 | FileDate *string `json:"file_date"` 446 | FinancingCashFlow *float64 `json:"financing_cash_flow"` 447 | FiscalYearEnd *string `json:"fiscal_year_end"` 448 | ForeignSales *float64 `json:"foreign_sales"` 449 | FreeCashFlow *float64 `json:"free_cash_flow"` 450 | IncomeTaxPaidSupplementalData *float64 `json:"income_tax_paid_supplemental_data"` 451 | InterestPaidSupplementalData *float64 `json:"int64erest_paid_supplemental_data"` 452 | InvestingCashFlow *float64 `json:"investing_cash_flow"` 453 | IssuanceOfCapitalStock *float64 `json:"issuance_of_capital_stock"` 454 | NetBusinessPurchaseAndSale *float64 `json:"net_business_purchase_and_sale"` 455 | NetCommonStockIssuance *float64 `json:"net_common_stock_issuance"` 456 | NetIncome *float64 `json:"net_income"` 457 | NetIncomeFromContinuingOperations *float64 `json:"net_income_from_continuing_operations"` 458 | NetIntangiblesPurchaseAndSale *float64 `json:"net_int64angibles_purchase_and_sale"` 459 | NetInvestmentPurchaseAndSale *float64 `json:"net_investment_purchase_and_sale"` 460 | NetIssuancePaymentsOfDebt *float64 `json:"net_issuance_payments_of_debt"` 461 | NetOtherFinancingCharges *float64 `json:"net_other_financing_charges"` 462 | NetOtherInvestingChanges *float64 `json:"net_other_investing_changes"` 463 | NetPPEPurchaseAndSale *float64 `json:"net_p_p_e_purchase_and_sale"` 464 | NetShortTermDebtIssuance *float64 `json:"net_short_term_debt_issuance"` 465 | NumberOfShareHolders *int64 `json:"number_of_share_holders"` 466 | OperatingCashFlow *float64 `json:"operating_cash_flow"` 467 | OtherNonCashItems *float64 `json:"other_non_cash_items"` 468 | Period *string `json:"period"` 469 | PeriodEndingDate *string `json:"period_ending_date"` 470 | PurchaseOfBusiness *float64 `json:"purchase_of_business"` 471 | PurchaseOfIntangibles *float64 `json:"purchase_of_int64angibles"` 472 | PurchaseOfInvestment *float64 `json:"purchase_of_investment"` 473 | PurchaseOfPPE *float64 `json:"purchase_of_p_p_e"` 474 | ReportType *string `json:"report_type"` 475 | RepurchaseOfCapitalStock *float64 `json:"repurchase_of_capital_stock"` 476 | SaleOfInvestment *float64 `json:"sale_of_investment"` 477 | StockBasedCompensation *float64 `json:"stock_based_compensation"` 478 | } 479 | 480 | type IncomeStatement struct { 481 | AccessionNumber *string `json:"accession_number"` 482 | CostOfRevenue *float64 `json:"cost_of_revenue"` 483 | CurrencyID *string `json:"currency_id"` 484 | EBIT *float64 `json:"e_b_i_t"` 485 | EBITDA *float64 `json:"e_b_i_t_d_a"` 486 | FileDate *string `json:"file_date"` 487 | FiscalYearEnd *string `json:"fiscal_year_end"` 488 | FormType *string `json:"form_type"` 489 | GrossProfit *float64 `json:"gross_profit"` 490 | InterestExpense *float64 `json:"int64erest_expense"` 491 | InterestExpenseNonOperating *float64 `json:"int64erest_expense"` 492 | InterestIncome *float64 `json:"int64erest_income"` 493 | InterestIncomeNonOperating *float64 `json:"int64erest_income_non_operating"` 494 | InterestAndSimilarIncome *float64 `json:"int64erestand_similar_income"` 495 | NetIncome *float64 `json:"net_income"` 496 | NetIncomeCommonStockholders *float64 `json:"net_income_common_stockholders"` 497 | NetIncomeContinuousOperations *float64 `json:"net_income_continuous_operations"` 498 | NetIncomeFromContinuingAndDiscontinuedOperation *float64 `json:"net_income_from_continuing_and_discontinued_operation"` 499 | NetIncomeFromContinuingOperationNetMinorityInterest *float64 `json:"net_income_from_continuing_operation_net_minority_int64erest"` 500 | NetIncomeIncludingNoncontrollingInterests *float64 `json:"net_income_including_noncontrolling_int64erests"` 501 | NetInterestIncome *float64 `json:"net_int64erest_income"` 502 | NetNonOperatingInterestIncomeExpense *float64 `json:"net_non_operating_int64erest_income_expense"` 503 | NonOperatingExpenses *float64 `json:"non_operating_expenses"` 504 | NonOperatingIncome *float64 `json:"non_operating_income"` 505 | NormalizedEBITDA *float64 `json:"normalized_e_b_i_t_d_a"` 506 | NormalizedIncome *float64 `json:"normalized_income"` 507 | NumberOfShareHolders *int64 `json:"number_of_share_holders"` 508 | OperatingExpense *float64 `json:"operating_expense"` 509 | OperatingIncome *float64 `json:"operating_income"` 510 | OperatingRevenue *float64 `json:"operating_revenue"` 511 | OtherIncomeExpense *float64 `json:"other_income_expense"` 512 | Period *string `json:"period"` 513 | PeriodEndingDate *string `json:"period_ending_date"` 514 | PretaxIncome *float64 `json:"pretax_income"` 515 | ReconciledCostOfRevenue *float64 `json:"reconciled_cost_of_revenue"` 516 | ReconciledDepreciation *float64 `json:"reconciled_depreciation"` 517 | ReportType *string `json:"report_type"` 518 | ResearchAndDevelopment *float64 `json:"research_and_development"` 519 | SellingGeneralAndAdministration *float64 `json:"selling_general_and_administration"` 520 | TaxEffectOfUnusualItems *float64 `json:"tax_effect_of_unusual_items"` 521 | TaxProvision *float64 `json:"tax_provision"` 522 | TaxRateForCalcs *float64 `json:"tax_rate_for_calcs"` 523 | TotalExpenses *float64 `json:"total_expenses"` 524 | TotalRevenue *float64 `json:"total_revenue"` 525 | } 526 | 527 | type BalanceSheetResults []map[string]BalanceSheet 528 | 529 | func (bsr *BalanceSheetResults) UnmarshalJSON(data []byte) error { 530 | results := make([]map[string]BalanceSheet, 0) 531 | if err := json.Unmarshal(data, &results); err == nil { 532 | *bsr = results 533 | return nil 534 | } 535 | 536 | r := make(map[string]BalanceSheet) 537 | err := json.Unmarshal(data, &r) 538 | if err == nil { 539 | *bsr = []map[string]BalanceSheet{r} 540 | } 541 | return err 542 | } 543 | 544 | type CashFlowStatements []map[string]CashFlowStatement 545 | 546 | func (cfs *CashFlowStatements) UnmarshalJSON(data []byte) error { 547 | results := make([]map[string]CashFlowStatement, 0) 548 | if err := json.Unmarshal(data, &results); err == nil { 549 | *cfs = results 550 | return nil 551 | } 552 | 553 | r := make(map[string]CashFlowStatement) 554 | err := json.Unmarshal(data, &r) 555 | if err == nil { 556 | *cfs = []map[string]CashFlowStatement{r} 557 | } 558 | return err 559 | } 560 | 561 | type IncomeStatements []map[string]IncomeStatement 562 | 563 | func (is *IncomeStatements) UnmarshalJSON(data []byte) error { 564 | results := make([]map[string]IncomeStatement, 0) 565 | if err := json.Unmarshal(data, &results); err == nil { 566 | *is = results 567 | return nil 568 | } 569 | 570 | r := make(map[string]IncomeStatement) 571 | err := json.Unmarshal(data, &r) 572 | if err == nil { 573 | *is = []map[string]IncomeStatement{r} 574 | } 575 | return err 576 | } 577 | 578 | type FinancialStatementsRestate struct { 579 | AsOfDate *string `json:"as_of_date"` 580 | BalanceSheet BalanceSheetResults `json:"balance_sheet"` 581 | CashFlowStatement CashFlowStatements `json:"cash_flow_statement"` 582 | CompanyID *string `json:"company_id"` 583 | IncomeStatement IncomeStatements `json:"income_statement"` 584 | } 585 | 586 | type Segmentation struct { 587 | AsOfDate *string `json:"as_of_date"` 588 | CompanyID *string `json:"company_id"` 589 | DepreciationAndAmortization *float64 `json:"depreciation_and_amortization"` 590 | OperatingIncome *float64 `json:"operating_income"` 591 | OperatingRevenue *float64 `json:"operating_revenue"` 592 | Period *string `json:"period"` 593 | TotalAssets *float64 `json:"total_assets"` 594 | } 595 | 596 | type EarningReport struct { 597 | AccessionNumber *string `json:"accession_number"` 598 | AsOfDate *string `json:"as_of_date"` 599 | BasicAverageShares *float64 `json:"basic_average_shares"` 600 | BasicContinuousOperations *float64 `json:"basic_continuous_operations"` 601 | BasicEPS *float64 `json:"basic_e_p_s"` 602 | ContinuingAndDiscontinuedBasicEPS *float64 `json:"continuing_and_discontinued_basic_e_p_s"` 603 | ContinuingAndDiscontinuedDilutedEPS *float64 `json:"continuing_and_discontinued_diluted_e_p_s"` 604 | CurrencyID *string `json:"currency_id"` 605 | DilutedAverageShares *float64 `json:"diluted_average_shares"` 606 | DilutedContinuousOperations *float64 `json:"diluted_continuous_operations"` 607 | DilutedEPS *float64 `json:"diluted_e_p_s"` 608 | DividendPerShare *float64 `json:"dividend_per_share"` 609 | FileDate *string `json:"file_date"` 610 | FiscalYearEnd *string `json:"fiscal_year_end"` 611 | FormType *string `json:"form_type"` 612 | NormalizedBasicEPS *float64 `json:"normalized_basic_e_p_s"` 613 | NormalizedDilutedEPS *float64 `json:"normalized_diluted_e_p_s"` 614 | Period *string `json:"period"` 615 | PeriodEndingDate *string `json:"period_ending_date"` 616 | ReportType *string `json:"report_type"` 617 | ShareClassID *string `json:"share_class_id"` 618 | } 619 | 620 | type HistoricalReturns struct { 621 | AsOfDate *string `json:"as_of_date"` 622 | Period *string `json:"period"` 623 | ShareClassID *string `json:"share_class_id"` 624 | TotalReturn *float64 `json:"total_return"` 625 | } 626 | 627 | type EarningReports []map[string]EarningReport 628 | 629 | func (ers *EarningReports) UnmarshalJSON(data []byte) error { 630 | results := make([]map[string]EarningReport, 0) 631 | if err := json.Unmarshal(data, &results); err == nil { 632 | *ers = results 633 | return nil 634 | } 635 | 636 | r := make(map[string]EarningReport) 637 | err := json.Unmarshal(data, &r) 638 | if err == nil { 639 | *ers = []map[string]EarningReport{r} 640 | } 641 | return err 642 | } 643 | 644 | type GetFinancialsResponse []struct { 645 | Error string 646 | Request string `json:"request"` 647 | Results []struct { 648 | ID string `json:"id"` 649 | Tables struct { 650 | FinancialStatementsRestate *FinancialStatementsRestate `json:"financial_statements_restate"` 651 | Segmentation map[string]Segmentation `json:"segmentation"` 652 | EarningReportsAOR EarningReports `json:"earning_reports_a_o_r"` 653 | EarningReportsRestate EarningReports `json:"earning_reports_restate"` 654 | HistoricalReturns map[string]HistoricalReturns `json:"historical_returns"` 655 | } `json:"tables"` 656 | Type string `json:"type"` 657 | } `json:"results"` 658 | Type string `json:"type"` 659 | } 660 | 661 | type OperationRatio struct { 662 | AsOfDate *string `json:"as_of_date"` 663 | AssetsTurnover *float64 `json:"assets_turnover"` 664 | CapExSalesRatio *float64 `json:"cap_ex_sales_ratio"` 665 | CashConversionCycle *float64 `json:"cash_conversion_cycle"` 666 | CommonEquityToAssets *float64 `json:"common_equity_to_assets"` 667 | CompanyID *string `json:"company_id"` 668 | CurrentRatio *float64 `json:"current_ratio"` 669 | DaysInInventory *float64 `json:"days_in_inventory"` 670 | DaysInPayment *float64 `json:"days_in_payment"` 671 | DaysInSales *float64 `json:"days_in_sales"` 672 | DebtToAssets *float64 `json:"debt_to_assets"` 673 | EBITDAMargin *float64 `json:"e_b_i_t_d_a_margin"` 674 | EBITMargin *float64 `json:"e_b_i_t_margin"` 675 | FCFNetIncomeRatio *float64 `json:"f_c_f_net_income_ratio"` 676 | FCFSalesRatio *float64 `json:"f_c_f_sales_ratio"` 677 | FinancialLeverage *float64 `json:"financial_leverage"` 678 | FiscalYearEnd *string `json:"fiscal_year_end"` 679 | FixAssetsTurnover *float64 `json:"fix_assets_turonver"` 680 | GrossMargin *float64 `json:"gross_margin"` 681 | InterestCoverage *float64 `json:"int64erest_coverage"` 682 | InventoryTurnover *float64 `json:"inventory_turnover"` 683 | LongTermDebtEquityRatio *float64 `json:"long_term_debt_equity_ratio"` 684 | LongTermDebtTotalCapitalRatio *float64 `json:"long_term_debt_total_capital_ratio"` 685 | NetIncomeGrowth *float64 `json:"net_income_growth"` 686 | NetIncomeContOpsGrowth *float64 `json:"net_income_cont_ops_growth"` 687 | NetMargin *float64 `json:"net_margin"` 688 | NormalizedNetProfitMargin *float64 `json:"normalized_net_profit_margin"` 689 | NormalizedROIC *float64 `json:"normalized_r_o_i_c"` 690 | OperationIncomeGrowth *float64 `json:"operation_income_growth"` 691 | OperationMargin *float64 `json:"operation_margin"` 692 | PaymentTurnover *float64 `json:"payment_turnover"` 693 | Period *string `json:"period"` 694 | PretaxMargin *float64 `json:"pretax_margin"` 695 | QuickRatio *float64 `json:"quick_ratio"` 696 | ROA *float64 `json:"r_o_a"` 697 | ROE *float64 `json:"r_o_e"` 698 | ROIC *float64 `json:"r_o_i_c"` 699 | ReceivableTurnover *float64 `json:"receivable_turnover"` 700 | ReportType *string `json:"report_type"` 701 | SalesPerEmployee *float64 `json:"sales_per_employee"` 702 | TaxRate *float64 `json:"tax_rate"` 703 | TotalDebtEquityRatio *float64 `json:"total_debt_equity_ratio"` 704 | } 705 | 706 | type AlphaBeta struct { 707 | Alpha *float64 `json:"alpha"` 708 | AsOfDate *string `json:"as_of_date"` 709 | Beta *float64 `json:"beta"` 710 | NonDivAlpha *float64 `json:"non_div_alpha"` 711 | NonDivBeta *float64 `json:"non_div_beta"` 712 | Period *string `json:"period"` 713 | ShareClassID *string `json:"share_class_id"` 714 | } 715 | 716 | type EarningsRatiosRestate struct { 717 | AsOfDate *string `json:"as_of_date"` 718 | DPSGrowth *float64 `json:"d_p_s_growth"` 719 | DilutedContEPSGrowth *float64 `json:"diluted_cont_e_p_s_growth"` 720 | DilutedEPSGrowth *float64 `json:"diluted_e_p_s_growth"` 721 | FiscalYearEnd *string `json:"fiscal_year_end"` 722 | Period *string `json:"period"` 723 | ReportType *string `json:"report_type"` 724 | ShareClassID *string `json:"share_class_id"` 725 | } 726 | 727 | type ValuationRatios struct { 728 | AsOfDate *string `json:"as_of_date"` 729 | BookValuePerShare *float64 `json:"book_value_per_share"` 730 | BookValueYield *float64 `json:"book_value_yield"` 731 | BuyBackYield *float64 `json:"buy_back_yield"` 732 | CFOPerShare *float64 `json:"c_f_o_per_share"` 733 | CFYield *float64 `json:"c_f_yield"` 734 | CashReturn *float64 `json:"cash_return"` 735 | DividendRate *float64 `json:"dividend_rate"` 736 | DividendYield *float64 `json:"dividend_yield"` 737 | EVToEBITDA *float64 `json:"e_v_to_e_b_i_t_d_a"` 738 | EarningYield *float64 `json:"earning_yield"` 739 | FCFPerShare *float64 `json:"f_c_f_per_share"` 740 | FCFRatio *float64 `json:"f_c_f_ratio"` 741 | FCFYield *float64 `json:"f_c_f_yield"` 742 | ForwardDividendYield *float64 `json:"forward_dividend_yield"` 743 | ForwardEarningYield *float64 `json:"forward_earning_yield"` 744 | ForwardPERatio *float64 `json:"forward_p_e_ratio"` 745 | NormalizedPERatio *float64 `json:"normalized_p_e_ratio"` 746 | PBRatio *float64 `json:"p_b_ratio"` 747 | PCFRatio *float64 `json:"p_c_f_ratio"` 748 | PEGPayback *float64 `json:"p_e_g_payback"` 749 | PEGRatio *float64 `json:"p_e_g_ratio"` 750 | PERatio *float64 `json:"p_e_ratio"` 751 | PSRatio *float64 `json:"p_s_ratio"` 752 | PayoutRatio *float64 `json:"payout_ratio"` 753 | PriceChange1M *float64 `json:"price_change1_m"` 754 | PriceToEBITDA *float64 `json:"priceto_e_b_i_t_d_a"` 755 | RatioPE5YearAverage *float64 `json:"ratio_p_e5_year_average"` 756 | SalesPerShare *float64 `json:"sales_per_share"` 757 | SalesYield *float64 `json:"sales_yield"` 758 | ShareClassID *string `json:"share_class_id"` 759 | SustainableGrowthRate *float64 `json:"sustainable_growth_rate"` 760 | TangibleBVPerShare3YearAvg *float64 `json:"tangible_b_v_per_share3_yr_avg"` 761 | TangibleBVPerShare5YearAvg *float64 `json:"tangible_b_v_per_share5_yr_avg"` 762 | TangibleBookValuePerShare *float64 `json:"tangible_book_value_per_share"` 763 | TotalYield *float64 `json:"total_yield"` 764 | WorkingCapitalPerShare *float64 `json:"working_capital_per_share"` 765 | WorkingCapitalPerShare3YearAvg *float64 `json:"working_capital_per_share3_yr_avg"` 766 | WorkingCapitalPerShare5YearAvg *float64 `json:"working_capital_per_share5_yr_avg"` 767 | } 768 | 769 | type OperationRatios []map[string]OperationRatio 770 | 771 | func (ors *OperationRatios) UnmarshalJSON(data []byte) error { 772 | results := make([]map[string]OperationRatio, 0) 773 | if err := json.Unmarshal(data, &results); err == nil { 774 | *ors = results 775 | return nil 776 | } 777 | 778 | r := make(map[string]OperationRatio) 779 | err := json.Unmarshal(data, &r) 780 | if err == nil { 781 | *ors = []map[string]OperationRatio{r} 782 | } 783 | return err 784 | } 785 | 786 | type GetRatiosResponse []struct { 787 | Error string 788 | Request string `json:"request"` 789 | Results []struct { 790 | ID string `json:"id"` 791 | Tables struct { 792 | OperationRatiosAOR OperationRatios `json:"operation_ratios_a_o_r"` 793 | OperationRatiosRestate OperationRatios `json:"operation_ratios_restate"` 794 | AlphaBeta map[string]AlphaBeta `json:"alpha_beta"` 795 | EarningsRatiosRestate map[string]EarningsRatiosRestate `json:"earnings_ratios_restate"` 796 | ValuationRatios *ValuationRatios `json:"valuation_ratios"` 797 | } `json:"tables"` 798 | Type string `json:"type"` 799 | } `json:"results"` 800 | Type string `json:"type"` 801 | } 802 | 803 | type PriceStatistics struct { 804 | ShareClassID *string `json:"share_class_id"` 805 | AsOfDate *string `json:"as_of_date"` 806 | Period *string `json:"period"` 807 | ArithmeticMean *float64 `json:"arithmetic_mean"` 808 | AverageVolume *float64 `json:"average_volume"` 809 | Best3MonthTotalReturn *float64 `json:"best3_month_return_total"` 810 | ClosePriceToMovingAverage *float64 `json:"close_price_to_moving_average"` 811 | HighPrice *float64 `json:"high_price"` 812 | LowPrice *float64 `json:"low_price"` 813 | MovingAveragePrice *float64 `json:"moving_average_price"` 814 | PercentageBelowHighPrice *float64 `json:"percentage_below_high_price"` 815 | StandardDeviation *float64 `json:"standard_deviation"` 816 | TotalVolume *float64 `json:"total_volume"` 817 | Worst3MonthTotalReturn *float64 `json:"worst3_month_total_return"` 818 | } 819 | 820 | type TrailingReturns struct { 821 | ShareClassID *string `json:"share_class_id"` 822 | AsOfDate *string `json:"as_of_date"` 823 | Period *string `json:"period"` 824 | TotalReturn *float64 `json:"total_return"` 825 | } 826 | 827 | type GetPriceStatisticsResponse []struct { 828 | Error string 829 | Request string `json:"request"` 830 | Results []struct { 831 | ID string `json:"id"` 832 | Tables struct { 833 | PriceStatistics map[string]PriceStatistics `json:"price_statistics"` 834 | TrailingReturns map[string]TrailingReturns `json:"trailing_returns"` 835 | } `json:"tables"` 836 | Type string `json:"type"` 837 | } `json:"results"` 838 | Type string `json:"type"` 839 | } 840 | --------------------------------------------------------------------------------