├── Makefile ├── .circleci └── config.yml ├── .gitignore ├── go.mod ├── common ├── common_test.go └── credentials.go ├── v2 ├── stream │ ├── entities.go │ ├── stream.go │ ├── datav2_test.go │ └── datav2.go └── entities.go ├── Gopkg.toml ├── examples ├── v2stream │ └── v2stream.go ├── README.md ├── mean-reversion │ └── mean-reversion.go ├── martingale │ └── martingale.go └── long-short │ └── long-short.go ├── stream ├── stream_test.go └── stream.go ├── Gopkg.lock ├── README.md ├── polygon ├── polygon_test.go ├── stream.go ├── entities.go └── rest.go ├── go.sum ├── alpaca ├── stream.go ├── entities.go └── alpaca_test.go └── LICENSE /Makefile: -------------------------------------------------------------------------------- 1 | default: 2 | go test ./... 3 | -------------------------------------------------------------------------------- /.circleci/config.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | jobs: 3 | build: 4 | docker: 5 | - image: circleci/golang:1.14.13 6 | working_directory: /go/src/github.com/alpacahq/alpaca-trade-api-go 7 | steps: 8 | - checkout 9 | - run: go test -v -cover ./... 10 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Binaries for programs and plugins 2 | *.exe 3 | *.exe~ 4 | *.dll 5 | *.so 6 | *.dylib 7 | 8 | # Test binary, build with `go test -c` 9 | *.test 10 | 11 | # Output of the go coverage tool, specifically when used with LiteIDE 12 | *.out 13 | .vscode 14 | vendor 15 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/alpacahq/alpaca-trade-api-go 2 | 3 | go 1.14 4 | 5 | require ( 6 | github.com/gorilla/websocket v1.4.0 7 | github.com/shopspring/decimal v1.2.0 8 | github.com/stretchr/testify v1.4.0 9 | gopkg.in/matryer/try.v1 v1.0.0-20150601225556-312d2599e12e 10 | nhooyr.io/websocket v1.8.6 11 | ) 12 | -------------------------------------------------------------------------------- /common/common_test.go: -------------------------------------------------------------------------------- 1 | package common 2 | 3 | import ( 4 | "os" 5 | "testing" 6 | 7 | "github.com/stretchr/testify/assert" 8 | "github.com/stretchr/testify/suite" 9 | ) 10 | 11 | type CommonTestSuite struct { 12 | suite.Suite 13 | } 14 | 15 | var _ = setEnv() 16 | 17 | func setEnv() (s struct{}) { 18 | os.Setenv(EnvApiKeyID, "KEY_ID") 19 | os.Setenv(EnvApiSecretKey, "SECRET_KEY") 20 | return 21 | } 22 | 23 | func TestCommonTestSuite(t *testing.T) { 24 | suite.Run(t, new(CommonTestSuite)) 25 | } 26 | 27 | func (s *CommonTestSuite) TestCredentials() { 28 | assert.Equal(s.T(), "KEY_ID", Credentials().ID) 29 | assert.Equal(s.T(), "SECRET_KEY", Credentials().Secret) 30 | } 31 | -------------------------------------------------------------------------------- /v2/stream/entities.go: -------------------------------------------------------------------------------- 1 | package stream 2 | 3 | import "time" 4 | 5 | // Trade is a stock trade that happened on the market 6 | type Trade struct { 7 | ID int64 8 | Symbol string 9 | Exchange string 10 | Price float64 11 | Size uint32 12 | Timestamp time.Time 13 | Conditions []string 14 | Tape string 15 | } 16 | 17 | // Quote is a stock quote from the market 18 | type Quote struct { 19 | Symbol string 20 | BidExchange string 21 | BidPrice float64 22 | BidSize uint32 23 | AskExchange string 24 | AskPrice float64 25 | AskSize uint32 26 | Timestamp time.Time 27 | Conditions []string 28 | Tape string 29 | } 30 | 31 | // Bar is an aggregate of trades 32 | type Bar struct { 33 | Symbol string 34 | Open float64 35 | High float64 36 | Low float64 37 | Close float64 38 | Volume uint64 39 | Timestamp time.Time 40 | } 41 | -------------------------------------------------------------------------------- /common/credentials.go: -------------------------------------------------------------------------------- 1 | package common 2 | 3 | import ( 4 | "os" 5 | "sync" 6 | ) 7 | 8 | var ( 9 | once sync.Once 10 | key *APIKey 11 | ) 12 | 13 | const ( 14 | EnvApiKeyID = "APCA_API_KEY_ID" 15 | EnvApiSecretKey = "APCA_API_SECRET_KEY" 16 | EnvApiOAuth = "APCA_API_OAUTH" 17 | EnvPolygonKeyID = "POLY_API_KEY_ID" 18 | ) 19 | 20 | type APIKey struct { 21 | ID string 22 | Secret string 23 | OAuth string 24 | PolygonKeyID string 25 | } 26 | 27 | // Credentials returns the user's Alpaca API key ID 28 | // and secret for use through the SDK. 29 | func Credentials() *APIKey { 30 | var polygonKeyID string 31 | if s := os.Getenv(EnvPolygonKeyID); s != "" { 32 | polygonKeyID = s 33 | } else { 34 | polygonKeyID = os.Getenv(EnvApiKeyID) 35 | } 36 | return &APIKey{ 37 | ID: os.Getenv(EnvApiKeyID), 38 | PolygonKeyID: polygonKeyID, 39 | Secret: os.Getenv(EnvApiSecretKey), 40 | OAuth: os.Getenv(EnvApiOAuth), 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /Gopkg.toml: -------------------------------------------------------------------------------- 1 | # Gopkg.toml example 2 | # 3 | # Refer to https://github.com/golang/dep/blob/master/docs/Gopkg.toml.md 4 | # for detailed Gopkg.toml documentation. 5 | # 6 | # required = ["github.com/user/thing/cmd/thing"] 7 | # ignored = ["github.com/user/project/pkgX", "bitbucket.org/user/project/pkgA/pkgY"] 8 | # 9 | # [[constraint]] 10 | # name = "github.com/user/project" 11 | # version = "1.0.0" 12 | # 13 | # [[constraint]] 14 | # name = "github.com/user/project2" 15 | # branch = "dev" 16 | # source = "github.com/myfork/project2" 17 | # 18 | # [[override]] 19 | # name = "github.com/x/y" 20 | # version = "2.4.0" 21 | # 22 | # [prune] 23 | # non-go = false 24 | # go-tests = true 25 | # unused-packages = true 26 | 27 | 28 | [[constraint]] 29 | name = "github.com/gorilla/websocket" 30 | version = "1.2.0" 31 | 32 | [[constraint]] 33 | name = "github.com/nats-io/nats.go" 34 | version = "1.5.0" 35 | 36 | [[constraint]] 37 | name = "github.com/shopspring/decimal" 38 | version = "1.0.1" 39 | 40 | [[constraint]] 41 | name = "github.com/stretchr/testify" 42 | version = "1.2.2" 43 | 44 | [prune] 45 | go-tests = true 46 | unused-packages = true 47 | -------------------------------------------------------------------------------- /examples/v2stream/v2stream.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | 7 | "github.com/alpacahq/alpaca-trade-api-go/alpaca" 8 | "github.com/alpacahq/alpaca-trade-api-go/common" 9 | "github.com/alpacahq/alpaca-trade-api-go/v2/stream" 10 | ) 11 | 12 | func main() { 13 | // You can set your credentials here in the code, or (preferably) via the 14 | // APCA_API_KEY_ID and APCA_API_SECRET_KEY environment variables 15 | apiKey := "YOUR_API_KEY_HERE" 16 | apiSecret := "YOUR_API_SECRET_HERE" 17 | if common.Credentials().ID == "" { 18 | os.Setenv(common.EnvApiKeyID, apiKey) 19 | } 20 | if common.Credentials().Secret == "" { 21 | os.Setenv(common.EnvApiSecretKey, apiSecret) 22 | } 23 | 24 | // uncomment if you have PRO subscription 25 | // stream.UseFeed("sip") 26 | 27 | if err := stream.SubscribeTradeUpdates(tradeUpdateHandler); err != nil { 28 | panic(err) 29 | } 30 | 31 | if err := stream.SubscribeTrades(tradeHandler, "AAPL"); err != nil { 32 | panic(err) 33 | } 34 | if err := stream.SubscribeQuotes(quoteHandler, "MSFT"); err != nil { 35 | panic(err) 36 | } 37 | if err := stream.SubscribeBars(barHandler, "IBM"); err != nil { 38 | panic(err) 39 | } 40 | 41 | select {} 42 | } 43 | 44 | func tradeUpdateHandler(update alpaca.TradeUpdate) { 45 | fmt.Println("trade update", update) 46 | } 47 | 48 | func tradeHandler(trade stream.Trade) { 49 | fmt.Println("trade", trade) 50 | } 51 | 52 | func quoteHandler(quote stream.Quote) { 53 | fmt.Println("quote", quote) 54 | } 55 | 56 | func barHandler(bar stream.Bar) { 57 | fmt.Println("bar", bar) 58 | } 59 | -------------------------------------------------------------------------------- /stream/stream_test.go: -------------------------------------------------------------------------------- 1 | package stream 2 | 3 | import ( 4 | "fmt" 5 | "testing" 6 | 7 | "github.com/alpacahq/alpaca-trade-api-go/alpaca" 8 | 9 | "github.com/stretchr/testify/assert" 10 | "github.com/stretchr/testify/suite" 11 | ) 12 | 13 | type StreamTestSuite struct { 14 | suite.Suite 15 | alp, poly *MockStream 16 | } 17 | 18 | func TestStreamTestSuite(t *testing.T) { 19 | suite.Run(t, new(StreamTestSuite)) 20 | } 21 | 22 | func (s *StreamTestSuite) SetupSuite() { 23 | s.alp = &MockStream{} 24 | s.poly = &MockStream{} 25 | u = &Unified{ 26 | alpaca: s.alp, 27 | data: s.poly, 28 | } 29 | } 30 | 31 | func (s *StreamTestSuite) TestStream() { 32 | h := func(msg interface{}) {} 33 | 34 | // successful 35 | assert.Nil(s.T(), Register(alpaca.TradeUpdates, h)) 36 | assert.Nil(s.T(), Register(alpaca.AccountUpdates, h)) 37 | assert.Nil(s.T(), Register("T.*", h)) 38 | assert.Nil(s.T(), Unregister(alpaca.TradeUpdates)) 39 | assert.Nil(s.T(), Unregister(alpaca.AccountUpdates)) 40 | assert.Nil(s.T(), Unregister("T.*")) 41 | assert.Nil(s.T(), Close()) 42 | assert.Nil(s.T(), Deregister(alpaca.TradeUpdates)) 43 | assert.Nil(s.T(), Deregister(alpaca.AccountUpdates)) 44 | assert.Nil(s.T(), Deregister("T.*")) 45 | 46 | // failure 47 | s.alp.fail = true 48 | assert.NotNil(s.T(), Register(alpaca.TradeUpdates, h)) 49 | assert.NotNil(s.T(), Unregister(alpaca.TradeUpdates)) 50 | assert.NotNil(s.T(), Close()) 51 | assert.NotNil(s.T(), Deregister(alpaca.TradeUpdates)) 52 | } 53 | 54 | type MockStream struct { 55 | fail bool 56 | } 57 | 58 | func (ms *MockStream) Subscribe(key string, handler func(msg interface{})) error { 59 | if ms.fail { 60 | return fmt.Errorf("failed to subscribe") 61 | } 62 | 63 | return nil 64 | } 65 | 66 | func (ms *MockStream) Unsubscribe(key string) error { 67 | if ms.fail { 68 | return fmt.Errorf("failed to unsubscribe") 69 | } 70 | 71 | return nil 72 | } 73 | 74 | func (ms *MockStream) Close() error { 75 | if ms.fail { 76 | return fmt.Errorf("failed to close") 77 | } 78 | 79 | return nil 80 | } 81 | -------------------------------------------------------------------------------- /v2/entities.go: -------------------------------------------------------------------------------- 1 | package v2 2 | 3 | import "time" 4 | 5 | // Trade is a stock trade that happened on the market 6 | type Trade struct { 7 | ID int64 `json:"i"` 8 | Exchange string `json:"x"` 9 | Price float64 `json:"p"` 10 | Size uint32 `json:"s"` 11 | Timestamp time.Time `json:"t"` 12 | Conditions []string `json:"c"` 13 | Tape string `json:"z"` 14 | } 15 | 16 | // TradeItem contains a single trade or an error 17 | type TradeItem struct { 18 | Trade Trade 19 | Error error 20 | } 21 | 22 | // Quote is a stock quote from the market 23 | type Quote struct { 24 | BidExchange string `json:"bx"` 25 | BidPrice float64 `json:"bp"` 26 | BidSize uint32 `json:"bs"` 27 | AskExchange string `json:"ax"` 28 | AskPrice float64 `json:"ap"` 29 | AskSize uint32 `json:"as"` 30 | Timestamp time.Time `json:"t"` 31 | Conditions []string `json:"c"` 32 | Tape string `json:"z"` 33 | } 34 | 35 | // QuoteItem contains a single quote or an error 36 | type QuoteItem struct { 37 | Quote Quote 38 | Error error 39 | } 40 | 41 | // TimeFrame is the resolution of the bars 42 | type TimeFrame string 43 | 44 | // List of time frames 45 | const ( 46 | Min TimeFrame = "1Min" 47 | Hour TimeFrame = "1Hour" 48 | Day TimeFrame = "1Day" 49 | ) 50 | 51 | // Adjustment specifies the corporate action adjustment(s) for the bars 52 | type Adjustment string 53 | 54 | // List of adjustments 55 | const ( 56 | Raw Adjustment = "raw" 57 | Split Adjustment = "split" 58 | Dividend Adjustment = "dividend" 59 | All Adjustment = "all" 60 | ) 61 | 62 | // Bar is an aggregate of trades 63 | type Bar struct { 64 | Open float64 `json:"o"` 65 | High float64 `json:"h"` 66 | Low float64 `json:"l"` 67 | Close float64 `json:"c"` 68 | Volume uint64 `json:"v"` 69 | Timestamp time.Time `json:"t"` 70 | } 71 | 72 | // BarItem contains a single bar or an error 73 | type BarItem struct { 74 | Bar Bar 75 | Error error 76 | } 77 | 78 | // Snapshot is a snapshot of a symbol 79 | type Snapshot struct { 80 | LatestTrade *Trade `json:"latestTrade"` 81 | LatestQuote *Quote `json:"latestQuote"` 82 | MinuteBar *Bar `json:"minuteBar"` 83 | DailyBar *Bar `json:"dailyBar"` 84 | PrevDailyBar *Bar `json:"prevDailyBar"` 85 | } 86 | -------------------------------------------------------------------------------- /stream/stream.go: -------------------------------------------------------------------------------- 1 | package stream 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | "os" 7 | "sync" 8 | 9 | "github.com/alpacahq/alpaca-trade-api-go/alpaca" 10 | "github.com/alpacahq/alpaca-trade-api-go/polygon" 11 | ) 12 | 13 | var ( 14 | once sync.Once 15 | u *Unified 16 | 17 | dataStreamName string = "alpaca" 18 | ) 19 | 20 | func SetDataStream(streamName string) { 21 | switch streamName { 22 | case "alpaca": 23 | case "polygon": 24 | dataStreamName = streamName 25 | default: 26 | fmt.Fprintf(os.Stderr, "invalid data stream name %s\n", streamName) 27 | } 28 | } 29 | 30 | // Register a handler for a given stream, Alpaca or Polygon. 31 | func Register(stream string, handler func(msg interface{})) (err error) { 32 | once.Do(func() { 33 | if u == nil { 34 | 35 | var dataStream Stream 36 | if dataStreamName == "alpaca" { 37 | dataStream = alpaca.GetDataStream() 38 | } else if dataStreamName == "polygon" { 39 | dataStream = polygon.GetStream() 40 | } 41 | u = &Unified{ 42 | alpaca: alpaca.GetStream(), 43 | data: dataStream, 44 | } 45 | } 46 | }) 47 | 48 | switch stream { 49 | case alpaca.TradeUpdates: 50 | fallthrough 51 | case alpaca.AccountUpdates: 52 | err = u.alpaca.Subscribe(stream, handler) 53 | default: 54 | // data stream 55 | err = u.data.Subscribe(stream, handler) 56 | } 57 | 58 | return 59 | } 60 | 61 | // Deregister a handler for a given stream, Alpaca or Polygon. 62 | func Deregister(stream string) (err error) { 63 | once.Do(func() { 64 | if u == nil { 65 | err = errors.New("not yet subscribed to any channel") 66 | return 67 | } 68 | }) 69 | 70 | switch stream { 71 | case alpaca.TradeUpdates: 72 | fallthrough 73 | case alpaca.AccountUpdates: 74 | err = u.alpaca.Unsubscribe(stream) 75 | default: 76 | // data stream 77 | err = u.data.Unsubscribe(stream) 78 | } 79 | 80 | return 81 | } 82 | 83 | // Close gracefully closes all streams 84 | func Close() error { 85 | // close alpaca connection 86 | err1 := u.alpaca.Close() 87 | // close polygon connection 88 | err2 := u.data.Close() 89 | 90 | if err1 != nil { 91 | return err1 92 | } 93 | return err2 94 | } 95 | 96 | // Unified is the unified streaming structure combining the 97 | // interfaces from polygon and alpaca. 98 | type Unified struct { 99 | alpaca, data Stream 100 | } 101 | 102 | // Stream is the generic streaming interface implemented by 103 | // both alpaca and polygon. 104 | type Stream interface { 105 | Subscribe(key string, handler func(msg interface{})) error 106 | Unsubscribe(key string) error 107 | Close() error 108 | } 109 | -------------------------------------------------------------------------------- /Gopkg.lock: -------------------------------------------------------------------------------- 1 | # This file is autogenerated, do not edit; changes may be undone by the next 'dep ensure'. 2 | 3 | 4 | [[projects]] 5 | digest = "1:a2c1d0e43bd3baaa071d1b9ed72c27d78169b2b269f71c105ac4ba34b1be4a39" 6 | name = "github.com/davecgh/go-spew" 7 | packages = ["spew"] 8 | pruneopts = "UT" 9 | revision = "346938d642f2ec3594ed81d874461961cd0faa76" 10 | version = "v1.1.0" 11 | 12 | [[projects]] 13 | digest = "1:43dd08a10854b2056e615d1b1d22ac94559d822e1f8b6fcc92c1a1057e85188e" 14 | name = "github.com/gorilla/websocket" 15 | packages = ["."] 16 | pruneopts = "UT" 17 | revision = "ea4d1f681babbce9545c9c5f3d5194a789c89f5b" 18 | version = "v1.2.0" 19 | 20 | [[projects]] 21 | digest = "1:6ec5ca70ff99467ff3b134ca05ae1d247e42a0377a7c27a756ba932f4dfc3a88" 22 | name = "github.com/nats-io/go-nats" 23 | packages = [ 24 | ".", 25 | "encoders/builtin", 26 | "util", 27 | ] 28 | pruneopts = "UT" 29 | revision = "062418ea1c2181f52dc0f954f6204370519a868b" 30 | version = "v1.5.0" 31 | 32 | [[projects]] 33 | digest = "1:c3cd663f2f30b92536b9f290ac85c6310dae36a14cb8961553ae9ccf0d85ae41" 34 | name = "github.com/nats-io/nuid" 35 | packages = ["."] 36 | pruneopts = "UT" 37 | revision = "289cccf02c178dc782430d534e3c1f5b72af807f" 38 | version = "v1.0.0" 39 | 40 | [[projects]] 41 | digest = "1:0028cb19b2e4c3112225cd871870f2d9cf49b9b4276531f03438a88e94be86fe" 42 | name = "github.com/pmezard/go-difflib" 43 | packages = ["difflib"] 44 | pruneopts = "UT" 45 | revision = "792786c7400a136282c1664665ae0a8db921c6c2" 46 | version = "v1.0.0" 47 | 48 | [[projects]] 49 | digest = "1:81e02c4edb639c80559c0650f9401d3e2dcc3256d1fa215382bb7c83c1db9126" 50 | name = "github.com/shopspring/decimal" 51 | packages = ["."] 52 | pruneopts = "UT" 53 | revision = "cd690d0c9e2447b1ef2a129a6b7b49077da89b8e" 54 | version = "1.1.0" 55 | 56 | [[projects]] 57 | digest = "1:5110e3d4f130772fd39e6ce8208ad1955b242ccfcc8ad9d158857250579c82f4" 58 | name = "github.com/stretchr/testify" 59 | packages = [ 60 | "assert", 61 | "require", 62 | "suite", 63 | ] 64 | pruneopts = "UT" 65 | revision = "f35b8ab0b5a2cef36673838d662e249dd9c94686" 66 | version = "v1.2.2" 67 | 68 | [[projects]] 69 | digest = "1:cc9f0c72e45707522afc156b3d715443e2021acedff165a4803f72a0d2ed170f" 70 | name = "gopkg.in/matryer/try.v1" 71 | packages = ["."] 72 | pruneopts = "UT" 73 | revision = "312d2599e12e89ca89b52a09597394f449235d80" 74 | version = "v1" 75 | 76 | [solve-meta] 77 | analyzer-name = "dep" 78 | analyzer-version = 1 79 | input-imports = [ 80 | "github.com/gorilla/websocket", 81 | "github.com/nats-io/go-nats", 82 | "github.com/shopspring/decimal", 83 | "github.com/stretchr/testify/assert", 84 | "github.com/stretchr/testify/require", 85 | "github.com/stretchr/testify/suite", 86 | "gopkg.in/matryer/try.v1", 87 | ] 88 | solver-name = "gps-cdcl" 89 | solver-version = 1 90 | -------------------------------------------------------------------------------- /examples/README.md: -------------------------------------------------------------------------------- 1 | # Examples 2 | 3 | This directory contains example trading algorithms that connect to the paper-trading API. These scripts are meant as simple Go executibles, where you install the Alpaca package and build and run your Go executible. Please note you will need to replace the `API_KEY` and `API_SECRET` parameters at the top of the file with your own information from the [Alpaca dashboard](https://app.alpaca.markets/). Alternatively, you can set your environment variables "APCA_API_KEY_ID" and "APCA_API_SECRET_KEY", and the script will read your keys from there. Please also note that the performance of these scripts in a real trading environment is not guaranteed. While they are written with the goal of showing realistic uses of the SDK, there is no guarantee that the strategies they outline are a good fit for your own brokerage account. 4 | 5 | ## Mean Reversion 6 | 7 | This trading algorithm bases its strategy on a mean reversion theory, which essentially guesses that the stock price will correct to the mean. This means we'd want to execute trades when the stock price is below the running average, as the theory states that the stock price will eventually rise to the mean. The algorithm does this by taking the 20 minute running average stock price of a given stock (in this case "AAPL") and longs or sells based on the average. After every minute, the algorithm will re-evaluate the mean and see if adjustments to the position need to be made. For more information on this strategy, you can read [this link](https://medium.com/automation-generation/a-simple-mean-reversion-stock-trading-script-in-c-fdd3d147af95) detailing a mean reversion strategy in C#. 8 | 9 | ## Long-Short Equity 10 | 11 | This trading algorithm implements the long-short equity strategy. This means that the algorithm will rank a given universe of stocks based on a certain metric, and long the top ranked stocks and short the lower ranked stocks. More specifically, the algorithm uses the frequently used 130/30 percent equity split between longs and shorts (130% of equity used for longs, 30% of equity used for shorts). The algorithm will then grab the top and bottom 25% of stocks, and long or short them accordingly. The algorithm will purchase equal quantities across a bucket of stocks, so all stocks in the long bucket are ordered with the same quantity (same with the short bucket). After every minute, the algorithm will re-rank the stocks and make adjustments to the position if necessary. For more information on this strategy, read this link [here](https://www.investopedia.com/terms/l/long-shortequity.asp). 12 | 13 | Some stocks cannot be shorted. In this case, the algorithm uses the leftover equity from the stocks that could not be shorted and shorts the stocks have already been shorted. 14 | 15 | The algorithm uses percent change in stock price over the past 10 minutes to rank the stocks, where the stocks that rose the most are longed and the ones that sunk the most are shorted. -------------------------------------------------------------------------------- /v2/stream/stream.go: -------------------------------------------------------------------------------- 1 | package stream 2 | 3 | import ( 4 | "log" 5 | "sync" 6 | 7 | "github.com/alpacahq/alpaca-trade-api-go/alpaca" 8 | ) 9 | 10 | var ( 11 | once sync.Once 12 | dataStream *datav2stream 13 | alpacaStream *alpaca.Stream 14 | ) 15 | 16 | func initStreamsOnce() { 17 | once.Do(func() { 18 | if dataStream == nil { 19 | dataStream = newDatav2Stream() 20 | } 21 | if alpacaStream == nil { 22 | alpacaStream = alpaca.GetStream() 23 | } 24 | }) 25 | } 26 | 27 | // UseFeed sets the feed used by the data v2 stream. Supported feeds: iex, sip. 28 | func UseFeed(feed string) error { 29 | initStreamsOnce() 30 | return dataStream.useFeed(feed) 31 | } 32 | 33 | // SubscribeTrades issues a subscribe command to the given symbols and 34 | // registers the handler to be called for each trade. 35 | func SubscribeTrades(handler func(trade Trade), symbols ...string) error { 36 | initStreamsOnce() 37 | return dataStream.subscribeTrades(handler, symbols...) 38 | } 39 | 40 | // SubscribeQuotes issues a subscribe command to the given symbols and 41 | // registers the handler to be called for each quote. 42 | func SubscribeQuotes(handler func(quote Quote), symbols ...string) error { 43 | initStreamsOnce() 44 | return dataStream.subscribeQuotes(handler, symbols...) 45 | } 46 | 47 | // SubscribeBars issues a subscribe command to the given symbols and 48 | // registers the handler to be called for each bar. 49 | func SubscribeBars(handler func(bar Bar), symbols ...string) error { 50 | initStreamsOnce() 51 | return dataStream.subscribeBars(handler, symbols...) 52 | } 53 | 54 | // SubscribeTradeUpdates issues a subscribe command to the user's trade updates and 55 | // registers the handler to be called for each update. 56 | func SubscribeTradeUpdates(handler func(update alpaca.TradeUpdate)) error { 57 | initStreamsOnce() 58 | return alpacaStream.Subscribe(alpaca.TradeUpdates, func(msg interface{}) { 59 | update, ok := msg.(alpaca.TradeUpdate) 60 | if !ok { 61 | log.Printf("unexpected trade update: %v", msg) 62 | return 63 | } 64 | handler(update) 65 | }) 66 | } 67 | 68 | // UnsubscribeTrades issues an unsubscribe command for the given trade symbols 69 | func UnsubscribeTrades(symbols ...string) error { 70 | initStreamsOnce() 71 | return dataStream.unsubscribe(symbols, nil, nil) 72 | } 73 | 74 | // UnsubscribeQuotes issues an unsubscribe command for the given quote symbols 75 | func UnsubscribeQuotes(symbols ...string) error { 76 | initStreamsOnce() 77 | return dataStream.unsubscribe(nil, symbols, nil) 78 | } 79 | 80 | // UnsubscribeBars issues an unsubscribe command for the given bar symbols 81 | func UnsubscribeBars(symbols ...string) error { 82 | initStreamsOnce() 83 | return dataStream.unsubscribe(nil, nil, symbols) 84 | } 85 | 86 | // UnsubscribeTradeUpdates issues an unsubscribe command for the user's trade updates 87 | func UnsubscribeTradeUpdates() error { 88 | initStreamsOnce() 89 | return alpacaStream.Unsubscribe(alpaca.TradeUpdates) 90 | } 91 | 92 | // Close gracefully closes all streams 93 | func Close() error { 94 | var alpacaErr, dataErr error 95 | if alpacaStream != nil { 96 | alpacaErr = alpacaStream.Close() 97 | } 98 | if dataStream != nil { 99 | dataErr = dataStream.close(true) 100 | } 101 | if alpacaErr != nil { 102 | return alpacaErr 103 | } 104 | return dataErr 105 | } 106 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | # alpaca-trade-api-go 3 | 4 | [![CircleCI Status](https://circleci.com/gh/alpacahq/alpaca-trade-api-go.svg?style=svg)](https://circleci.com/gh/alpacahq/alpaca-trade-api-go) 5 | [![Go Report Card](https://goreportcard.com/badge/github.com/alpacahq/alpaca-trade-api-go)](https://goreportcard.com/report/github.com/alpacahq/alpaca-trade-api-go) 6 | 7 | `alpaca-trade-api-go` is a Go library for the Alpaca trade API. It allows rapid 8 | trading algo development easily, with support for the both REST and streaming interfaces. 9 | For details of each API behavior, please see the online API document. 10 | 11 | ## Installation 12 | 13 | ``` 14 | $ go get github.com/alpacahq/alpaca-trade-api-go/common 15 | $ go get github.com/alpacahq/alpaca-trade-api-go/polygon 16 | $ go get github.com/alpacahq/alpaca-trade-api-go/stream 17 | $ go get github.com/alpacahq/alpaca-trade-api-go/alpaca 18 | ``` 19 | 20 | ## Example 21 | 22 | In order to call Alpaca's trade API, you need to obtain an API key pair. 23 | Replace and with what you get from the web console. 24 | 25 | ### REST example 26 | 27 | ```go 28 | import ( 29 | "os" 30 | "fmt" 31 | 32 | "github.com/alpacahq/alpaca-trade-api-go/alpaca" 33 | "github.com/alpacahq/alpaca-trade-api-go/common" 34 | ) 35 | 36 | func init() { 37 | os.Setenv(common.EnvApiKeyID, "") 38 | os.Setenv(common.EnvApiSecretKey, "") 39 | 40 | fmt.Printf("Running w/ credentials [%v %v]\n", common.Credentials().ID, common.Credentials().Secret) 41 | 42 | alpaca.SetBaseUrl("https://paper-api.alpaca.markets") 43 | } 44 | 45 | func main() { 46 | alpacaClient := alpaca.NewClient(common.Credentials()) 47 | acct, err := alpacaClient.GetAccount() 48 | if err != nil { 49 | panic(err) 50 | } 51 | 52 | fmt.Println(*acct) 53 | } 54 | ``` 55 | 56 | ### Streaming example 57 | 58 | The SDK provides a unified streaming interface for both data updates 59 | (from Alpaca or Polygon), and Alpaca's trade/account updates. 60 | The following example subscribes to trade updates, and prints any messages received, 61 | and subscribes to live quotes for AAPL, and prints any quotes received. 62 | The main function also ends with an empty `select{}` statement which causes the 63 | program to run indefinitely. 64 | 65 | In order to use Polygon streaming, you need to call `stream.SetDataStream("polygon")`. 66 | This requires your Alpaca account to be eligible for Polygon integration 67 | (for details of the setup, please read Alpaca API document). 68 | ```go 69 | package main 70 | 71 | import ( 72 | "fmt" 73 | "os" 74 | 75 | "github.com/alpacahq/alpaca-trade-api-go/alpaca" 76 | "github.com/alpacahq/alpaca-trade-api-go/common" 77 | "github.com/alpacahq/alpaca-trade-api-go/stream" 78 | ) 79 | 80 | func main() { 81 | os.Setenv(common.EnvApiKeyID, "your_key_id") 82 | os.Setenv(common.EnvApiSecretKey, "your_secret_key") 83 | 84 | if err := stream.Register(alpaca.TradeUpdates, tradeHandler); err != nil { 85 | panic(err) 86 | } 87 | 88 | if err := stream.Register("Q.AAPL", quoteHandler); err != nil { 89 | panic(err) 90 | } 91 | 92 | select {} 93 | } 94 | 95 | func tradeHandler(msg interface{}) { 96 | tradeupdate := msg.(alpaca.TradeUpdate) 97 | fmt.Printf("%s event received for order %s.\n", tradeupdate.Event, tradeupdate.Order.ID) 98 | } 99 | 100 | func quoteHandler(msg interface{}) { 101 | quote := msg.(alpaca.StreamQuote) 102 | 103 | fmt.Println(quote.Symbol, quote.BidPrice, quote.BidSize, quote.AskPrice, quote.AskSize) 104 | } 105 | ``` 106 | 107 | #### Deregister 108 | You could also deregister from a channel. e.g: 109 | 110 | ```go 111 | if err := stream.Deregister("Q.AAPL"); err != nil { 112 | panic(err) 113 | } 114 | ``` 115 | 116 | ## API Document 117 | 118 | The HTTP API document is located at https://docs.alpaca.markets/ 119 | 120 | ## Authentication 121 | 122 | The Alpaca API requires API key ID and secret key, which you can obtain from 123 | the web console after you sign in. This key pair can then be applied to the SDK 124 | either by setting environment variables (`APCA_API_KEY_ID=` and `APCA_API_SECRET_KEY=`), 125 | or hardcoding them into the Go code directly as shown in the examples above. 126 | 127 | ```sh 128 | $ export APCA_API_KEY_ID=xxxxx 129 | $ export APCA_API_SECRET_KEY=yyyyy 130 | ``` 131 | 132 | ## Endpoint 133 | 134 | For paper trading, set the environment variable `APCA_API_BASE_URL`. 135 | 136 | ```sh 137 | $ export APCA_API_BASE_URL=https://paper-api.alpaca.markets 138 | ``` 139 | 140 | You can also instead use the function `alpaca.SetBaseUrl("https://paper-api.alpaca.markets")` 141 | to configure the endpoint. 142 | 143 | 144 | ## Running Multiple Strategies 145 | There's a way to execute more than one algorithm at once.
146 | The websocket connection is limited to 1 connection per account.
147 | For that exact purpose this ![project](https://github.com/shlomikushchi/alpaca-proxy-agent) was created
148 | The steps to execute this are: 149 | * Run the Alpaca Proxy Agent as described in the project's README 150 | * Define this env variable: `DATA_PROXY_WS` to be the address of the proxy agent. (e.g: `DATA_PROXY_WS=http://127.0.0.1:8765`) 151 | * execute your algorithm. it will connect to the servers through the proxy agent allowing you to execute multiple strategies 152 | 153 | note: this env variable could be used to proxy the data websocket through a custom server too. 154 | 155 | ## GoDoc 156 | 157 | For a more in-depth look at the SDK, see the 158 | [GoDoc](https://godoc.org/github.com/alpacahq/alpaca-trade-api-go) 159 | -------------------------------------------------------------------------------- /v2/stream/datav2_test.go: -------------------------------------------------------------------------------- 1 | package stream 2 | 3 | import ( 4 | "testing" 5 | "time" 6 | 7 | "github.com/stretchr/testify/assert" 8 | "github.com/stretchr/testify/require" 9 | "github.com/vmihailenco/msgpack/v5" 10 | ) 11 | 12 | // tradeWithT is the incoming trade message that also contains the T type key 13 | type tradeWithT struct { 14 | Type string `msgpack:"T"` 15 | ID int64 `msgpack:"i"` 16 | Symbol string `msgpack:"S"` 17 | Exchange string `msgpack:"x"` 18 | Price float64 `msgpack:"p"` 19 | Size uint32 `msgpack:"s"` 20 | Timestamp time.Time `msgpack:"t"` 21 | Conditions []string `msgpack:"c"` 22 | Tape string `msgpack:"z"` 23 | // NewField is for testing correct handling of added fields in the future 24 | NewField uint64 `msgpack:"n"` 25 | } 26 | 27 | // quoteWithT is the incoming quote message that also contains the T type key 28 | type quoteWithT struct { 29 | Type string `msgpack:"T"` 30 | Symbol string `msgpack:"S"` 31 | BidExchange string `msgpack:"bx"` 32 | BidPrice float64 `msgpack:"bp"` 33 | BidSize uint32 `msgpack:"bs"` 34 | AskExchange string `msgpack:"ax"` 35 | AskPrice float64 `msgpack:"ap"` 36 | AskSize uint32 `msgpack:"as"` 37 | Timestamp time.Time `msgpack:"t"` 38 | Conditions []string `msgpack:"c"` 39 | Tape string `msgpack:"z"` 40 | // NewField is for testing correct handling of added fields in the future 41 | NewField uint64 `msgpack:"n"` 42 | } 43 | 44 | // barWithT is the incoming bar message that also contains the T type key 45 | type barWithT struct { 46 | Type string `msgpack:"T"` 47 | Symbol string `msgpack:"S"` 48 | Open float64 `msgpack:"o"` 49 | High float64 `msgpack:"h"` 50 | Low float64 `msgpack:"l"` 51 | Close float64 `msgpack:"c"` 52 | Volume uint64 `msgpack:"v"` 53 | Timestamp time.Time `msgpack:"t"` 54 | // NewField is for testing correct handling of added fields in the future 55 | NewField uint64 `msgpack:"n"` 56 | } 57 | 58 | type other struct { 59 | Type string `msgpack:"T"` 60 | Whatever string `msgpack:"w"` 61 | } 62 | 63 | var testTime = time.Date(2021, 03, 04, 15, 16, 17, 18, time.UTC) 64 | 65 | var testTrade = tradeWithT{ 66 | Type: "t", 67 | ID: 42, 68 | Symbol: "TEST", 69 | Exchange: "X", 70 | Price: 100, 71 | Size: 10, 72 | Timestamp: testTime, 73 | Conditions: []string{" "}, 74 | Tape: "A", 75 | } 76 | 77 | var testQuote = quoteWithT{ 78 | Type: "q", 79 | Symbol: "TEST", 80 | BidExchange: "B", 81 | BidPrice: 99.9, 82 | BidSize: 100, 83 | AskExchange: "A", 84 | AskPrice: 100.1, 85 | AskSize: 200, 86 | Timestamp: testTime, 87 | Conditions: []string{"R"}, 88 | Tape: "B", 89 | } 90 | 91 | var testBar = barWithT{ 92 | Type: "b", 93 | Symbol: "TEST", 94 | Open: 100, 95 | High: 101.2, 96 | Low: 98.67, 97 | Close: 101.1, 98 | Volume: 2560, 99 | Timestamp: time.Date(2021, 03, 05, 16, 0, 0, 0, time.UTC), 100 | } 101 | 102 | var testOther = other{ 103 | Type: "o", 104 | Whatever: "whatever", 105 | } 106 | 107 | func TestHandleMessages(t *testing.T) { 108 | b, err := msgpack.Marshal([]interface{}{testOther, testTrade, testQuote, testBar}) 109 | require.NoError(t, err) 110 | 111 | s := &datav2stream{} 112 | var trade Trade 113 | s.tradeHandlers = map[string]func(trade Trade){ 114 | "TEST": func(got Trade) { 115 | trade = got 116 | }, 117 | } 118 | var quote Quote 119 | s.quoteHandlers = map[string]func(quote Quote){ 120 | "TEST": func(got Quote) { 121 | quote = got 122 | }, 123 | } 124 | var bar Bar 125 | s.barHandlers = map[string]func(bar Bar){ 126 | "TEST": func(got Bar) { 127 | bar = got 128 | }, 129 | } 130 | 131 | err = s.handleMessage(b) 132 | require.NoError(t, err) 133 | 134 | assert.EqualValues(t, 42, trade.ID) 135 | assert.EqualValues(t, "TEST", trade.Symbol) 136 | assert.EqualValues(t, "X", trade.Exchange) 137 | assert.EqualValues(t, 100, trade.Price) 138 | assert.EqualValues(t, 10, trade.Size) 139 | assert.True(t, trade.Timestamp.Equal(testTime)) 140 | assert.EqualValues(t, []string{" "}, trade.Conditions) 141 | assert.EqualValues(t, "A", trade.Tape) 142 | 143 | assert.EqualValues(t, "TEST", quote.Symbol) 144 | assert.EqualValues(t, "B", quote.BidExchange) 145 | assert.EqualValues(t, 99.9, quote.BidPrice) 146 | assert.EqualValues(t, 100, quote.BidSize) 147 | assert.EqualValues(t, "A", quote.AskExchange) 148 | assert.EqualValues(t, 100.1, quote.AskPrice) 149 | assert.EqualValues(t, 200, quote.AskSize) 150 | assert.True(t, quote.Timestamp.Equal(testTime)) 151 | assert.EqualValues(t, []string{"R"}, quote.Conditions) 152 | assert.EqualValues(t, "B", quote.Tape) 153 | 154 | assert.EqualValues(t, "TEST", bar.Symbol) 155 | assert.EqualValues(t, 100, bar.Open) 156 | assert.EqualValues(t, 101.2, bar.High) 157 | assert.EqualValues(t, 98.67, bar.Low) 158 | assert.EqualValues(t, 101.1, bar.Close) 159 | assert.EqualValues(t, 2560, bar.Volume) 160 | } 161 | 162 | func BenchmarkHandleMessages(b *testing.B) { 163 | msgs, _ := msgpack.Marshal([]interface{}{testTrade, testQuote, testBar}) 164 | s := &datav2stream{ 165 | tradeHandlers: map[string]func(trade Trade){ 166 | "*": func(trade Trade) {}, 167 | }, 168 | quoteHandlers: map[string]func(quote Quote){ 169 | "*": func(quote Quote) {}, 170 | }, 171 | barHandlers: map[string]func(bar Bar){ 172 | "*": func(bar Bar) {}, 173 | }, 174 | } 175 | 176 | b.ResetTimer() 177 | 178 | for i := 0; i < b.N; i++ { 179 | s.handleMessage(msgs) 180 | } 181 | } 182 | -------------------------------------------------------------------------------- /polygon/polygon_test.go: -------------------------------------------------------------------------------- 1 | package polygon 2 | 3 | import ( 4 | "bytes" 5 | "fmt" 6 | "io" 7 | "net/http" 8 | "net/url" 9 | "testing" 10 | "time" 11 | 12 | "github.com/stretchr/testify/assert" 13 | "github.com/stretchr/testify/suite" 14 | ) 15 | 16 | type PolygonTestSuite struct { 17 | suite.Suite 18 | } 19 | 20 | func TestPolygonTestSuite(t *testing.T) { 21 | suite.Run(t, new(PolygonTestSuite)) 22 | } 23 | 24 | func (s *PolygonTestSuite) TestPolygon() { 25 | // get historic aggregates 26 | { 27 | // successful 28 | get = func(u *url.URL) (*http.Response, error) { 29 | return &http.Response{ 30 | Body: genBody([]byte(aggBody)), 31 | }, nil 32 | } 33 | 34 | now := time.Now() 35 | limit := 1 36 | 37 | resp, err := GetHistoricAggregates("APCA", Minute, &now, &now, &limit) 38 | assert.Nil(s.T(), err) 39 | assert.NotNil(s.T(), resp) 40 | 41 | // api failure 42 | get = func(u *url.URL) (*http.Response, error) { 43 | return &http.Response{}, fmt.Errorf("fail") 44 | } 45 | 46 | resp, err = GetHistoricAggregates("APCA", Minute, &now, &now, &limit) 47 | assert.NotNil(s.T(), err) 48 | assert.Nil(s.T(), resp) 49 | } 50 | 51 | // get historic trades 52 | { 53 | // successful 54 | get = func(u *url.URL) (*http.Response, error) { 55 | return &http.Response{ 56 | Body: genBody([]byte(tradesBody)), 57 | }, nil 58 | } 59 | 60 | date := "2018-01-03" 61 | 62 | resp, err := GetHistoricTrades("APCA", date, nil) 63 | assert.Nil(s.T(), err) 64 | assert.NotNil(s.T(), resp) 65 | 66 | // api failure 67 | get = func(u *url.URL) (*http.Response, error) { 68 | return &http.Response{}, fmt.Errorf("fail") 69 | } 70 | 71 | resp, err = GetHistoricTrades("APCA", date, nil) 72 | assert.NotNil(s.T(), err) 73 | assert.Nil(s.T(), resp) 74 | } 75 | 76 | // get historic quotes 77 | { 78 | // successful 79 | get = func(u *url.URL) (*http.Response, error) { 80 | return &http.Response{ 81 | Body: genBody([]byte(quotesBody)), 82 | }, nil 83 | } 84 | 85 | date := "2018-01-03" 86 | 87 | resp, err := GetHistoricQuotes("APCA", date) 88 | assert.Nil(s.T(), err) 89 | assert.NotNil(s.T(), resp) 90 | 91 | // api failure 92 | get = func(u *url.URL) (*http.Response, error) { 93 | return &http.Response{}, fmt.Errorf("fail") 94 | } 95 | 96 | resp, err = GetHistoricQuotes("APCA", date) 97 | assert.NotNil(s.T(), err) 98 | assert.Nil(s.T(), resp) 99 | } 100 | 101 | // get exchange data 102 | { 103 | // successful 104 | get = func(u *url.URL) (*http.Response, error) { 105 | return &http.Response{ 106 | Body: genBody([]byte(exchangeBody)), 107 | }, nil 108 | } 109 | 110 | resp, err := GetStockExchanges() 111 | assert.Nil(s.T(), err) 112 | assert.NotNil(s.T(), resp) 113 | 114 | // api failure 115 | get = func(u *url.URL) (*http.Response, error) { 116 | return &http.Response{}, fmt.Errorf("fail") 117 | } 118 | 119 | resp, err = GetStockExchanges() 120 | assert.NotNil(s.T(), err) 121 | assert.Nil(s.T(), resp) 122 | } 123 | } 124 | 125 | type nopCloser struct { 126 | io.Reader 127 | } 128 | 129 | func (nopCloser) Close() error { return nil } 130 | 131 | func genBody(buf []byte) io.ReadCloser { 132 | return nopCloser{bytes.NewBuffer(buf)} 133 | } 134 | 135 | const ( 136 | aggBody = `{ 137 | "symbol": "APCA", 138 | "aggType": "min", 139 | "map": { 140 | "o": "open", 141 | "c": "close", 142 | "h": "high", 143 | "l": "low", 144 | "v": "volume", 145 | "t": "timestamp" 146 | }, 147 | "ticks": [ 148 | { 149 | "o": 47.53, 150 | "c": 47.53, 151 | "h": 47.53, 152 | "l": 47.53, 153 | "v": 16100, 154 | "t": 1199278800000 155 | } 156 | ] 157 | }` 158 | quotesBody = `{ 159 | "day": "2018-01-03", 160 | "map": { 161 | "aE": "askexchange", 162 | "aP": "askprice", 163 | "aS": "asksize", 164 | "bE": "bidexchange", 165 | "bP": "bidprice", 166 | "bS": "bidsize", 167 | "c": "condition", 168 | "t": "timestamp" 169 | }, 170 | "msLatency": 7, 171 | "status": "success", 172 | "symbol": "APCA", 173 | "ticks": [ 174 | { 175 | "c": 0, 176 | "bE": "8", 177 | "aE": "11", 178 | "bP": 98.79, 179 | "aP": 98.89, 180 | "bS": 5, 181 | "aS": 1, 182 | "t": 1514938489451 183 | } 184 | ], 185 | "type": "quotes" 186 | }` 187 | tradesBody = `{ 188 | "day": "2018-01-03", 189 | "map": { 190 | "c1": "condition1", 191 | "c2": "condition2", 192 | "c3": "condition3", 193 | "c4": "condition4", 194 | "e": "exchange", 195 | "p": "price", 196 | "s": "size", 197 | "t": "timestamp" 198 | }, 199 | "msLatency": 10, 200 | "status": "success", 201 | "symbol": "APCA", 202 | "ticks": [ 203 | { 204 | "c1": 37, 205 | "c2": 12, 206 | "c3": 14, 207 | "c4": 0, 208 | "e": "8", 209 | "p": 98.82, 210 | "s": 61, 211 | "t": 1514938489451 212 | } 213 | ], 214 | "type": "trades" 215 | }` 216 | exchangeBody = `[ 217 | { 218 | "id": 1, 219 | "type": "exchange", 220 | "market": "equities", 221 | "mic": "XASE", 222 | "name": "NYSE American (AMEX)", 223 | "tape": "A" 224 | }, 225 | { 226 | "id": 2, 227 | "type": "exchange", 228 | "market": "equities", 229 | "mic": "XBOS", 230 | "name": "NASDAQ OMX BX", 231 | "tape": "B" 232 | }, 233 | { 234 | "id": 15, 235 | "type": "exchange", 236 | "market": "equities", 237 | "mic": "IEXG", 238 | "name": "IEX", 239 | "tape": "V" 240 | }, 241 | { 242 | "id": 16, 243 | "type": "TRF", 244 | "market": "equities", 245 | "mic": "XCBO", 246 | "name": "Chicago Board Options Exchange", 247 | "tape": "W" 248 | } 249 | ]` 250 | ) 251 | -------------------------------------------------------------------------------- /polygon/stream.go: -------------------------------------------------------------------------------- 1 | package polygon 2 | 3 | import ( 4 | "encoding/json" 5 | "errors" 6 | "fmt" 7 | "log" 8 | "os" 9 | "strings" 10 | "sync" 11 | "sync/atomic" 12 | "time" 13 | 14 | "github.com/alpacahq/alpaca-trade-api-go/common" 15 | "github.com/gorilla/websocket" 16 | ) 17 | 18 | const ( 19 | MinuteAggs = "AM" 20 | SecondAggs = "A" 21 | Trades = "T" 22 | Quotes = "Q" 23 | ) 24 | 25 | const ( 26 | MaxConnectionAttempts = 3 27 | ) 28 | 29 | var ( 30 | once sync.Once 31 | str *Stream 32 | ) 33 | 34 | type Stream struct { 35 | sync.Mutex 36 | sync.Once 37 | conn *websocket.Conn 38 | authenticated, closed atomic.Value 39 | handlers sync.Map 40 | } 41 | 42 | // Subscribe to the specified Polygon stream channel. 43 | func (s *Stream) Subscribe(channel string, handler func(msg interface{})) (err error) { 44 | if s.conn == nil { 45 | s.conn = openSocket() 46 | } 47 | 48 | if err = s.auth(); err != nil { 49 | return 50 | } 51 | 52 | s.Do(func() { 53 | go s.start() 54 | }) 55 | 56 | s.handlers.Store(channel, handler) 57 | 58 | if err = s.sub(channel); err != nil { 59 | s.handlers.Delete(channel) 60 | return 61 | } 62 | 63 | return 64 | } 65 | 66 | // Unsubscribe the specified Polygon stream channel. 67 | func (s *Stream) Unsubscribe(channel string) (err error) { 68 | if s.conn == nil { 69 | err = errors.New("not yet subscribed to any channel") 70 | return 71 | } 72 | 73 | if err = s.auth(); err != nil { 74 | return 75 | } 76 | 77 | s.handlers.Delete(channel) 78 | 79 | err = s.unsub(channel) 80 | 81 | return 82 | } 83 | 84 | // Close gracefully closes the Polygon stream. 85 | func (s *Stream) Close() error { 86 | s.Lock() 87 | defer s.Unlock() 88 | 89 | if s.conn == nil { 90 | return nil 91 | } 92 | 93 | if err := s.conn.WriteMessage( 94 | websocket.CloseMessage, 95 | websocket.FormatCloseMessage(websocket.CloseNormalClosure, ""), 96 | ); err != nil { 97 | return err 98 | } 99 | 100 | // so we know it was gracefully closed 101 | s.closed.Store(true) 102 | 103 | return s.conn.Close() 104 | } 105 | 106 | func (s *Stream) reconnect() { 107 | s.authenticated.Store(false) 108 | s.conn = openSocket() 109 | if err := s.auth(); err != nil { 110 | return 111 | } 112 | s.handlers.Range(func(key, value interface{}) bool { 113 | // there should be no errors if we've previously successfully connected 114 | s.sub(key.(string)) 115 | return true 116 | }) 117 | } 118 | 119 | func (s *Stream) handleError(err error) { 120 | if websocket.IsCloseError(err) { 121 | // if this was a graceful closure, don't reconnect 122 | if s.closed.Load().(bool) { 123 | return 124 | } 125 | } else { 126 | log.Printf("polygon stream read error (%v)", err) 127 | } 128 | 129 | s.reconnect() 130 | } 131 | 132 | func (s *Stream) start() { 133 | for { 134 | if _, arrayBytes, err := s.conn.ReadMessage(); err == nil { 135 | msgArray := []interface{}{} 136 | if err := json.Unmarshal(arrayBytes, &msgArray); err == nil { 137 | for _, msg := range msgArray { 138 | msgMap := msg.(map[string]interface{}) 139 | channel := fmt.Sprintf("%s.%s", msgMap["ev"], msgMap["sym"]) 140 | handler, ok := s.handlers.Load(channel) 141 | if !ok { 142 | // see if an "all symbols" handler was registered 143 | handler, ok = s.handlers.Load(fmt.Sprintf("%s.*", msgMap["ev"])) 144 | } 145 | if ok { 146 | msgBytes, _ := json.Marshal(msg) 147 | switch msgMap["ev"] { 148 | case SecondAggs: 149 | fallthrough 150 | case MinuteAggs: 151 | var minuteAgg StreamAggregate 152 | if err := json.Unmarshal(msgBytes, &minuteAgg); err == nil { 153 | h := handler.(func(msg interface{})) 154 | h(minuteAgg) 155 | } else { 156 | s.handleError(err) 157 | } 158 | case Quotes: 159 | var quoteUpdate StreamQuote 160 | if err := json.Unmarshal(msgBytes, "eUpdate); err == nil { 161 | h := handler.(func(msg interface{})) 162 | h(quoteUpdate) 163 | } else { 164 | s.handleError(err) 165 | } 166 | case Trades: 167 | var tradeUpdate StreamTrade 168 | if err := json.Unmarshal(msgBytes, &tradeUpdate); err == nil { 169 | h := handler.(func(msg interface{})) 170 | h(tradeUpdate) 171 | } else { 172 | s.handleError(err) 173 | } 174 | } 175 | } else { 176 | 177 | } 178 | } 179 | } else { 180 | s.handleError(err) 181 | } 182 | } else { 183 | s.handleError(err) 184 | } 185 | } 186 | } 187 | 188 | func (s *Stream) sub(channel string) (err error) { 189 | s.Lock() 190 | defer s.Unlock() 191 | 192 | subReq := PolygonClientMsg{ 193 | Action: "subscribe", 194 | Params: channel, 195 | } 196 | 197 | if err = s.conn.WriteJSON(subReq); err != nil { 198 | return 199 | } 200 | 201 | return 202 | } 203 | 204 | func (s *Stream) unsub(channel string) (err error) { 205 | s.Lock() 206 | defer s.Unlock() 207 | 208 | subReq := PolygonClientMsg{ 209 | Action: "unsubscribe", 210 | Params: channel, 211 | } 212 | 213 | err = s.conn.WriteJSON(subReq) 214 | 215 | return 216 | } 217 | 218 | func (s *Stream) isAuthenticated() bool { 219 | return s.authenticated.Load().(bool) 220 | } 221 | 222 | func (s *Stream) auth() (err error) { 223 | s.Lock() 224 | defer s.Unlock() 225 | 226 | if s.isAuthenticated() { 227 | return 228 | } 229 | 230 | authRequest := PolygonClientMsg{ 231 | Action: "auth", 232 | Params: common.Credentials().PolygonKeyID, 233 | } 234 | 235 | if err = s.conn.WriteJSON(authRequest); err != nil { 236 | return 237 | } 238 | 239 | msg := []PolygonAuthMsg{} 240 | 241 | // ensure the auth response comes in a timely manner 242 | s.conn.SetReadDeadline(time.Now().Add(5 * time.Second)) 243 | defer s.conn.SetReadDeadline(time.Time{}) 244 | 245 | if err = s.conn.ReadJSON(&msg); err != nil { 246 | return 247 | } 248 | 249 | if !strings.EqualFold(msg[0].Status, "auth_success") { 250 | return fmt.Errorf("failed to authorize Polygon stream") 251 | } 252 | 253 | s.authenticated.Store(true) 254 | 255 | return 256 | } 257 | 258 | // GetStream returns the singleton Polygon stream structure. 259 | func GetStream() *Stream { 260 | once.Do(func() { 261 | str = &Stream{ 262 | authenticated: atomic.Value{}, 263 | handlers: sync.Map{}, 264 | } 265 | 266 | str.authenticated.Store(false) 267 | str.closed.Store(false) 268 | }) 269 | 270 | return str 271 | } 272 | 273 | func openSocket() *websocket.Conn { 274 | /* 275 | For backwards compatibility, POLYGON_WS_URL is kept but the proper way should be to use 276 | DATA_PROXY_WS both for the alpaca ws and the polygon ws. 277 | */ 278 | polygonStreamEndpoint, ok := os.LookupEnv("POLYGON_WS_URL") 279 | if !ok { 280 | if s := os.Getenv("DATA_PROXY_WS"); s != "" { 281 | polygonStreamEndpoint = s 282 | } else { 283 | polygonStreamEndpoint = "wss://socket.polygon.io/stocks" 284 | } 285 | } 286 | connectionAttempts := 0 287 | for connectionAttempts < MaxConnectionAttempts { 288 | connectionAttempts++ 289 | c, _, err := websocket.DefaultDialer.Dial(polygonStreamEndpoint, nil) 290 | if err != nil { 291 | if connectionAttempts == MaxConnectionAttempts { 292 | panic(err) 293 | } 294 | } else { 295 | // consume connection message 296 | msg := []PolgyonServerMsg{} 297 | if err = c.ReadJSON(&msg); err == nil { 298 | return c 299 | } 300 | } 301 | time.Sleep(1 * time.Second) 302 | } 303 | panic(fmt.Errorf("Error: Could not open Polygon stream (max retries exceeded).")) 304 | } 305 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/alpacahq/alpaca-trade-api-go v1.6.2 h1:dsL3Gd4fqHRe4xoe8vAxLjAa/2CgX+wJorfOf+5eIXs= 2 | github.com/alpacahq/alpaca-trade-api-go v1.6.2/go.mod h1:2rhtJj16xMctdr82x8q1JLKIq9Zqxh6cxDjMIDo8JxY= 3 | github.com/cheekybits/is v0.0.0-20150225183255-68e9c0620927 h1:SKI1/fuSdodxmNNyVBR8d7X/HuLnRpvvFO0AgyQk764= 4 | github.com/cheekybits/is v0.0.0-20150225183255-68e9c0620927/go.mod h1:h/aW8ynjgkuj+NQRlZcDbAbM1ORAbXjXX77sX7T289U= 5 | github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8= 6 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 7 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 8 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 9 | github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE= 10 | github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI= 11 | github.com/gin-gonic/gin v1.6.3 h1:ahKqKTFpO5KTPHxWZjEdPScmYaGtLo8Y4DMHoEsnp14= 12 | github.com/gin-gonic/gin v1.6.3/go.mod h1:75u5sXoLsGZoRN5Sgbi1eraJ4GU3++wFwWzhwvtwp4M= 13 | github.com/go-playground/assert/v2 v2.0.1 h1:MsBgLAaY856+nPRTKrp3/OZK38U/wa0CcBYNjji3q3A= 14 | github.com/go-playground/assert/v2 v2.0.1/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4= 15 | github.com/go-playground/locales v0.13.0 h1:HyWk6mgj5qFqCT5fjGBuRArbVDfE4hi8+e8ceBS/t7Q= 16 | github.com/go-playground/locales v0.13.0/go.mod h1:taPMhCMXrRLJO55olJkUXHZBHCxTMfnGwq/HNwmWNS8= 17 | github.com/go-playground/universal-translator v0.17.0 h1:icxd5fm+REJzpZx7ZfpaD876Lmtgy7VtROAbHHXk8no= 18 | github.com/go-playground/universal-translator v0.17.0/go.mod h1:UkSxE5sNxxRwHyU+Scu5vgOQjsIJAF8j9muTVoKLVtA= 19 | github.com/go-playground/validator/v10 v10.2.0 h1:KgJ0snyC2R9VXYN2rneOtQcw5aHQB1Vv0sFl1UcHBOY= 20 | github.com/go-playground/validator/v10 v10.2.0/go.mod h1:uOYAAleCW8F/7oMFd6aG0GOhaH6EGOAJShg8Id5JGkI= 21 | github.com/gobwas/httphead v0.0.0-20180130184737-2c6c146eadee h1:s+21KNqlpePfkah2I+gwHF8xmJWRjooY+5248k6m4A0= 22 | github.com/gobwas/httphead v0.0.0-20180130184737-2c6c146eadee/go.mod h1:L0fX3K22YWvt/FAX9NnzrNzcI4wNYi9Yku4O0LKYflo= 23 | github.com/gobwas/pool v0.2.0 h1:QEmUOlnSjWtnpRGHF3SauEiOsy82Cup83Vf2LcMlnc8= 24 | github.com/gobwas/pool v0.2.0/go.mod h1:q8bcK0KcYlCgd9e7WYLm9LpyS+YeLd8JVDW6WezmKEw= 25 | github.com/gobwas/ws v1.0.2 h1:CoAavW/wd/kulfZmSIBt6p24n4j7tHgNVCjsfHVNUbo= 26 | github.com/gobwas/ws v1.0.2/go.mod h1:szmBTxLgaFppYjEmNtny/v3w89xOydFnnZMcgRRu/EM= 27 | github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= 28 | github.com/golang/protobuf v1.3.5 h1:F768QJ1E9tib+q5Sc8MkdJi1RxLTbRcTf8LJV56aRls= 29 | github.com/golang/protobuf v1.3.5/go.mod h1:6O5/vntMXwX2lRkT1hjjk0nAC1IDOTvTlVgjlRvqsdk= 30 | github.com/google/go-cmp v0.4.0 h1:xsAVV57WRhGj6kEIi8ReJzQlHHqcBYCElAvkovg3B/4= 31 | github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 32 | github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= 33 | github.com/gorilla/websocket v1.4.1 h1:q7AeDBpnBk8AogcD4DSag/Ukw/KV+YhzLj2bP5HvKCM= 34 | github.com/gorilla/websocket v1.4.1/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= 35 | github.com/json-iterator/go v1.1.9 h1:9yzud/Ht36ygwatGx56VwCZtlI/2AD15T1X2sjSuGns= 36 | github.com/json-iterator/go v1.1.9/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= 37 | github.com/klauspost/compress v1.10.3 h1:OP96hzwJVBIHYU52pVTI6CczrxPvrGfgqF9N5eTO0Q8= 38 | github.com/klauspost/compress v1.10.3/go.mod h1:aoV0uJVorq1K+umq18yTdKaF57EivdYsUV+/s2qKfXs= 39 | github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI= 40 | github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= 41 | github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= 42 | github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE= 43 | github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= 44 | github.com/leodido/go-urn v1.2.0 h1:hpXL4XnriNwQ/ABnpepYM/1vCLWNDfUNts8dX3xTG6Y= 45 | github.com/leodido/go-urn v1.2.0/go.mod h1:+8+nEpDfqqsY+g338gtMEUOtuK+4dEMhiQEgxpxOKII= 46 | github.com/matryer/try v0.0.0-20161228173917-9ac251b645a2 h1:JAEbJn3j/FrhdWA9jW8B5ajsLIjeuEHLi8xE4fk997o= 47 | github.com/matryer/try v0.0.0-20161228173917-9ac251b645a2/go.mod h1:0KeJpeMD6o+O4hW7qJOT7vyQPKrWmj26uf5wMc/IiIs= 48 | github.com/mattn/go-isatty v0.0.12 h1:wuysRhFDzyxgEmMf5xjvJ2M9dZoWAXNNr5LSBS7uHXY= 49 | github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= 50 | github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421 h1:ZqeYNhU3OHLH3mGKHDcjJRFFRrJa6eAM5H+CtDdOsPc= 51 | github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= 52 | github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742 h1:Esafd1046DLDQ0W1YjYsBW+p8U2u7vzgW2SQVmlNazg= 53 | github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= 54 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 55 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 56 | github.com/shopspring/decimal v1.2.0 h1:abSATXmQEYyShuxI4/vyW3tV1MrKAJzCZ/0zLUXYbsQ= 57 | github.com/shopspring/decimal v1.2.0/go.mod h1:DKyhrW/HYNuLGql+MJL6WCR6knT2jwCFRcu2hWCYk4o= 58 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 59 | github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= 60 | github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= 61 | github.com/stretchr/testify v1.6.1 h1:hDPOHmpOpP40lSULcqw7IrRb/u7w6RpDC9399XyoNd0= 62 | github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 63 | github.com/ugorji/go v1.1.7 h1:/68gy2h+1mWMrwZFeD1kQialdSzAb432dtpeJ42ovdo= 64 | github.com/ugorji/go v1.1.7/go.mod h1:kZn38zHttfInRq0xu/PH0az30d+z6vm202qpg1oXVMw= 65 | github.com/ugorji/go/codec v1.1.7 h1:2SvQaVZ1ouYrrKKwoSk2pzd4A9evlKJb9oTL+OaLUSs= 66 | github.com/ugorji/go/codec v1.1.7/go.mod h1:Ax+UKWsSmolVDwsd+7N3ZtXu+yMGCf907BLYF3GoBXY= 67 | github.com/vmihailenco/msgpack/v5 v5.1.4 h1:6K44/cU6dMNGkVTGGuu7ef2NdSRFMhAFGGLfE3cqtHM= 68 | github.com/vmihailenco/msgpack/v5 v5.1.4/go.mod h1:C5gboKD0TJPqWDTVTtrQNfRbiBwHZGo8UTqP/9/XvLI= 69 | github.com/vmihailenco/tagparser v0.1.2 h1:gnjoVuB/kljJ5wICEEOpx98oXMWPLj22G67Vbd1qPqc= 70 | github.com/vmihailenco/tagparser v0.1.2/go.mod h1:OeAg3pn3UbLjkWt+rN9oFYB6u/cQgqMEUPoW2WPyhdI= 71 | golang.org/x/sys v0.0.0-20200116001909-b77594299b42 h1:vEOn+mP2zCOVzKckCZy6YsCtDblrpj/w7B9nxGNELpg= 72 | golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 73 | golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= 74 | golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= 75 | golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 76 | golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4= 77 | golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 78 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 79 | gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY= 80 | gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 81 | gopkg.in/matryer/try.v1 v1.0.0-20150601225556-312d2599e12e h1:bJHzu9Qwc9wQRWJ/WVkJGAfs+riucl/tKAFNxf9pzqk= 82 | gopkg.in/matryer/try.v1 v1.0.0-20150601225556-312d2599e12e/go.mod h1:tve0rTLdGlwnXF7iBO9rbAEyeXvuuPx0n4DvXS/Nw7o= 83 | gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 84 | gopkg.in/yaml.v2 v2.2.8 h1:obN1ZagJSUGI0Ek/LBmuj4SNLPfIny3KsKFopxRdj10= 85 | gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 86 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo= 87 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 88 | nhooyr.io/websocket v1.8.6 h1:s+C3xAMLwGmlI31Nyn/eAehUlZPwfYZu2JXM621Q5/k= 89 | nhooyr.io/websocket v1.8.6/go.mod h1:B70DZP8IakI65RVQ51MsWP/8jndNma26DVA/nFSCgW0= 90 | -------------------------------------------------------------------------------- /alpaca/stream.go: -------------------------------------------------------------------------------- 1 | package alpaca 2 | 3 | import ( 4 | "encoding/json" 5 | "errors" 6 | "fmt" 7 | "log" 8 | "net/url" 9 | "os" 10 | "strings" 11 | "sync" 12 | "sync/atomic" 13 | "time" 14 | 15 | "github.com/alpacahq/alpaca-trade-api-go/common" 16 | "github.com/gorilla/websocket" 17 | ) 18 | 19 | const ( 20 | TradeUpdates = "trade_updates" 21 | AccountUpdates = "account_updates" 22 | ) 23 | 24 | const ( 25 | MaxConnectionAttempts = 3 26 | ) 27 | 28 | var ( 29 | once sync.Once 30 | str *Stream 31 | streamUrl = "" 32 | 33 | dataOnce sync.Once 34 | dataStr *Stream 35 | ) 36 | 37 | type Stream struct { 38 | sync.Mutex 39 | sync.Once 40 | conn *websocket.Conn 41 | authenticated, closed atomic.Value 42 | handlers sync.Map 43 | base string 44 | } 45 | 46 | // Subscribe to the specified Alpaca stream channel. 47 | func (s *Stream) Subscribe(channel string, handler func(msg interface{})) (err error) { 48 | switch { 49 | case channel == TradeUpdates: 50 | fallthrough 51 | case channel == AccountUpdates: 52 | fallthrough 53 | case strings.HasPrefix(channel, "Q."): 54 | fallthrough 55 | case strings.HasPrefix(channel, "T."): 56 | fallthrough 57 | case strings.HasPrefix(channel, "AM."): 58 | default: 59 | err = fmt.Errorf("invalid stream (%s)", channel) 60 | return 61 | } 62 | if s.conn == nil { 63 | s.conn, err = s.openSocket() 64 | if err != nil { 65 | return 66 | } 67 | } 68 | 69 | if err = s.auth(); err != nil { 70 | return 71 | } 72 | s.Do(func() { 73 | go s.start() 74 | }) 75 | 76 | s.handlers.Store(channel, handler) 77 | 78 | if err = s.sub(channel); err != nil { 79 | s.handlers.Delete(channel) 80 | return 81 | } 82 | return 83 | } 84 | 85 | // Unsubscribe the specified Polygon stream channel. 86 | func (s *Stream) Unsubscribe(channel string) (err error) { 87 | if s.conn == nil { 88 | err = errors.New("not yet subscribed to any channel") 89 | return 90 | } 91 | 92 | if err = s.auth(); err != nil { 93 | return 94 | } 95 | 96 | s.handlers.Delete(channel) 97 | 98 | err = s.unsub(channel) 99 | 100 | return 101 | } 102 | 103 | // Close gracefully closes the Alpaca stream. 104 | func (s *Stream) Close() error { 105 | s.Lock() 106 | defer s.Unlock() 107 | 108 | if s.conn == nil { 109 | return nil 110 | } 111 | 112 | if err := s.conn.WriteMessage( 113 | websocket.CloseMessage, 114 | websocket.FormatCloseMessage(websocket.CloseNormalClosure, ""), 115 | ); err != nil { 116 | return err 117 | } 118 | 119 | // so we know it was gracefully closed 120 | s.closed.Store(true) 121 | 122 | return s.conn.Close() 123 | } 124 | 125 | func (s *Stream) reconnect() error { 126 | s.authenticated.Store(false) 127 | conn, err := s.openSocket() 128 | if err != nil { 129 | return err 130 | } 131 | s.conn = conn 132 | if err := s.auth(); err != nil { 133 | return err 134 | } 135 | s.handlers.Range(func(key, value interface{}) bool { 136 | // there should be no errors if we've previously successfully connected 137 | s.sub(key.(string)) 138 | return true 139 | }) 140 | return nil 141 | } 142 | 143 | func (s *Stream) findHandler(stream string) func(interface{}) { 144 | if v, ok := s.handlers.Load(stream); ok { 145 | return v.(func(interface{})) 146 | } 147 | if strings.HasPrefix(stream, "Q.") || 148 | strings.HasPrefix(stream, "T.") || 149 | strings.HasPrefix(stream, "AM.") { 150 | c := stream[:strings.Index(stream, ".")] 151 | if v, ok := s.handlers.Load(c + ".*"); ok { 152 | return v.(func(interface{})) 153 | } 154 | } 155 | return nil 156 | } 157 | 158 | func (s *Stream) start() { 159 | for { 160 | msg := ServerMsg{} 161 | 162 | if err := s.conn.ReadJSON(&msg); err == nil { 163 | handler := s.findHandler(msg.Stream) 164 | if handler != nil { 165 | msgBytes, _ := json.Marshal(msg.Data) 166 | switch { 167 | case msg.Stream == TradeUpdates: 168 | var tradeupdate TradeUpdate 169 | json.Unmarshal(msgBytes, &tradeupdate) 170 | handler(tradeupdate) 171 | case strings.HasPrefix(msg.Stream, "Q."): 172 | var quote StreamQuote 173 | json.Unmarshal(msgBytes, "e) 174 | handler(quote) 175 | case strings.HasPrefix(msg.Stream, "T."): 176 | var trade StreamTrade 177 | json.Unmarshal(msgBytes, &trade) 178 | handler(trade) 179 | case strings.HasPrefix(msg.Stream, "AM."): 180 | var agg StreamAgg 181 | json.Unmarshal(msgBytes, &agg) 182 | handler(agg) 183 | 184 | default: 185 | handler(msg.Data) 186 | } 187 | } 188 | } else { 189 | if websocket.IsCloseError(err) { 190 | // if this was a graceful closure, don't reconnect 191 | if s.closed.Load().(bool) { 192 | return 193 | } 194 | } else { 195 | log.Printf("alpaca stream read error (%v)", err) 196 | } 197 | 198 | for { 199 | if err := s.reconnect(); err != nil { 200 | log.Printf("alpaca stream reconnect error (%v)", err) 201 | time.Sleep(30 * time.Second) 202 | } else { 203 | break 204 | } 205 | } 206 | } 207 | } 208 | } 209 | 210 | func (s *Stream) sub(channel string) (err error) { 211 | s.Lock() 212 | defer s.Unlock() 213 | 214 | subReq := ClientMsg{ 215 | Action: "listen", 216 | Data: map[string]interface{}{ 217 | "streams": []interface{}{ 218 | channel, 219 | }, 220 | }, 221 | } 222 | 223 | if err = s.conn.WriteJSON(subReq); err != nil { 224 | return 225 | } 226 | 227 | return 228 | } 229 | 230 | func (s *Stream) unsub(channel string) (err error) { 231 | s.Lock() 232 | defer s.Unlock() 233 | 234 | unsubReq := ClientMsg{ 235 | Action: "unlisten", 236 | Data: map[string]interface{}{ 237 | "streams": []interface{}{ 238 | channel, 239 | }, 240 | }, 241 | } 242 | 243 | if err = s.conn.WriteJSON(unsubReq); err != nil { 244 | return 245 | } 246 | 247 | return 248 | } 249 | 250 | func (s *Stream) isAuthenticated() bool { 251 | return s.authenticated.Load().(bool) 252 | } 253 | 254 | func (s *Stream) auth() (err error) { 255 | s.Lock() 256 | defer s.Unlock() 257 | 258 | if s.isAuthenticated() { 259 | return 260 | } 261 | 262 | authRequest := ClientMsg{ 263 | Action: "authenticate", 264 | Data: map[string]interface{}{ 265 | "key_id": common.Credentials().ID, 266 | "secret_key": common.Credentials().Secret, 267 | }, 268 | } 269 | 270 | if err = s.conn.WriteJSON(authRequest); err != nil { 271 | return 272 | } 273 | 274 | msg := ServerMsg{} 275 | 276 | // ensure the auth response comes in a timely manner 277 | s.conn.SetReadDeadline(time.Now().Add(5 * time.Second)) 278 | defer s.conn.SetReadDeadline(time.Time{}) 279 | 280 | if err = s.conn.ReadJSON(&msg); err != nil { 281 | return 282 | } 283 | 284 | m := msg.Data.(map[string]interface{}) 285 | 286 | if !strings.EqualFold(m["status"].(string), "authorized") { 287 | return fmt.Errorf("failed to authorize alpaca stream") 288 | } 289 | 290 | s.authenticated.Store(true) 291 | 292 | return 293 | } 294 | 295 | // GetStream returns the singleton Alpaca stream structure. 296 | func GetStream() *Stream { 297 | once.Do(func() { 298 | str = &Stream{ 299 | authenticated: atomic.Value{}, 300 | handlers: sync.Map{}, 301 | base: base, 302 | } 303 | 304 | str.authenticated.Store(false) 305 | str.closed.Store(false) 306 | }) 307 | 308 | return str 309 | } 310 | 311 | func GetDataStream() *Stream { 312 | dataOnce.Do(func() { 313 | if s := os.Getenv("DATA_PROXY_WS"); s != "" { 314 | streamUrl = s 315 | } else { 316 | streamUrl = dataURL 317 | } 318 | dataStr = &Stream{ 319 | authenticated: atomic.Value{}, 320 | handlers: sync.Map{}, 321 | base: streamUrl, 322 | } 323 | 324 | dataStr.authenticated.Store(false) 325 | dataStr.closed.Store(false) 326 | }) 327 | 328 | return dataStr 329 | } 330 | 331 | func (s *Stream) openSocket() (*websocket.Conn, error) { 332 | scheme := "wss" 333 | ub, _ := url.Parse(s.base) 334 | if ub.Scheme == "http" { 335 | scheme = "ws" 336 | } 337 | u := url.URL{Scheme: scheme, Host: ub.Host, Path: "/stream"} 338 | connectionAttempts := 0 339 | for connectionAttempts < MaxConnectionAttempts { 340 | connectionAttempts++ 341 | c, _, err := websocket.DefaultDialer.Dial(u.String(), nil) 342 | if err == nil { 343 | return c, nil 344 | } 345 | if connectionAttempts == MaxConnectionAttempts { 346 | return nil, err 347 | } 348 | time.Sleep(1 * time.Second) 349 | } 350 | return nil, fmt.Errorf("Error: Could not open Alpaca stream (max retries exceeded).") 351 | } 352 | -------------------------------------------------------------------------------- /examples/mean-reversion/mean-reversion.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "math" 6 | "os" 7 | "time" 8 | 9 | "github.com/alpacahq/alpaca-trade-api-go/alpaca" 10 | "github.com/alpacahq/alpaca-trade-api-go/common" 11 | "github.com/shopspring/decimal" 12 | ) 13 | 14 | type alpacaClientContainer struct { 15 | client *alpaca.Client 16 | runningAverage float64 17 | lastOrder string 18 | amtBars int 19 | stock string 20 | } 21 | 22 | var alpacaClient alpacaClientContainer 23 | 24 | func init() { 25 | API_KEY := "YOUR_API_KEY_HERE" 26 | API_SECRET := "YOUR_API_SECRET_HERE" 27 | BASE_URL := "https://paper-api.alpaca.markets" 28 | 29 | // Check for environment variables 30 | if common.Credentials().ID == "" { 31 | os.Setenv(common.EnvApiKeyID, API_KEY) 32 | } 33 | if common.Credentials().Secret == "" { 34 | os.Setenv(common.EnvApiSecretKey, API_SECRET) 35 | } 36 | alpaca.SetBaseUrl(BASE_URL) 37 | 38 | // Check if user input a stock, default is AAPL 39 | stock := "AAPL" 40 | if len(os.Args[1:]) == 1 { 41 | stock = os.Args[1] 42 | } 43 | alpacaClient = alpacaClientContainer{ 44 | alpaca.NewClient(common.Credentials()), 45 | 0.0, 46 | "", 47 | 20, 48 | stock, 49 | } 50 | } 51 | 52 | func main() { 53 | // First, cancel any existing orders so they don't impact our buying power. 54 | status, until, limit := "open", time.Now(), 100 55 | orders, _ := alpacaClient.client.ListOrders(&status, nil, &until, &limit, nil, nil) 56 | for _, order := range orders { 57 | _ = alpacaClient.client.CancelOrder(order.ID) 58 | } 59 | 60 | // Wait for market to open 61 | fmt.Println("Waiting for market to open...") 62 | for { 63 | isOpen := alpacaClient.awaitMarketOpen() 64 | if isOpen { 65 | break 66 | } 67 | time.Sleep(1 * time.Minute) 68 | } 69 | fmt.Println("Market Opened.") 70 | 71 | // Wait until 20 bars of data since market open have been collected. 72 | fmt.Printf("Waiting for %d bars...\n", alpacaClient.amtBars) 73 | for { 74 | rawTime, _ := time.Parse(time.RFC3339, time.Now().String()) 75 | currTime := rawTime.String() 76 | cal, _ := alpacaClient.client.GetCalendar(&currTime, &currTime) 77 | marketOpen, _ := time.Parse(time.RFC3339, cal[0].Open) 78 | bars, _ := alpacaClient.client.GetSymbolBars(alpacaClient.stock, alpaca.ListBarParams{Timeframe: "minute", StartDt: &marketOpen}) 79 | if len(bars) >= alpacaClient.amtBars { 80 | break 81 | } else { 82 | time.Sleep(1 * time.Minute) 83 | } 84 | } 85 | fmt.Printf("We have %d bars.\n", alpacaClient.amtBars) 86 | 87 | for { 88 | alpacaClient.run() 89 | } 90 | } 91 | 92 | // Rebalance our portfolio every minute based off running average data. 93 | func (alp alpacaClientContainer) run() { 94 | if alpacaClient.lastOrder != "" { 95 | _ = alp.client.CancelOrder(alpacaClient.lastOrder) 96 | } 97 | 98 | // Figure out when the market will close so we can prepare to sell beforehand. 99 | clock, _ := alp.client.GetClock() 100 | if clock.NextClose.Sub(clock.Timestamp) < 15*time.Minute { 101 | // Close all positions when 15 minutes til market close. 102 | fmt.Println("Market closing soon. Closing positions.") 103 | 104 | positions, _ := alp.client.ListPositions() 105 | for _, position := range positions { 106 | var orderSide string 107 | if position.Side == "long" { 108 | orderSide = "sell" 109 | } else { 110 | orderSide = "buy" 111 | } 112 | qty, _ := position.Qty.Float64() 113 | qty = math.Abs(qty) 114 | alp.submitMarketOrder(int(qty), position.Symbol, orderSide) 115 | } 116 | // Run script again after market close for next trading day. 117 | fmt.Println("Sleeping until market close (15 minutes).") 118 | time.Sleep(15 * time.Minute) 119 | } else { 120 | // Rebalance the portfolio. 121 | alp.rebalance() 122 | time.Sleep(1 * time.Minute) 123 | } 124 | } 125 | 126 | // Spin until the market is open. 127 | func (alp alpacaClientContainer) awaitMarketOpen() bool { 128 | clock, _ := alp.client.GetClock() 129 | if clock.IsOpen { 130 | return true 131 | } 132 | timeToOpen := int(clock.NextOpen.Sub(clock.Timestamp).Minutes()) 133 | fmt.Printf("%d minutes until next market open.\n", timeToOpen) 134 | return false 135 | } 136 | 137 | // Rebalance our position after an update. 138 | func (alp alpacaClientContainer) rebalance() { 139 | // Get our position, if any. 140 | positionQty := 0 141 | positionVal := 0.0 142 | position, err := alp.client.GetPosition(alpacaClient.stock) 143 | if err != nil { 144 | } else { 145 | positionQty = int(position.Qty.IntPart()) 146 | positionVal, _ = position.MarketValue.Float64() 147 | } 148 | 149 | // Get the new updated price and running average. 150 | bars, _ := alp.client.GetSymbolBars(alpacaClient.stock, alpaca.ListBarParams{Timeframe: "minute", Limit: &alpacaClient.amtBars}) 151 | currPrice := float64(bars[len(bars)-1].Close) 152 | alpacaClient.runningAverage = 0.0 153 | for _, bar := range bars { 154 | alpacaClient.runningAverage += float64(bar.Close) 155 | } 156 | alpacaClient.runningAverage /= float64(alpacaClient.amtBars) 157 | 158 | if currPrice > alpacaClient.runningAverage { 159 | // Sell our position if the price is above the running average, if any. 160 | if positionQty > 0 { 161 | fmt.Println("Setting long position to zero") 162 | alp.submitLimitOrder(positionQty, alpacaClient.stock, currPrice, "sell") 163 | } else { 164 | fmt.Println("No action required.") 165 | } 166 | } else if currPrice < alpacaClient.runningAverage { 167 | // Determine optimal amount of shares based on portfolio and market data. 168 | account, _ := alp.client.GetAccount() 169 | buyingPower, _ := account.BuyingPower.Float64() 170 | positions, _ := alp.client.ListPositions() 171 | portfolioVal, _ := account.Cash.Float64() 172 | for _, position := range positions { 173 | rawVal, _ := position.MarketValue.Float64() 174 | portfolioVal += rawVal 175 | } 176 | portfolioShare := (alpacaClient.runningAverage - currPrice) / currPrice * 200 177 | targetPositionValue := portfolioVal * portfolioShare 178 | amountToAdd := targetPositionValue - positionVal 179 | 180 | // Add to our position, constrained by our buying power; or, sell down to optimal amount of shares. 181 | if amountToAdd > 0 { 182 | if amountToAdd > buyingPower { 183 | amountToAdd = buyingPower 184 | } 185 | var qtyToBuy = int(amountToAdd / currPrice) 186 | alp.submitLimitOrder(qtyToBuy, alpacaClient.stock, currPrice, "buy") 187 | } else { 188 | amountToAdd *= -1 189 | var qtyToSell = int(amountToAdd / currPrice) 190 | if qtyToSell > positionQty { 191 | qtyToSell = positionQty 192 | } 193 | alp.submitLimitOrder(qtyToSell, alpacaClient.stock, currPrice, "sell") 194 | } 195 | } 196 | } 197 | 198 | // Submit a limit order if quantity is above 0. 199 | func (alp alpacaClientContainer) submitLimitOrder(qty int, symbol string, price float64, side string) error { 200 | account, _ := alp.client.GetAccount() 201 | if qty > 0 { 202 | adjSide := alpaca.Side(side) 203 | limPrice := decimal.NewFromFloat(price) 204 | order, err := alp.client.PlaceOrder(alpaca.PlaceOrderRequest{ 205 | AccountID: account.ID, 206 | AssetKey: &symbol, 207 | Qty: decimal.NewFromInt(int64(qty)), 208 | Side: adjSide, 209 | Type: "limit", 210 | LimitPrice: &limPrice, 211 | TimeInForce: "day", 212 | }) 213 | if err == nil { 214 | fmt.Printf("Limit order of | %d %s %s | sent.\n", qty, symbol, side) 215 | } else { 216 | fmt.Printf("Order of | %d %s %s | did not go through.\n", qty, symbol, side) 217 | } 218 | alpacaClient.lastOrder = order.ID 219 | return err 220 | } 221 | fmt.Printf("Quantity is <= 0, order of | %d %s %s | not sent.\n", qty, symbol, side) 222 | return nil 223 | } 224 | 225 | // Submit a market order if quantity is above 0. 226 | func (alp alpacaClientContainer) submitMarketOrder(qty int, symbol string, side string) error { 227 | account, _ := alp.client.GetAccount() 228 | if qty > 0 { 229 | adjSide := alpaca.Side(side) 230 | lastOrder, err := alp.client.PlaceOrder(alpaca.PlaceOrderRequest{ 231 | AccountID: account.ID, 232 | AssetKey: &symbol, 233 | Qty: decimal.NewFromInt(int64(qty)), 234 | Side: adjSide, 235 | Type: "market", 236 | TimeInForce: "day", 237 | }) 238 | if err == nil { 239 | fmt.Printf("Market order of | %d %s %s | completed.\n", qty, symbol, side) 240 | alpacaClient.lastOrder = lastOrder.ID 241 | } else { 242 | fmt.Printf("Order of | %d %s %s | did not go through.\n", qty, symbol, side) 243 | } 244 | return err 245 | } 246 | fmt.Printf("Quantity is <= 0, order of | %d %s %s | not sent.\n", qty, symbol, side) 247 | return nil 248 | } 249 | -------------------------------------------------------------------------------- /polygon/entities.go: -------------------------------------------------------------------------------- 1 | package polygon 2 | 3 | import "time" 4 | 5 | // SymbolsMetadata is the structure that defines symbol 6 | // metadata served through polygon's REST API. 7 | type SymbolsMetadata struct { 8 | Symbols []struct { 9 | Symbol string `json:"symbol"` 10 | Name string `json:"name"` 11 | Type string `json:"type"` 12 | Updated time.Time `json:"updated"` 13 | IsOTC bool `json:"isOTC"` 14 | URL string `json:"url"` 15 | } `json:"symbols"` 16 | } 17 | 18 | // HistoricTrades is the structure that defines trade 19 | // data served through polygon's REST API. 20 | type HistoricTrades struct { 21 | Day string `json:"day"` 22 | Map struct { 23 | C1 string `json:"c1"` 24 | C2 string `json:"c2"` 25 | C3 string `json:"c3"` 26 | C4 string `json:"c4"` 27 | E string `json:"e"` 28 | P string `json:"p"` 29 | S string `json:"s"` 30 | T string `json:"t"` 31 | } `json:"map"` 32 | MsLatency int `json:"msLatency"` 33 | Status string `json:"status"` 34 | Symbol string `json:"symbol"` 35 | Ticks []TradeTick `json:"ticks"` 36 | Type string `json:"type"` 37 | } 38 | 39 | // TradeTick is the structure that contains the actual 40 | // tick data included in a HistoricTrades response 41 | type TradeTick struct { 42 | Timestamp int64 `json:"t"` 43 | Price float64 `json:"p"` 44 | Size int `json:"s"` 45 | Exchange string `json:"e"` 46 | Condition1 int `json:"c1"` 47 | Condition2 int `json:"c2"` 48 | Condition3 int `json:"c3"` 49 | Condition4 int `json:"c4"` 50 | } 51 | 52 | type MapItem struct { 53 | Name string `json:"name"` 54 | Type string `json:"type"` 55 | } 56 | 57 | // HistoricTradesV2 is the structure that defines trade 58 | // data served through polygon's REST API. 59 | type HistoricTradesV2 struct { 60 | ResultsCount int64 `json:"results_count"` 61 | Ticker string `json:"ticker"` 62 | Results []TradeTickV2 `json:"results"` 63 | Map map[string]MapItem `json:"map"` 64 | } 65 | 66 | // TradeTickV2 is the structure that contains the actual 67 | // tick data included in a HistoricTradesV2 response 68 | type TradeTickV2 struct { 69 | SIPTimestamp *int64 `json:"t"` 70 | ParticipantTimestamp *int64 `json:"y"` 71 | TRFTimestamp *int64 `json:"f"` 72 | SequenceNumber *int `json:"q"` 73 | ID *string `json:"i"` 74 | OrigID *string `json:"I"` 75 | Exchange *int `json:"x"` 76 | TRFID *int `json:"r"` 77 | Size *int `json:"s"` 78 | Conditions *[]int `json:"c"` 79 | Price *float64 `json:"p"` 80 | Tape *int `json:"z"` 81 | Correction *int `json:"e"` 82 | } 83 | 84 | // HistoricQuotes is the structure that defines quote 85 | // data served through polygon's REST API. 86 | type HistoricQuotes struct { 87 | Day string `json:"day"` 88 | Map struct { 89 | AE string `json:"aE"` 90 | AP string `json:"aP"` 91 | AS string `json:"aS"` 92 | BE string `json:"bE"` 93 | BP string `json:"bP"` 94 | BS string `json:"bS"` 95 | C string `json:"c"` 96 | T string `json:"t"` 97 | } `json:"map"` 98 | MsLatency int `json:"msLatency"` 99 | Status string `json:"status"` 100 | Symbol string `json:"symbol"` 101 | Ticks []QuoteTick `json:"ticks"` 102 | Type string `json:"type"` 103 | } 104 | 105 | // QuoteTick is the structure that contains the actual 106 | // tick data included in a HistoricQuotes response 107 | type QuoteTick struct { 108 | Timestamp int64 `json:"t"` 109 | BidExchange string `json:"bE"` 110 | AskExchange string `json:"aE"` 111 | BidPrice float64 `json:"bP"` 112 | AskPrice float64 `json:"aP"` 113 | BidSize int `json:"bS"` 114 | AskSize int `json:"aS"` 115 | Condition int `json:"c"` 116 | } 117 | 118 | // HistoricQuotesV2 is the structure that defines trade 119 | // data served through polygon's REST API. 120 | type HistoricQuotesV2 struct { 121 | ResultsCount int64 `json:"results_count"` 122 | Ticker string `json:"ticker"` 123 | Results []QuoteTickV2 `json:"results"` 124 | Map map[string]MapItem `json:"map"` 125 | } 126 | 127 | // QuoteTickV2 is the structure that contains the actual 128 | // tick data included in a HistoricQuotesV2 response 129 | type QuoteTickV2 struct { 130 | SIPTimestamp *int64 `json:"t"` 131 | ParticipantTimestamp *int64 `json:"y"` 132 | TRFTimestamp *int64 `json:"f"` 133 | SequenceNumber *int `json:"q"` 134 | Indicators *[]int `json:"i"` 135 | BidExchange *int `json:"x"` 136 | AskExchange *int `json:"X"` 137 | TRFID *int `json:"r"` 138 | Size *int `json:"s"` 139 | Conditions *[]int `json:"c"` 140 | BidPrice *float64 `json:"p"` 141 | AskPrice *float64 `json:"P"` 142 | BidSize *int `json:"s"` 143 | AskSize *int `json:"S"` 144 | Tape *int `json:"z"` 145 | } 146 | 147 | // HistoricAggregates is the structure that defines 148 | // aggregate data served through Polygon's v1 REST API. 149 | type HistoricAggregates struct { 150 | Symbol string `json:"symbol"` 151 | AggregateType AggType `json:"aggType"` 152 | Map struct { 153 | O string `json:"o"` 154 | C string `json:"c"` 155 | H string `json:"h"` 156 | L string `json:"l"` 157 | V string `json:"v"` 158 | D string `json:"d"` 159 | } `json:"map"` 160 | Ticks []AggTick `json:"ticks"` 161 | } 162 | 163 | // HistoricAggregatesV2 is the structure that defines 164 | // aggregate data served through Polygon's v2 REST API. 165 | type HistoricAggregatesV2 struct { 166 | Symbol string `json:"ticker"` 167 | Adjusted bool `json:"adjusted"` 168 | QueryCount int `json:"queryCount"` 169 | ResultsCount int `json:"resultsCount"` 170 | Ticks []AggTick `json:"results"` 171 | } 172 | 173 | type GetHistoricTradesParams struct { 174 | Offset int64 `json:"offset"` 175 | Limit int64 `json:"limit"` 176 | } 177 | 178 | type HistoricTicksV2Params struct { 179 | Timestamp int64 `json:"timestamp"` 180 | TimestampLimit int64 `json:"timestamp_limit"` 181 | Reverse bool `json:"reverse"` 182 | Limit int64 `json:"limit"` 183 | } 184 | 185 | // AggTick is the structure that contains the actual 186 | // tick data included in a HistoricAggregates response 187 | type AggTick struct { 188 | Open float64 `json:"o"` 189 | High float64 `json:"h"` 190 | Low float64 `json:"l"` 191 | Close float64 `json:"c"` 192 | Volume float64 `json:"v"` 193 | EpochMilliseconds int64 `json:"t"` 194 | Items int64 `json:"n"` // v2 response only 195 | } 196 | 197 | // AggType used in the HistoricAggregates response 198 | type AggType string 199 | 200 | const ( 201 | // Minute timeframe aggregates 202 | Minute AggType = "minute" 203 | // Day timeframe aggregates 204 | Day AggType = "day" 205 | ) 206 | 207 | // polygon stream 208 | 209 | // PolygonClientMsg is the standard message sent by clients of the stream interface 210 | type PolygonClientMsg struct { 211 | Action string `json:"action"` 212 | Params string `json:"params"` 213 | } 214 | 215 | type PolygonAuthMsg struct { 216 | Event string `json:"ev"` 217 | Status string `json:"status"` 218 | Message string `json:"message"` 219 | } 220 | 221 | // PolygonServerMsg contains the field that is present in all responses to identify their type 222 | type PolgyonServerMsg struct { 223 | Event string `json:"ev"` 224 | } 225 | 226 | // StreamTrade is the structure that defines a trade that 227 | // polygon transmits via websocket protocol. 228 | type StreamTrade struct { 229 | Symbol string `json:"sym"` 230 | Exchange int `json:"x"` 231 | TradeID string `json:"i"` 232 | Price float64 `json:"p"` 233 | Size int64 `json:"s"` 234 | Timestamp int64 `json:"t"` 235 | Conditions []int `json:"c"` 236 | } 237 | 238 | // StreamQuote is the structure that defines a quote that 239 | // polygon transmits via websocket protocol. 240 | type StreamQuote struct { 241 | Symbol string `json:"sym"` 242 | Condition int `json:"c"` 243 | BidExchange int `json:"bx"` 244 | AskExchange int `json:"ax"` 245 | BidPrice float64 `json:"bp"` 246 | AskPrice float64 `json:"ap"` 247 | BidSize int64 `json:"bs"` 248 | AskSize int64 `json:"as"` 249 | Timestamp int64 `json:"t"` 250 | } 251 | 252 | // StreamAggregate is the structure that defines an aggregate that 253 | // polygon transmits via websocket protocol. 254 | type StreamAggregate struct { 255 | Event string `json:"ev"` 256 | Symbol string `json:"sym"` 257 | Volume int `json:"v"` 258 | AccumulatedVolume int `json:"av"` 259 | OpeningPrice float64 `json:"op"` 260 | VWAP float64 `json:"vw"` 261 | OpenPrice float64 `json:"o"` 262 | ClosePrice float64 `json:"c"` 263 | HighPrice float64 `json:"h"` 264 | LowPrice float64 `json:"l"` 265 | Average float64 `json:"a"` 266 | StartTimestamp int64 `json:"s"` 267 | EndTimestamp int64 `json:"e"` 268 | } 269 | 270 | // Exchange defines the Stocks / Equities "Exchange" endpoint response 271 | type StockExchange struct { 272 | Id int64 `json:"id"` 273 | Type string `json:"type"` 274 | Market string `json:"market"` 275 | Mic string `json:"mic"` 276 | Name string `json:"name"` 277 | Tape string `json:"tape"` 278 | } 279 | -------------------------------------------------------------------------------- /examples/martingale/martingale.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | "math" 7 | "math/rand" 8 | "os" 9 | "strconv" 10 | "strings" 11 | "time" 12 | 13 | "github.com/alpacahq/alpaca-trade-api-go/alpaca" 14 | "github.com/alpacahq/alpaca-trade-api-go/common" 15 | "github.com/alpacahq/alpaca-trade-api-go/polygon" 16 | "github.com/alpacahq/alpaca-trade-api-go/stream" 17 | "github.com/shopspring/decimal" 18 | ) 19 | 20 | type alpacaClientContainer struct { 21 | client *alpaca.Client 22 | tickSize int 23 | tickIndex int 24 | baseBet float64 25 | currStreak streak 26 | currOrder string 27 | lastPrice float64 28 | lastTradeTime time.Time 29 | stock string 30 | position int64 31 | equity float64 32 | marginMult float64 33 | seconds int 34 | } 35 | 36 | type streak struct { 37 | start float64 38 | count int 39 | increasing bool 40 | } 41 | 42 | var alpacaClient alpacaClientContainer 43 | 44 | // The MartingaleTrader bets that streaks of increases or decreases in a stock's 45 | // price are likely to break, and increases its bet each time it is wrong. 46 | func init() { 47 | API_KEY := "YOUR_API_KEY_HERE" 48 | API_SECRET := "YOUR_API_SECRET_HERE" 49 | BASE_URL := "https://paper-api.alpaca.markets" 50 | 51 | // Check for environment variables 52 | if common.Credentials().ID == "" { 53 | os.Setenv(common.EnvApiKeyID, API_KEY) 54 | } 55 | if common.Credentials().Secret == "" { 56 | os.Setenv(common.EnvApiSecretKey, API_SECRET) 57 | } 58 | // os.Setenv("APCA_API_VERSION", "v1") 59 | alpaca.SetBaseUrl(BASE_URL) 60 | 61 | // Check if user input a stock, default is SPY 62 | stock := "AAPL" 63 | if len(os.Args[1:]) == 1 { 64 | stock = os.Args[1] 65 | } 66 | 67 | client := alpaca.NewClient(common.Credentials()) 68 | 69 | // Cancel any open orders so they don't interfere with this script 70 | client.CancelAllOrders() 71 | 72 | pos, err := client.GetPosition(stock) 73 | position := int64(0) 74 | if err != nil { 75 | // No position exists 76 | } else { 77 | position = pos.Qty.IntPart() 78 | } 79 | 80 | // Figure out how much money we have to work with, accounting for margin 81 | accountInfo, err := client.GetAccount() 82 | if err != nil { 83 | panic(err) 84 | } 85 | 86 | equity, _ := accountInfo.Equity.Float64() 87 | marginMult, err := strconv.ParseFloat(accountInfo.Multiplier, 64) 88 | if err != nil { 89 | panic(err) 90 | } 91 | 92 | totalBuyingPower := marginMult * equity 93 | fmt.Printf("Initial total buying power = %.2f\n", totalBuyingPower) 94 | 95 | alpacaClient = alpacaClientContainer{ 96 | client, 97 | 5, 98 | 4, 99 | .1, 100 | streak{ 101 | 0, 102 | 0, 103 | true, 104 | }, 105 | "", 106 | 0, 107 | time.Now().UTC(), 108 | stock, 109 | position, 110 | equity, 111 | marginMult, 112 | 0, 113 | } 114 | } 115 | 116 | func main() { 117 | USE_POLYGON := false 118 | 119 | // First, cancel any existing orders so they don't impact our buying power. 120 | status, until, limit := "open", time.Now(), 100 121 | orders, _ := alpacaClient.client.ListOrders(&status, &until, &limit, nil) 122 | for _, order := range orders { 123 | _ = alpacaClient.client.CancelOrder(order.ID) 124 | } 125 | 126 | if USE_POLYGON { 127 | stream.SetDataStream("polygon") 128 | if err := stream.Register(fmt.Sprintf("A.%s", alpacaClient.stock), handleAggs); err != nil { 129 | panic(err) 130 | } 131 | } else { 132 | if err := stream.Register(fmt.Sprintf("T.%s", alpacaClient.stock), handleAlpacaAggs); err != nil { 133 | panic(err) 134 | } 135 | } 136 | 137 | if err := stream.Register("trade_updates", handleTrades); err != nil { 138 | panic(err) 139 | } 140 | 141 | select {} 142 | } 143 | 144 | // Listen for second aggregates and perform trading logic 145 | func handleAggs(msg interface{}) { 146 | data := msg.(polygon.StreamAggregate) 147 | 148 | if data.Symbol != alpacaClient.stock { 149 | return 150 | } 151 | 152 | alpacaClient.tickIndex = (alpacaClient.tickIndex + 1) % alpacaClient.tickSize 153 | if alpacaClient.tickIndex == 0 { 154 | // It's time to update 155 | 156 | // Update price info 157 | tickOpen := alpacaClient.lastPrice 158 | tickClose := float64(data.ClosePrice) 159 | alpacaClient.lastPrice = tickClose 160 | 161 | alpacaClient.processTick(tickOpen, tickClose) 162 | } 163 | } 164 | 165 | // Listen for quote data and perform trading logic 166 | func handleAlpacaAggs(msg interface{}) { 167 | data := msg.(alpaca.StreamTrade) 168 | 169 | if data.Symbol != alpacaClient.stock { 170 | return 171 | } 172 | 173 | now := time.Now().UTC() 174 | if now.Sub(alpacaClient.lastTradeTime) < time.Second { 175 | // don't react every tick unless at least 1 second past 176 | return 177 | } 178 | alpacaClient.lastTradeTime = now 179 | 180 | alpacaClient.tickIndex = (alpacaClient.tickIndex + 1) % alpacaClient.tickSize 181 | if alpacaClient.tickIndex == 0 { 182 | // It's time to update 183 | 184 | // Update price info 185 | tickOpen := alpacaClient.lastPrice 186 | tickClose := float64(data.Price) 187 | alpacaClient.lastPrice = tickClose 188 | 189 | alpacaClient.processTick(tickOpen, tickClose) 190 | } 191 | } 192 | 193 | // Listen for updates to our orders 194 | func handleTrades(msg interface{}) { 195 | data := msg.(alpaca.TradeUpdate) 196 | fmt.Printf("%s event received for order %s.\n", data.Event, data.Order.ID) 197 | 198 | if data.Order.Symbol != alpacaClient.stock { 199 | // The order was for a position unrelated to this script 200 | return 201 | } 202 | 203 | eventType := data.Event 204 | oid := data.Order.ID 205 | 206 | if eventType == "fill" || eventType == "partial_fill" { 207 | // Our position size has changed 208 | pos, err := alpacaClient.client.GetPosition(alpacaClient.stock) 209 | if err != nil { 210 | alpacaClient.position = 0 211 | } else { 212 | alpacaClient.position = pos.Qty.IntPart() 213 | } 214 | 215 | fmt.Printf("New position size due to order fill: %d\n", alpacaClient.position) 216 | if eventType == "fill" && alpacaClient.currOrder == oid { 217 | alpacaClient.currOrder = "" 218 | } 219 | } else if eventType == "rejected" || eventType == "canceled" { 220 | if alpacaClient.currOrder == oid { 221 | // Our last order should be removed 222 | alpacaClient.currOrder = "" 223 | } 224 | } else if eventType == "new" { 225 | alpacaClient.currOrder = oid 226 | } else { 227 | fmt.Printf("Unexpected order event type %s received\n", eventType) 228 | } 229 | } 230 | 231 | func (alp alpacaClientContainer) processTick(tickOpen float64, tickClose float64) { 232 | // Update streak info 233 | diff := tickClose - tickOpen 234 | if math.Abs(diff) >= .01 { 235 | // There was a meaningful change in the price 236 | alp.currStreak.count++ 237 | increasing := tickOpen > tickClose 238 | if alp.currStreak.increasing != increasing { 239 | // It moved in the opposite direction of the streak. 240 | // Therefore, the streak is over, and we should reset. 241 | 242 | // Empty out the position 243 | if alp.position != 0 { 244 | _, err := alp.sendOrder(0) 245 | if err != nil { 246 | panic(err) 247 | } 248 | } 249 | 250 | // Reset variables 251 | alp.currStreak.increasing = increasing 252 | alp.currStreak.start = tickOpen 253 | alp.currStreak.count = 0 254 | } else { 255 | // Calculate the number of shares we want to be holding 256 | totalBuyingPower := alp.equity * alp.marginMult 257 | targetValue := math.Pow(2, float64(alp.currStreak.count)) * alp.baseBet * totalBuyingPower 258 | if targetValue > totalBuyingPower { 259 | // Limit the amount we can buy to a bit (1 share) 260 | // less than our total buying power 261 | targetValue = totalBuyingPower - alp.lastPrice 262 | } 263 | targetQty := int(targetValue / alp.lastPrice) 264 | if alp.currStreak.increasing { 265 | targetQty = -targetQty 266 | } 267 | 268 | // We don't want to have two orders open at once 269 | if int64(targetQty)-alp.position != 0 { 270 | if alpacaClient.currOrder != "" { 271 | err := alp.client.CancelOrder(alpacaClient.currOrder) 272 | 273 | if err != nil { 274 | panic(err) 275 | } 276 | 277 | alpacaClient.currOrder = "" 278 | } 279 | 280 | _, err := alp.sendOrder(targetQty) 281 | 282 | if err != nil { 283 | panic(err) 284 | } 285 | } 286 | } 287 | } 288 | 289 | // Update our account balance 290 | acct, err := alp.client.GetAccount() 291 | if err != nil { 292 | panic(err) 293 | } 294 | 295 | alp.equity, _ = acct.Equity.Float64() 296 | } 297 | 298 | func (alp alpacaClientContainer) sendOrder(targetQty int) (string, error) { 299 | delta := float64(int64(targetQty) - alp.position) 300 | 301 | fmt.Printf("Ordering towards %d...\n", targetQty) 302 | 303 | qty := float64(0) 304 | side := alpaca.Side("") 305 | 306 | if delta > 0 { 307 | side = alpaca.Buy 308 | qty = delta 309 | if alp.position < 0 { 310 | qty = math.Min(math.Abs(float64(alp.position)), qty) 311 | } 312 | fmt.Printf("Buying %d shares.\n", int64(qty)) 313 | 314 | } else if delta < 0 { 315 | side = alpaca.Sell 316 | qty = math.Abs(delta) 317 | if alp.position > 0 { 318 | qty = math.Min(math.Abs(float64(alp.position)), qty) 319 | } 320 | fmt.Printf("Selling %d shares.\n", int64(qty)) 321 | } 322 | 323 | // Follow [L] instructions to use limit orders 324 | if qty > 0 { 325 | account, _ := alp.client.GetAccount() 326 | 327 | // [L] Uncomment line below 328 | limitPrice := decimal.NewFromFloat(alp.lastPrice) 329 | 330 | alp.currOrder = randomString() 331 | alp.client.PlaceOrder(alpaca.PlaceOrderRequest{ 332 | AccountID: account.ID, 333 | AssetKey: &alp.stock, 334 | Qty: decimal.NewFromFloat(qty), 335 | Side: side, 336 | Type: alpaca.Limit, // [L] Change to alpaca.Limit 337 | // [L] Uncomment line below 338 | LimitPrice: &limitPrice, 339 | TimeInForce: alpaca.Day, 340 | ClientOrderID: alp.currOrder, 341 | }) 342 | 343 | return alp.currOrder, nil 344 | } 345 | 346 | return "", errors.New("Non-positive quantity given") 347 | } 348 | 349 | func randomString() string { 350 | rand.Seed(time.Now().Unix()) 351 | characters := "abcdefghijklmnopqrstuvwxyz" 352 | resSize := 10 353 | 354 | var output strings.Builder 355 | 356 | for i := 0; i < resSize; i++ { 357 | index := rand.Intn(len(characters)) 358 | output.WriteString(string(characters[index])) 359 | } 360 | return output.String() 361 | } 362 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /polygon/rest.go: -------------------------------------------------------------------------------- 1 | package polygon 2 | 3 | import ( 4 | "bytes" 5 | "encoding/json" 6 | "fmt" 7 | "io/ioutil" 8 | "net/http" 9 | "net/url" 10 | "os" 11 | "strconv" 12 | "time" 13 | 14 | "github.com/alpacahq/alpaca-trade-api-go/common" 15 | try "gopkg.in/matryer/try.v1" 16 | ) 17 | 18 | const ( 19 | aggURL = "%v/v1/historic/agg/%v/%v" 20 | aggv2URL = "%v/v2/aggs/ticker/%v/range/%v/%v/%v/%v" 21 | tradesURL = "%v/v1/historic/trades/%v/%v" 22 | tradesv2URL = "%v/v2/ticks/stocks/trades/%v/%v" 23 | quotesURL = "%v/v1/historic/quotes/%v/%v" 24 | quotesv2URL = "%v/v2/ticks/stocks/nbbo/%v/%v" 25 | exchangeURL = "%v/v1/meta/exchanges" 26 | ) 27 | 28 | var ( 29 | // DefaultClient is the default Polygon client using the 30 | // environment variable set credentials 31 | DefaultClient = NewClient(common.Credentials()) 32 | base = "https://api.polygon.io" 33 | get = func(u *url.URL) (*http.Response, error) { 34 | return http.Get(u.String()) 35 | } 36 | ) 37 | 38 | func init() { 39 | if s := os.Getenv("POLYGON_BASE_URL"); s != "" { 40 | base = s 41 | } 42 | } 43 | 44 | // APIError wraps the detailed code and message supplied 45 | // by Polygon's API for debugging purposes 46 | type APIError struct { 47 | Code string `json:"code"` 48 | Message string `json:"message"` 49 | } 50 | 51 | func (e *APIError) Error() string { 52 | return e.Message 53 | } 54 | 55 | // Client is a Polygon REST API client 56 | type Client struct { 57 | credentials *common.APIKey 58 | } 59 | 60 | // NewClient creates a new Polygon client with specified 61 | // credentials 62 | func NewClient(credentials *common.APIKey) *Client { 63 | return &Client{credentials: credentials} 64 | } 65 | 66 | // GetHistoricAggregates requests Polygon's v1 REST API for historic aggregates 67 | // for the provided resolution based on the provided query parameters. 68 | func (c *Client) GetHistoricAggregates( 69 | symbol string, 70 | resolution AggType, 71 | from, to *time.Time, 72 | limit *int) (*HistoricAggregates, error) { 73 | 74 | u, err := url.Parse(fmt.Sprintf(aggURL, base, resolution, symbol)) 75 | if err != nil { 76 | return nil, err 77 | } 78 | 79 | q := u.Query() 80 | q.Set("apiKey", c.credentials.PolygonKeyID) 81 | 82 | if from != nil { 83 | q.Set("from", from.Format(time.RFC3339)) 84 | } 85 | 86 | if to != nil { 87 | q.Set("to", to.Format(time.RFC3339)) 88 | } 89 | 90 | if limit != nil { 91 | q.Set("limit", strconv.FormatInt(int64(*limit), 10)) 92 | } 93 | 94 | u.RawQuery = q.Encode() 95 | 96 | resp, err := get(u) 97 | if err != nil { 98 | return nil, err 99 | } 100 | 101 | if resp.StatusCode >= http.StatusMultipleChoices { 102 | return nil, fmt.Errorf("status code %v", resp.StatusCode) 103 | } 104 | 105 | agg := &HistoricAggregates{} 106 | 107 | if err = unmarshal(resp, agg); err != nil { 108 | return nil, err 109 | } 110 | 111 | return agg, nil 112 | } 113 | 114 | // GetHistoricAggregates requests Polygon's v2 REST API for historic aggregates 115 | // for the provided resolution based on the provided query parameters. 116 | func (c *Client) GetHistoricAggregatesV2( 117 | symbol string, 118 | multiplier int, 119 | resolution AggType, 120 | from, to *time.Time, 121 | unadjusted *bool) (*HistoricAggregatesV2, error) { 122 | 123 | u, err := url.Parse(fmt.Sprintf(aggv2URL, base, symbol, multiplier, resolution, from.Unix()*1000, to.Unix()*1000)) 124 | if err != nil { 125 | return nil, err 126 | } 127 | 128 | q := u.Query() 129 | q.Set("apiKey", c.credentials.PolygonKeyID) 130 | 131 | if unadjusted != nil { 132 | q.Set("unadjusted", strconv.FormatBool(*unadjusted)) 133 | } 134 | 135 | u.RawQuery = q.Encode() 136 | 137 | resp, err := get(u) 138 | if err != nil { 139 | return nil, err 140 | } 141 | 142 | if resp.StatusCode >= http.StatusMultipleChoices { 143 | return nil, fmt.Errorf("status code %v", resp.StatusCode) 144 | } 145 | 146 | agg := &HistoricAggregatesV2{} 147 | 148 | if err = unmarshal(resp, agg); err != nil { 149 | return nil, err 150 | } 151 | 152 | return agg, nil 153 | } 154 | 155 | // GetHistoricTrades requests polygon's REST API for historic trades 156 | // on the provided date. 157 | // 158 | // Deprecated: This v1 endpoint should no longer be used, as it will be removed from the Polygon API 159 | // in the future. Please use GetHistoricTradesV2 instead. 160 | func (c *Client) GetHistoricTrades( 161 | symbol string, 162 | date string, 163 | opts *GetHistoricTradesParams) (totalTrades *HistoricTrades, err error) { 164 | 165 | offset := int64(0) 166 | limit := int64(10000) 167 | if opts != nil { 168 | offset = opts.Offset 169 | if opts.Limit != 0 { 170 | limit = opts.Limit 171 | } 172 | } 173 | for { 174 | u, err := url.Parse(fmt.Sprintf(tradesURL, base, symbol, date)) 175 | if err != nil { 176 | return nil, err 177 | } 178 | 179 | q := u.Query() 180 | q.Set("apiKey", c.credentials.PolygonKeyID) 181 | q.Set("limit", strconv.FormatInt(limit, 10)) 182 | 183 | if offset > 0 { 184 | q.Set("offset", strconv.FormatInt(offset, 10)) 185 | } 186 | 187 | u.RawQuery = q.Encode() 188 | 189 | var resp *http.Response 190 | 191 | if err = try.Do(func(attempt int) (bool, error) { 192 | resp, err = get(u) 193 | return (attempt < 3), err 194 | }); err != nil { 195 | return nil, err 196 | } 197 | 198 | if resp.StatusCode >= http.StatusMultipleChoices { 199 | return nil, fmt.Errorf("status code %v", resp.StatusCode) 200 | } 201 | 202 | trades := &HistoricTrades{} 203 | 204 | if err = unmarshal(resp, trades); err != nil { 205 | return nil, err 206 | } 207 | 208 | if totalTrades == nil { 209 | totalTrades = trades 210 | } else { 211 | totalTrades.Ticks = append(totalTrades.Ticks, trades.Ticks...) 212 | } 213 | 214 | if len(trades.Ticks) == 10000 { 215 | offset = trades.Ticks[len(trades.Ticks)-1].Timestamp 216 | } else { 217 | break 218 | } 219 | } 220 | 221 | return totalTrades, nil 222 | } 223 | 224 | // GetHistoricTradesV2 requests polygon's REST API for historic trades 225 | // on the provided date. 226 | func (c *Client) GetHistoricTradesV2(ticker string, date string, opts *HistoricTicksV2Params) (*HistoricTradesV2, error) { 227 | u, err := url.Parse(fmt.Sprintf(tradesv2URL, base, ticker, date)) 228 | if err != nil { 229 | return nil, err 230 | } 231 | 232 | q := u.Query() 233 | q.Set("apiKey", c.credentials.PolygonKeyID) 234 | u.RawQuery = q.Encode() 235 | 236 | resp, err := c.get(u, opts) 237 | if err != nil { 238 | return nil, err 239 | } 240 | 241 | trades := &HistoricTradesV2{} 242 | 243 | if err = unmarshal(resp, trades); err != nil { 244 | return nil, err 245 | } 246 | 247 | return trades, nil 248 | } 249 | 250 | // GetHistoricQuotes requests polygon's REST API for historic quotes 251 | // on the provided date. 252 | // 253 | // Deprecated: This v1 endpoint should no longer be used, as it will be removed from the Polygon API 254 | // in the future. Please use GetHistoricQuotesV2 instead. 255 | func (c *Client) GetHistoricQuotes(symbol, date string) (totalQuotes *HistoricQuotes, err error) { 256 | offset := int64(0) 257 | for { 258 | u, err := url.Parse(fmt.Sprintf(quotesURL, base, symbol, date)) 259 | if err != nil { 260 | return nil, err 261 | } 262 | 263 | q := u.Query() 264 | q.Set("apiKey", c.credentials.PolygonKeyID) 265 | q.Set("limit", strconv.FormatInt(10000, 10)) 266 | 267 | if offset > 0 { 268 | q.Set("offset", strconv.FormatInt(offset, 10)) 269 | } 270 | 271 | u.RawQuery = q.Encode() 272 | 273 | var resp *http.Response 274 | 275 | if err = try.Do(func(attempt int) (bool, error) { 276 | resp, err = get(u) 277 | return (attempt < 3), err 278 | }); err != nil { 279 | return nil, err 280 | } 281 | 282 | if resp.StatusCode >= http.StatusMultipleChoices { 283 | return nil, fmt.Errorf("status code %v", resp.StatusCode) 284 | } 285 | 286 | quotes := &HistoricQuotes{} 287 | 288 | if err = unmarshal(resp, quotes); err != nil { 289 | return nil, err 290 | } 291 | 292 | if totalQuotes == nil { 293 | totalQuotes = quotes 294 | } else { 295 | totalQuotes.Ticks = append(totalQuotes.Ticks, quotes.Ticks...) 296 | } 297 | 298 | if len(quotes.Ticks) == 10000 { 299 | offset = quotes.Ticks[len(quotes.Ticks)-1].Timestamp 300 | } else { 301 | break 302 | } 303 | } 304 | 305 | return totalQuotes, nil 306 | } 307 | 308 | // GetHistoricQuotesV2 requests polygon's REST API for historic trades 309 | // on the provided date. 310 | func (c *Client) GetHistoricQuotesV2(ticker string, date string, opts *HistoricTicksV2Params) (*HistoricQuotesV2, error) { 311 | u, err := url.Parse(fmt.Sprintf(quotesv2URL, base, ticker, date)) 312 | if err != nil { 313 | return nil, err 314 | } 315 | 316 | q := u.Query() 317 | q.Set("apiKey", c.credentials.PolygonKeyID) 318 | u.RawQuery = q.Encode() 319 | 320 | resp, err := c.get(u, opts) 321 | if err != nil { 322 | return nil, err 323 | } 324 | 325 | quotes := &HistoricQuotesV2{} 326 | 327 | if err = unmarshal(resp, quotes); err != nil { 328 | return nil, err 329 | } 330 | 331 | return quotes, nil 332 | } 333 | 334 | // GetStockExchanges requests available stock and equity exchanges on polygon.io 335 | func (c *Client) GetStockExchanges() ([]StockExchange, error) { 336 | u, err := url.Parse(fmt.Sprintf(exchangeURL, base)) 337 | if err != nil { 338 | return nil, err 339 | } 340 | 341 | q := u.Query() 342 | q.Set("apiKey", c.credentials.PolygonKeyID) 343 | 344 | u.RawQuery = q.Encode() 345 | 346 | resp, err := get(u) 347 | if err != nil { 348 | return nil, err 349 | } 350 | 351 | if resp.StatusCode >= http.StatusMultipleChoices { 352 | return nil, fmt.Errorf("status code %v", resp.StatusCode) 353 | } 354 | 355 | var exchanges []StockExchange 356 | if err = unmarshal(resp, &exchanges); err != nil { 357 | return nil, err 358 | } 359 | 360 | return exchanges, nil 361 | 362 | } 363 | 364 | // GetHistoricAggregates requests polygon's REST API for historic aggregates 365 | // for the provided resolution based on the provided query parameters using 366 | // the default Polygon client. 367 | func GetHistoricAggregates( 368 | symbol string, 369 | resolution AggType, 370 | from, to *time.Time, 371 | limit *int) (*HistoricAggregates, error) { 372 | return DefaultClient.GetHistoricAggregates(symbol, resolution, from, to, limit) 373 | } 374 | 375 | // GetHistoricTrades requests polygon's REST API for historic trades 376 | // on the provided date using the default Polygon client. 377 | func GetHistoricTrades( 378 | symbol string, 379 | date string, 380 | opts *GetHistoricTradesParams) (totalTrades *HistoricTrades, err error) { 381 | return DefaultClient.GetHistoricTrades(symbol, date, opts) 382 | } 383 | 384 | // GetHistoricQuotes requests polygon's REST API for historic quotes 385 | // on the provided date using the default Polygon client. 386 | func GetHistoricQuotes(symbol, date string) (totalQuotes *HistoricQuotes, err error) { 387 | return DefaultClient.GetHistoricQuotes(symbol, date) 388 | } 389 | 390 | // GetStockExchanges queries Polygon.io REST API for information on available 391 | // stock and equities exchanges 392 | func GetStockExchanges() ([]StockExchange, error) { 393 | return DefaultClient.GetStockExchanges() 394 | } 395 | 396 | func unmarshal(resp *http.Response, data interface{}) error { 397 | defer resp.Body.Close() 398 | 399 | body, err := ioutil.ReadAll(resp.Body) 400 | if err != nil { 401 | return err 402 | } 403 | 404 | return json.Unmarshal(body, data) 405 | } 406 | 407 | func verify(resp *http.Response) (err error) { 408 | if resp.StatusCode >= http.StatusMultipleChoices { 409 | var body []byte 410 | defer resp.Body.Close() 411 | 412 | body, err = ioutil.ReadAll(resp.Body) 413 | if err != nil { 414 | return err 415 | } 416 | fmt.Println(string(body)) 417 | 418 | apiErr := APIError{} 419 | 420 | err = json.Unmarshal(body, &apiErr) 421 | if err == nil { 422 | err = &apiErr 423 | } 424 | } 425 | 426 | return 427 | } 428 | 429 | // Gets data with request body marshalling 430 | func (c *Client) get(u *url.URL, data interface{}) (*http.Response, error) { 431 | buf, err := json.Marshal(data) 432 | if err != nil { 433 | return nil, err 434 | } 435 | 436 | req, err := http.NewRequest(http.MethodGet, u.String(), bytes.NewReader(buf)) 437 | if err != nil { 438 | return nil, err 439 | } 440 | 441 | resp, err := http.DefaultClient.Do(req) 442 | if err != nil { 443 | return nil, err 444 | } 445 | 446 | if err = verify(resp); err != nil { 447 | return nil, err 448 | } 449 | 450 | return resp, nil 451 | } 452 | -------------------------------------------------------------------------------- /examples/long-short/long-short.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "math" 6 | "os" 7 | "sort" 8 | "time" 9 | 10 | "github.com/alpacahq/alpaca-trade-api-go/alpaca" 11 | "github.com/alpacahq/alpaca-trade-api-go/common" 12 | "github.com/shopspring/decimal" 13 | ) 14 | 15 | type alpacaClientContainer struct { 16 | client *alpaca.Client 17 | long bucket 18 | short bucket 19 | allStocks []stockField 20 | blacklist []string 21 | } 22 | type bucket struct { 23 | list []string 24 | qty int 25 | adjustedQty int 26 | equityAmt float64 27 | } 28 | type stockField struct { 29 | name string 30 | pc float64 31 | } 32 | 33 | var alpacaClient alpacaClientContainer 34 | 35 | func init() { 36 | API_KEY := "YOUR_API_KEY_HERE" 37 | API_SECRET := "YOUR_API_SECRET_HERE" 38 | BASE_URL := "https://paper-api.alpaca.markets" 39 | 40 | // Check for environment variables 41 | if common.Credentials().ID == "" { 42 | os.Setenv(common.EnvApiKeyID, API_KEY) 43 | } 44 | if common.Credentials().Secret == "" { 45 | os.Setenv(common.EnvApiSecretKey, API_SECRET) 46 | } 47 | alpaca.SetBaseUrl(BASE_URL) 48 | 49 | // Format the allStocks variable for use in the class. 50 | allStocks := []stockField{} 51 | stockList := []string{"DOMO", "TLRY", "SQ", "MRO", "AAPL", "GM", "SNAP", "SHOP", "SPLK", "BA", "AMZN", "SUI", "SUN", "TSLA", "CGC", "SPWR", "NIO", "CAT", "MSFT", "PANW", "OKTA", "TWTR", "TM", "RTN", "ATVI", "GS", "BAC", "MS", "TWLO", "QCOM"} 52 | for _, stock := range stockList { 53 | allStocks = append(allStocks, stockField{stock, 0}) 54 | } 55 | 56 | alpacaClient = alpacaClientContainer{ 57 | alpaca.NewClient(common.Credentials()), 58 | bucket{[]string{}, -1, -1, 0}, 59 | bucket{[]string{}, -1, -1, 0}, 60 | make([]stockField, len(allStocks)), 61 | []string{}, 62 | } 63 | 64 | copy(alpacaClient.allStocks, allStocks) 65 | } 66 | 67 | func main() { 68 | // First, cancel any existing orders so they don't impact our buying power. 69 | status, until, limit := "open", time.Now(), 100 70 | orders, _ := alpacaClient.client.ListOrders(&status, nil, &until, &limit, nil, nil) 71 | for _, order := range orders { 72 | _ = alpacaClient.client.CancelOrder(order.ID) 73 | } 74 | 75 | // Wait for market to open. 76 | fmt.Println("Waiting for market to open...") 77 | for { 78 | isOpen := alpacaClient.awaitMarketOpen() 79 | if isOpen { 80 | break 81 | } 82 | time.Sleep(1 * time.Minute) 83 | } 84 | fmt.Println("Market Opened.") 85 | 86 | for { 87 | alpacaClient.run() 88 | } 89 | } 90 | 91 | // Rebalance the portfolio every minute, making necessary trades. 92 | func (alp alpacaClientContainer) run() { 93 | 94 | // Figure out when the market will close so we can prepare to sell beforehand. 95 | clock, _ := alpacaClient.client.GetClock() 96 | if clock.NextClose.Sub(clock.Timestamp) < 15*time.Minute { 97 | // Close all positions when 15 minutes til market close. 98 | fmt.Println("Market closing soon. Closing positions.") 99 | 100 | positions, _ := alpacaClient.client.ListPositions() 101 | for _, position := range positions { 102 | var orderSide string 103 | if position.Side == "long" { 104 | orderSide = "sell" 105 | } else { 106 | orderSide = "buy" 107 | } 108 | qty, _ := position.Qty.Float64() 109 | qty = math.Abs(qty) 110 | alpacaClient.submitOrder(int(qty), position.Symbol, orderSide) 111 | } 112 | // Run script again after market close for next trading day. 113 | fmt.Println("Sleeping until market close (15 minutes).") 114 | time.Sleep(15 * time.Minute) 115 | } else { 116 | // Rebalance the portfolio. 117 | alpacaClient.rebalance() 118 | time.Sleep(1 * time.Minute) 119 | } 120 | } 121 | 122 | // Spin until the market is open. 123 | func (alp alpacaClientContainer) awaitMarketOpen() bool { 124 | clock, _ := alpacaClient.client.GetClock() 125 | if clock.IsOpen { 126 | return true 127 | } 128 | timeToOpen := int(clock.NextOpen.Sub(clock.Timestamp).Minutes()) 129 | fmt.Printf("%d minutes until next market open.\n", timeToOpen) 130 | return false 131 | } 132 | 133 | // Rebalance our position after an update. 134 | func (alp alpacaClientContainer) rebalance() { 135 | alpacaClient.rerank() 136 | 137 | fmt.Printf("We are taking a long position in: %v.\n", alpacaClient.long.list) 138 | fmt.Printf("We are taking a short position in: %v.\n", alpacaClient.short.list) 139 | 140 | fmt.Print("We are taking a long position in: ") 141 | fmt.Printf("%v", alpacaClient.long.list) 142 | fmt.Println() 143 | fmt.Print("We are taking a short position in: ") 144 | fmt.Printf("%v", alpacaClient.short.list) 145 | fmt.Println() 146 | 147 | // Clear existing orders again. 148 | status, until, limit := "open", time.Now(), 100 149 | orders, _ := alpacaClient.client.ListOrders(&status, nil, &until, &limit, nil, nil) 150 | for _, order := range orders { 151 | _ = alpacaClient.client.CancelOrder(order.ID) 152 | } 153 | 154 | // Remove positions that are no longer in the short or long list, and make a list of positions that do not need to change. Adjust position quantities if needed. 155 | alpacaClient.blacklist = nil 156 | var executed [2][]string 157 | positions, _ := alpacaClient.client.ListPositions() 158 | for _, position := range positions { 159 | indLong := indexOf(alpacaClient.long.list, position.Symbol) 160 | indShort := indexOf(alpacaClient.short.list, position.Symbol) 161 | 162 | rawQty, _ := position.Qty.Float64() 163 | qty := int(math.Abs(rawQty)) 164 | side := "buy" 165 | if indLong < 0 { 166 | // Position is not in long list. 167 | if indShort < 0 { 168 | // Position not in short list either. Clear position. 169 | if position.Side == "long" { 170 | side = "sell" 171 | } else { 172 | side = "buy" 173 | } 174 | alpacaClient.submitOrder(int(math.Abs(float64(qty))), position.Symbol, side) 175 | } else { 176 | if position.Side == "long" { 177 | // Position changed from long to short. Clear long position to prep for short sell. 178 | side = "sell" 179 | alpacaClient.submitOrder(qty, position.Symbol, side) 180 | } else { 181 | // Position in short list 182 | if qty == alpacaClient.short.qty { 183 | // Position is where we want it. Pass for now 184 | } else { 185 | // Need to adjust position amount. 186 | diff := qty - alpacaClient.short.qty 187 | if diff > 0 { 188 | // Too many short positions. Buy some back to rebalance. 189 | side = "buy" 190 | } else { 191 | // Too little short positions. Sell some more. 192 | diff = int(math.Abs(float64(diff))) 193 | side = "sell" 194 | } 195 | qty = diff 196 | alpacaClient.submitOrder(qty, position.Symbol, side) 197 | } 198 | executed[1] = append(executed[1], position.Symbol) 199 | alpacaClient.blacklist = append(alpacaClient.blacklist, position.Symbol) 200 | } 201 | } 202 | } else { 203 | // Position in long list. 204 | if position.Side == "short" { 205 | // Position changed from short to long. Clear short position to prep for long purchase. 206 | side = "buy" 207 | alpacaClient.submitOrder(qty, position.Symbol, side) 208 | } else { 209 | if qty == alpacaClient.long.qty { 210 | // Position is where we want it. Pass for now. 211 | } else { 212 | // Need to adjust position amount 213 | diff := qty - alpacaClient.long.qty 214 | if diff > 0 { 215 | // Too many long positions. Sell some to rebalance. 216 | side = "sell" 217 | } else { 218 | diff = int(math.Abs(float64(diff))) 219 | side = "buy" 220 | } 221 | qty = diff 222 | alpacaClient.submitOrder(qty, position.Symbol, side) 223 | } 224 | executed[0] = append(executed[0], position.Symbol) 225 | alpacaClient.blacklist = append(alpacaClient.blacklist, position.Symbol) 226 | } 227 | } 228 | } 229 | 230 | // Send orders to all remaining stocks in the long and short list. 231 | longBOResp := alpacaClient.sendBatchOrder(alpacaClient.long.qty, alpacaClient.long.list, "buy") 232 | executed[0] = append(executed[0], longBOResp[0][:]...) 233 | if len(longBOResp[1][:]) > 0 { 234 | // Handle rejected/incomplete orders and determine new quantities to purchase. 235 | 236 | longTPResp := alpacaClient.getTotalPrice(executed[0]) 237 | if longTPResp > 0 { 238 | alpacaClient.long.adjustedQty = int(alpacaClient.long.equityAmt / longTPResp) 239 | } else { 240 | alpacaClient.long.adjustedQty = -1 241 | } 242 | } else { 243 | alpacaClient.long.adjustedQty = -1 244 | } 245 | 246 | shortBOResp := alpacaClient.sendBatchOrder(alpacaClient.short.qty, alpacaClient.short.list, "sell") 247 | executed[1] = append(executed[1], shortBOResp[0][:]...) 248 | if len(shortBOResp[1][:]) > 0 { 249 | // Handle rejected/incomplete orders and determine new quantities to purchase. 250 | shortTPResp := alpacaClient.getTotalPrice(executed[1]) 251 | if shortTPResp > 0 { 252 | alpacaClient.short.adjustedQty = int(alpacaClient.short.equityAmt / shortTPResp) 253 | } else { 254 | alpacaClient.short.adjustedQty = -1 255 | } 256 | } else { 257 | alpacaClient.short.adjustedQty = -1 258 | } 259 | 260 | // Reorder stocks that didn't throw an error so that the equity quota is reached. 261 | if alpacaClient.long.adjustedQty > -1 { 262 | alpacaClient.long.qty = alpacaClient.long.adjustedQty - alpacaClient.long.qty 263 | for _, stock := range executed[0] { 264 | alpacaClient.submitOrder(alpacaClient.long.qty, stock, "buy") 265 | } 266 | } 267 | 268 | if alpacaClient.short.adjustedQty > -1 { 269 | alpacaClient.short.qty = alpacaClient.short.adjustedQty - alpacaClient.short.qty 270 | for _, stock := range executed[1] { 271 | alpacaClient.submitOrder(alpacaClient.short.qty, stock, "sell") 272 | } 273 | } 274 | } 275 | 276 | // Re-rank all stocks to adjust longs and shorts. 277 | func (alp alpacaClientContainer) rerank() { 278 | alpacaClient.rank() 279 | 280 | // Grabs the top and bottom quarter of the sorted stock list to get the long and short lists. 281 | longShortAmount := int(len(alpacaClient.allStocks) / 4) 282 | alpacaClient.long.list = nil 283 | alpacaClient.short.list = nil 284 | 285 | for i, stock := range alpacaClient.allStocks { 286 | if i < longShortAmount { 287 | alpacaClient.short.list = append(alpacaClient.short.list, stock.name) 288 | } else if i > (len(alpacaClient.allStocks) - 1 - longShortAmount) { 289 | alpacaClient.long.list = append(alpacaClient.long.list, stock.name) 290 | } else { 291 | continue 292 | } 293 | } 294 | 295 | // Determine amount to long/short based on total stock price of each bucket. 296 | account, _ := alpacaClient.client.GetAccount() 297 | equity, _ := account.Cash.Float64() 298 | positions, _ := alpacaClient.client.ListPositions() 299 | for _, position := range positions { 300 | rawVal, _ := position.MarketValue.Float64() 301 | equity += rawVal 302 | } 303 | 304 | alpacaClient.short.equityAmt = equity * 0.30 305 | alpacaClient.long.equityAmt = equity + alpacaClient.short.equityAmt 306 | 307 | longTotal := alpacaClient.getTotalPrice(alpacaClient.long.list) 308 | shortTotal := alpacaClient.getTotalPrice(alpacaClient.short.list) 309 | 310 | alpacaClient.long.qty = int(alpacaClient.long.equityAmt / longTotal) 311 | alpacaClient.short.qty = int(alpacaClient.short.equityAmt / shortTotal) 312 | } 313 | 314 | // Get the total price of the array of input stocks. 315 | func (alp alpacaClientContainer) getTotalPrice(arr []string) float64 { 316 | totalPrice := 0.0 317 | for _, stock := range arr { 318 | numBars := 1 319 | bar, _ := alpacaClient.client.GetSymbolBars(stock, alpaca.ListBarParams{Timeframe: "minute", Limit: &numBars}) 320 | totalPrice += float64(bar[0].Close) 321 | } 322 | return totalPrice 323 | } 324 | 325 | // Submit an order if quantity is above 0. 326 | func (alp alpacaClientContainer) submitOrder(qty int, symbol string, side string) error { 327 | account, _ := alpacaClient.client.GetAccount() 328 | if qty > 0 { 329 | adjSide := alpaca.Side(side) 330 | _, err := alpacaClient.client.PlaceOrder(alpaca.PlaceOrderRequest{ 331 | AccountID: account.ID, 332 | AssetKey: &symbol, 333 | Qty: decimal.NewFromFloat(float64(qty)), 334 | Side: adjSide, 335 | Type: "market", 336 | TimeInForce: "day", 337 | }) 338 | if err == nil { 339 | fmt.Printf("Market order of | %d %s %s | completed.\n", qty, symbol, side) 340 | } else { 341 | fmt.Printf("Order of | %d %s %s | did not go through.\n", qty, symbol, side) 342 | } 343 | return err 344 | } 345 | fmt.Printf("Quantity is <= 0, order of | %d %s %s | not sent.\n", qty, symbol, side) 346 | return nil 347 | } 348 | 349 | // Submit a batch order that returns completed and uncompleted orders. 350 | func (alp alpacaClientContainer) sendBatchOrder(qty int, stocks []string, side string) [2][]string { 351 | var executed []string 352 | var incomplete []string 353 | for _, stock := range stocks { 354 | index := indexOf(alpacaClient.blacklist, stock) 355 | if index == -1 { 356 | resp := alpacaClient.submitOrder(qty, stock, side) 357 | if resp != nil { 358 | incomplete = append(incomplete, stock) 359 | } else { 360 | executed = append(executed, stock) 361 | } 362 | } 363 | } 364 | return [2][]string{executed, incomplete} 365 | } 366 | 367 | // Get percent changes of the stock prices over the past 10 days. 368 | func (alp alpacaClientContainer) getPercentChanges() { 369 | length := 10 370 | for i, stock := range alpacaClient.allStocks { 371 | startTime, endTime := time.Unix(time.Now().Unix()-int64(length*60), 0), time.Now() 372 | bars, _ := alpacaClient.client.GetSymbolBars(stock.name, alpaca.ListBarParams{Timeframe: "minute", StartDt: &startTime, EndDt: &endTime}) 373 | percentChange := (bars[len(bars)-1].Close - bars[0].Open) / bars[0].Open 374 | alpacaClient.allStocks[i].pc = float64(percentChange) 375 | } 376 | } 377 | 378 | // Mechanism used to rank the stocks, the basis of the Long-Short Equity Strategy. 379 | func (alp alpacaClientContainer) rank() { 380 | // Ranks all stocks by percent change over the past 10 days (higher is better). 381 | alpacaClient.getPercentChanges() 382 | 383 | // Sort the stocks in place by the percent change field (marked by pc). 384 | sort.Slice(alpacaClient.allStocks, func(i, j int) bool { 385 | return alpacaClient.allStocks[i].pc < alpacaClient.allStocks[j].pc 386 | }) 387 | } 388 | 389 | // Helper method to imitate the indexOf array method. 390 | func indexOf(arr []string, str string) int { 391 | for i, elem := range arr { 392 | if elem == str { 393 | return i 394 | } 395 | } 396 | return -1 397 | } 398 | -------------------------------------------------------------------------------- /v2/stream/datav2.go: -------------------------------------------------------------------------------- 1 | package stream 2 | 3 | import ( 4 | "bytes" 5 | "context" 6 | "errors" 7 | "fmt" 8 | "log" 9 | "net/http" 10 | "net/url" 11 | "os" 12 | "strings" 13 | "sync" 14 | "sync/atomic" 15 | "time" 16 | 17 | "github.com/alpacahq/alpaca-trade-api-go/common" 18 | "github.com/vmihailenco/msgpack/v5" 19 | "nhooyr.io/websocket" 20 | ) 21 | 22 | var ( 23 | // DataStreamURL is the URL for the data websocket stream. 24 | // The DATA_PROXY_WS environment variable overrides it. 25 | DataStreamURL = "https://stream.data.alpaca.markets" 26 | 27 | // MaxConnectionAttempts is the maximum number of retries for connecting to the websocket 28 | MaxConnectionAttempts = 3 29 | 30 | messageBufferSize = 1000 31 | ) 32 | 33 | var ( 34 | stream *datav2stream 35 | ) 36 | 37 | type datav2stream struct { 38 | // opts 39 | feed string 40 | 41 | // connection flow 42 | conn *websocket.Conn 43 | authenticated atomic.Value 44 | closed atomic.Value 45 | 46 | // handlers 47 | tradeHandlers map[string]func(trade Trade) 48 | quoteHandlers map[string]func(quote Quote) 49 | barHandlers map[string]func(bar Bar) 50 | 51 | // concurrency 52 | readerOnce sync.Once 53 | wsWriteMutex sync.Mutex 54 | wsReadMutex sync.Mutex 55 | handlersMutex sync.RWMutex 56 | } 57 | 58 | func newDatav2Stream() *datav2stream { 59 | if s := os.Getenv("DATA_PROXY_WS"); s != "" { 60 | DataStreamURL = s 61 | } 62 | stream = &datav2stream{ 63 | feed: "iex", 64 | authenticated: atomic.Value{}, 65 | tradeHandlers: make(map[string]func(trade Trade)), 66 | quoteHandlers: make(map[string]func(quote Quote)), 67 | barHandlers: make(map[string]func(bar Bar)), 68 | } 69 | 70 | stream.authenticated.Store(false) 71 | stream.closed.Store(false) 72 | 73 | return stream 74 | } 75 | 76 | func (s *datav2stream) useFeed(feed string) error { 77 | feed = strings.ToLower(feed) 78 | switch feed { 79 | case "iex", "sip": 80 | default: 81 | return errors.New("unsupported feed: " + feed) 82 | } 83 | if s.feed == feed { 84 | return nil 85 | } 86 | s.feed = feed 87 | if s.conn == nil { 88 | return nil 89 | } 90 | // we are already connected to the wrong feed 91 | // to restart it we close the stream and readForever will do the reconnect 92 | return s.close(false) 93 | } 94 | 95 | func (s *datav2stream) subscribeTrades(handler func(trade Trade), symbols ...string) error { 96 | if err := s.ensureRunning(); err != nil { 97 | return err 98 | } 99 | 100 | if err := s.sub(symbols, nil, nil); err != nil { 101 | return err 102 | } 103 | 104 | s.handlersMutex.Lock() 105 | defer s.handlersMutex.Unlock() 106 | 107 | for _, symbol := range symbols { 108 | s.tradeHandlers[symbol] = handler 109 | } 110 | 111 | return nil 112 | } 113 | 114 | func (s *datav2stream) subscribeQuotes(handler func(quote Quote), symbols ...string) error { 115 | if err := s.ensureRunning(); err != nil { 116 | return err 117 | } 118 | 119 | if err := s.sub(nil, symbols, nil); err != nil { 120 | return err 121 | } 122 | 123 | s.handlersMutex.Lock() 124 | defer s.handlersMutex.Unlock() 125 | 126 | for _, symbol := range symbols { 127 | s.quoteHandlers[symbol] = handler 128 | } 129 | 130 | return nil 131 | } 132 | 133 | func (s *datav2stream) subscribeBars(handler func(bar Bar), symbols ...string) error { 134 | if err := s.ensureRunning(); err != nil { 135 | return err 136 | } 137 | 138 | if err := s.sub(nil, nil, symbols); err != nil { 139 | return err 140 | } 141 | 142 | s.handlersMutex.Lock() 143 | defer s.handlersMutex.Unlock() 144 | 145 | for _, symbol := range symbols { 146 | s.barHandlers[symbol] = handler 147 | } 148 | 149 | return nil 150 | } 151 | 152 | func (s *datav2stream) unsubscribe(trades []string, quotes []string, bars []string) error { 153 | if err := s.ensureRunning(); err != nil { 154 | return err 155 | } 156 | 157 | s.handlersMutex.Lock() 158 | defer s.handlersMutex.Unlock() 159 | 160 | for _, trade := range trades { 161 | delete(s.tradeHandlers, trade) 162 | } 163 | for _, quote := range quotes { 164 | delete(s.quoteHandlers, quote) 165 | } 166 | for _, bar := range bars { 167 | delete(s.barHandlers, bar) 168 | } 169 | 170 | if err := s.unsub(trades, quotes, bars); err != nil { 171 | return err 172 | } 173 | 174 | return nil 175 | } 176 | 177 | func (s *datav2stream) close(final bool) error { 178 | if s.conn == nil { 179 | return nil 180 | } 181 | 182 | s.wsWriteMutex.Lock() 183 | defer s.wsWriteMutex.Unlock() 184 | 185 | if final { 186 | s.closed.Store(true) 187 | } 188 | 189 | if err := s.conn.Close(websocket.StatusNormalClosure, ""); err != nil { 190 | return err 191 | } 192 | s.conn = nil 193 | return nil 194 | } 195 | 196 | func (s *datav2stream) ensureRunning() error { 197 | if s.conn != nil { 198 | return nil 199 | } 200 | 201 | if err := s.connect(); err != nil { 202 | return err 203 | } 204 | s.readerOnce.Do(func() { 205 | go s.readForever() 206 | }) 207 | return nil 208 | } 209 | 210 | func (s *datav2stream) connect() error { 211 | // first close any previous connections 212 | s.close(false) 213 | 214 | s.authenticated.Store(false) 215 | conn, err := openSocket(s.feed) 216 | if err != nil { 217 | return err 218 | } 219 | s.conn = conn 220 | if err := s.auth(); err != nil { 221 | return err 222 | } 223 | trades := make([]string, 0, len(s.tradeHandlers)) 224 | for trade := range s.tradeHandlers { 225 | trades = append(trades, trade) 226 | } 227 | quotes := make([]string, 0, len(s.quoteHandlers)) 228 | for quote := range s.quoteHandlers { 229 | quotes = append(quotes, quote) 230 | } 231 | bars := make([]string, 0) 232 | for bar := range s.barHandlers { 233 | bars = append(bars, bar) 234 | } 235 | return s.sub(trades, quotes, bars) 236 | } 237 | 238 | func (s *datav2stream) readForever() { 239 | msgs := make(chan []byte, messageBufferSize) 240 | defer close(msgs) 241 | go s.handleMessages(msgs) 242 | 243 | for { 244 | s.wsReadMutex.Lock() 245 | msgType, b, err := s.conn.Read(context.TODO()) 246 | s.wsReadMutex.Unlock() 247 | 248 | if err != nil { 249 | if websocket.CloseStatus(err) == websocket.StatusNormalClosure { 250 | // if this was a graceful closure, don't reconnect 251 | if s.closed.Load().(bool) { 252 | return 253 | } 254 | } else { 255 | log.Printf("alpaca stream read error (%v)", err) 256 | } 257 | 258 | err := s.connect() 259 | if err != nil { 260 | panic(err) 261 | } 262 | } 263 | if msgType != websocket.MessageBinary { 264 | continue 265 | } 266 | msgs <- b 267 | } 268 | } 269 | 270 | func (s *datav2stream) handleMessages(msgs <-chan []byte) { 271 | for msg := range msgs { 272 | if err := s.handleMessage(msg); err != nil { 273 | log.Printf("error handling incoming message: %v", err) 274 | } 275 | } 276 | } 277 | 278 | func (s *datav2stream) handleMessage(b []byte) error { 279 | d := msgpack.GetDecoder() 280 | defer msgpack.PutDecoder(d) 281 | 282 | reader := bytes.NewReader(b) 283 | d.Reset(reader) 284 | 285 | arrLen, err := d.DecodeArrayLen() 286 | if err != nil || arrLen < 1 { 287 | return err 288 | } 289 | 290 | for i := 0; i < arrLen; i++ { 291 | var n int 292 | n, err = d.DecodeMapLen() 293 | if err != nil { 294 | return err 295 | } 296 | if n < 1 { 297 | continue 298 | } 299 | 300 | key, err := d.DecodeString() 301 | if err != nil { 302 | return err 303 | } 304 | if key != "T" { 305 | return fmt.Errorf("first key is not T but: %s", key) 306 | } 307 | T, err := d.DecodeString() 308 | if err != nil { 309 | return err 310 | } 311 | n-- // T already processed 312 | 313 | switch T { 314 | case "t": 315 | err = s.handleTrade(d, n) 316 | case "q": 317 | err = s.handleQuote(d, n) 318 | case "b": 319 | err = s.handleBar(d, n) 320 | default: 321 | err = s.handleOther(d, n) 322 | } 323 | if err != nil { 324 | return err 325 | } 326 | } 327 | 328 | return nil 329 | } 330 | 331 | func (s *datav2stream) handleTrade(d *msgpack.Decoder, n int) error { 332 | trade := Trade{} 333 | for i := 0; i < n; i++ { 334 | key, err := d.DecodeString() 335 | if err != nil { 336 | return err 337 | } 338 | switch key { 339 | case "i": 340 | trade.ID, err = d.DecodeInt64() 341 | case "S": 342 | trade.Symbol, err = d.DecodeString() 343 | case "x": 344 | trade.Exchange, err = d.DecodeString() 345 | case "p": 346 | trade.Price, err = d.DecodeFloat64() 347 | case "s": 348 | trade.Size, err = d.DecodeUint32() 349 | case "t": 350 | trade.Timestamp, err = d.DecodeTime() 351 | case "c": 352 | var condCount int 353 | if condCount, err = d.DecodeArrayLen(); err != nil { 354 | return err 355 | } 356 | trade.Conditions = make([]string, condCount) 357 | for c := 0; c < condCount; c++ { 358 | if cond, err := d.DecodeString(); err != nil { 359 | return err 360 | } else { 361 | trade.Conditions[c] = cond 362 | } 363 | } 364 | case "z": 365 | trade.Tape, err = d.DecodeString() 366 | default: 367 | err = d.Skip() 368 | } 369 | if err != nil { 370 | return err 371 | } 372 | } 373 | s.handlersMutex.RLock() 374 | defer s.handlersMutex.RUnlock() 375 | handler, ok := s.tradeHandlers[trade.Symbol] 376 | if !ok { 377 | if handler, ok = s.tradeHandlers["*"]; !ok { 378 | return nil 379 | } 380 | } 381 | handler(trade) 382 | return nil 383 | } 384 | 385 | func (s *datav2stream) handleQuote(d *msgpack.Decoder, n int) error { 386 | quote := Quote{} 387 | for i := 0; i < n; i++ { 388 | key, err := d.DecodeString() 389 | if err != nil { 390 | return err 391 | } 392 | switch key { 393 | case "S": 394 | quote.Symbol, err = d.DecodeString() 395 | case "bx": 396 | quote.BidExchange, err = d.DecodeString() 397 | case "bp": 398 | quote.BidPrice, err = d.DecodeFloat64() 399 | case "bs": 400 | quote.BidSize, err = d.DecodeUint32() 401 | case "ax": 402 | quote.AskExchange, err = d.DecodeString() 403 | case "ap": 404 | quote.AskPrice, err = d.DecodeFloat64() 405 | case "as": 406 | quote.AskSize, err = d.DecodeUint32() 407 | case "t": 408 | quote.Timestamp, err = d.DecodeTime() 409 | case "c": 410 | var condCount int 411 | if condCount, err = d.DecodeArrayLen(); err != nil { 412 | return err 413 | } 414 | quote.Conditions = make([]string, condCount) 415 | for c := 0; c < condCount; c++ { 416 | if cond, err := d.DecodeString(); err != nil { 417 | return err 418 | } else { 419 | quote.Conditions[c] = cond 420 | } 421 | } 422 | case "z": 423 | quote.Tape, err = d.DecodeString() 424 | default: 425 | err = d.Skip() 426 | } 427 | if err != nil { 428 | return err 429 | } 430 | } 431 | s.handlersMutex.RLock() 432 | defer s.handlersMutex.RUnlock() 433 | handler, ok := s.quoteHandlers[quote.Symbol] 434 | if !ok { 435 | if handler, ok = s.quoteHandlers["*"]; !ok { 436 | return nil 437 | } 438 | } 439 | handler(quote) 440 | return nil 441 | } 442 | 443 | func (s *datav2stream) handleBar(d *msgpack.Decoder, n int) error { 444 | bar := Bar{} 445 | for i := 0; i < n; i++ { 446 | key, err := d.DecodeString() 447 | if err != nil { 448 | return err 449 | } 450 | switch key { 451 | case "S": 452 | bar.Symbol, err = d.DecodeString() 453 | case "o": 454 | bar.Open, err = d.DecodeFloat64() 455 | case "h": 456 | bar.High, err = d.DecodeFloat64() 457 | case "l": 458 | bar.Low, err = d.DecodeFloat64() 459 | case "c": 460 | bar.Close, err = d.DecodeFloat64() 461 | case "v": 462 | bar.Volume, err = d.DecodeUint64() 463 | case "t": 464 | bar.Timestamp, err = d.DecodeTime() 465 | default: 466 | err = d.Skip() 467 | } 468 | if err != nil { 469 | return err 470 | } 471 | } 472 | s.handlersMutex.RLock() 473 | defer s.handlersMutex.RUnlock() 474 | handler, ok := s.barHandlers[bar.Symbol] 475 | if !ok { 476 | if handler, ok = s.barHandlers["*"]; !ok { 477 | return nil 478 | } 479 | } 480 | handler(bar) 481 | return nil 482 | } 483 | 484 | func (s *datav2stream) handleOther(d *msgpack.Decoder, n int) error { 485 | for i := 0; i < n; i++ { 486 | // key 487 | if err := d.Skip(); err != nil { 488 | return err 489 | } 490 | // value 491 | if err := d.Skip(); err != nil { 492 | return err 493 | } 494 | } 495 | return nil 496 | } 497 | 498 | func (s *datav2stream) sub(trades []string, quotes []string, bars []string) error { 499 | return s.handleSubscription(true, trades, quotes, bars) 500 | } 501 | 502 | func (s *datav2stream) unsub(trades []string, quotes []string, bars []string) error { 503 | return s.handleSubscription(false, trades, quotes, bars) 504 | } 505 | 506 | func (s *datav2stream) handleSubscription(subscribe bool, trades []string, quotes []string, bars []string) error { 507 | if len(trades)+len(quotes)+len(bars) == 0 { 508 | return nil 509 | } 510 | 511 | action := "subscribe" 512 | if !subscribe { 513 | action = "unsubscribe" 514 | } 515 | 516 | msg, err := msgpack.Marshal(map[string]interface{}{ 517 | "action": action, 518 | "trades": trades, 519 | "quotes": quotes, 520 | "bars": bars, 521 | }) 522 | if err != nil { 523 | return err 524 | } 525 | 526 | s.wsWriteMutex.Lock() 527 | defer s.wsWriteMutex.Unlock() 528 | 529 | if err := s.conn.Write(context.TODO(), websocket.MessageBinary, msg); err != nil { 530 | return err 531 | } 532 | 533 | return nil 534 | } 535 | 536 | func (s *datav2stream) isAuthenticated() bool { 537 | return s.authenticated.Load().(bool) 538 | } 539 | 540 | func (s *datav2stream) auth() (err error) { 541 | if s.isAuthenticated() { 542 | return 543 | } 544 | 545 | msg, err := msgpack.Marshal(map[string]string{ 546 | "action": "auth", 547 | "key": common.Credentials().ID, 548 | "secret": common.Credentials().Secret, 549 | }) 550 | if err != nil { 551 | return err 552 | } 553 | 554 | s.wsWriteMutex.Lock() 555 | defer s.wsWriteMutex.Unlock() 556 | 557 | if err := s.conn.Write(context.TODO(), websocket.MessageBinary, msg); err != nil { 558 | return err 559 | } 560 | 561 | var resps []map[string]interface{} 562 | 563 | // ensure the auth response comes in a timely manner 564 | ctx, cancel := context.WithTimeout(context.TODO(), 5*time.Second) 565 | defer cancel() 566 | 567 | s.wsReadMutex.Lock() 568 | defer s.wsReadMutex.Unlock() 569 | 570 | _, b, err := s.conn.Read(ctx) 571 | if err != nil { 572 | return err 573 | } 574 | if err := msgpack.Unmarshal(b, &resps); err != nil { 575 | return err 576 | } 577 | if len(resps) < 1 { 578 | return errors.New("received empty array") 579 | } 580 | if resps[0]["T"] == "error" { 581 | errString, ok := resps[0]["msg"].(string) 582 | if ok { 583 | return errors.New("failed to authorize: " + errString) 584 | } 585 | } 586 | if resps[0]["T"] != "success" || resps[0]["msg"] != "authenticated" { 587 | return errors.New("failed to authorize alpaca stream") 588 | } 589 | 590 | s.authenticated.Store(true) 591 | 592 | return 593 | } 594 | 595 | func openSocket(feed string) (*websocket.Conn, error) { 596 | scheme := "wss" 597 | ub, _ := url.Parse(DataStreamURL) 598 | switch ub.Scheme { 599 | case "http", "ws": 600 | scheme = "ws" 601 | } 602 | u := url.URL{Scheme: scheme, Host: ub.Host, Path: "/v2/" + strings.ToLower(feed)} 603 | for attempts := 1; attempts <= MaxConnectionAttempts; attempts++ { 604 | c, _, err := websocket.Dial(context.TODO(), u.String(), &websocket.DialOptions{ 605 | CompressionMode: websocket.CompressionContextTakeover, 606 | HTTPHeader: http.Header{ 607 | "Content-Type": []string{"application/msgpack"}, 608 | }, 609 | }) 610 | if err == nil { 611 | return c, readConnected(c) 612 | } 613 | log.Printf("failed to open Alpaca data stream: %v", err) 614 | if attempts == MaxConnectionAttempts { 615 | return nil, err 616 | } 617 | time.Sleep(time.Second) 618 | } 619 | return nil, errors.New("could not open Alpaca data stream (max retries exceeded)") 620 | } 621 | 622 | func readConnected(conn *websocket.Conn) error { 623 | _, b, err := conn.Read(context.TODO()) 624 | if err != nil { 625 | return err 626 | } 627 | var resps []map[string]interface{} 628 | if err := msgpack.Unmarshal(b, &resps); err != nil { 629 | return err 630 | } 631 | if len(resps) < 1 { 632 | return errors.New("received empty array") 633 | } 634 | if resps[0]["T"] != "success" || resps[0]["msg"] != "connected" { 635 | return errors.New("missing connected message") 636 | } 637 | return nil 638 | } 639 | -------------------------------------------------------------------------------- /alpaca/entities.go: -------------------------------------------------------------------------------- 1 | package alpaca 2 | 3 | import ( 4 | "time" 5 | 6 | v2 "github.com/alpacahq/alpaca-trade-api-go/v2" 7 | "github.com/shopspring/decimal" 8 | ) 9 | 10 | type Account struct { 11 | ID string `json:"id"` 12 | AccountNumber string `json:"account_number"` 13 | CreatedAt time.Time `json:"created_at"` 14 | UpdatedAt time.Time `json:"updated_at"` 15 | DeletedAt *time.Time `json:"deleted_at"` 16 | Status string `json:"status"` 17 | Currency string `json:"currency"` 18 | Cash decimal.Decimal `json:"cash"` 19 | CashWithdrawable decimal.Decimal `json:"cash_withdrawable"` 20 | TradingBlocked bool `json:"trading_blocked"` 21 | TransfersBlocked bool `json:"transfers_blocked"` 22 | AccountBlocked bool `json:"account_blocked"` 23 | ShortingEnabled bool `json:"shorting_enabled"` 24 | BuyingPower decimal.Decimal `json:"buying_power"` 25 | PatternDayTrader bool `json:"pattern_day_trader"` 26 | DaytradeCount int64 `json:"daytrade_count"` 27 | DaytradingBuyingPower decimal.Decimal `json:"daytrading_buying_power"` 28 | RegTBuyingPower decimal.Decimal `json:"regt_buying_power"` 29 | Equity decimal.Decimal `json:"equity"` 30 | LastEquity decimal.Decimal `json:"last_equity"` 31 | Multiplier string `json:"multiplier"` 32 | InitialMargin decimal.Decimal `json:"initial_margin"` 33 | MaintenanceMargin decimal.Decimal `json:"maintenance_margin"` 34 | LastMaintenanceMargin decimal.Decimal `json:"last_maintenance_margin"` 35 | LongMarketValue decimal.Decimal `json:"long_market_value"` 36 | ShortMarketValue decimal.Decimal `json:"short_market_value"` 37 | PortfolioValue decimal.Decimal `json:"portfolio_value"` 38 | } 39 | 40 | type Order struct { 41 | ID string `json:"id"` 42 | ClientOrderID string `json:"client_order_id"` 43 | CreatedAt time.Time `json:"created_at"` 44 | UpdatedAt time.Time `json:"updated_at"` 45 | SubmittedAt time.Time `json:"submitted_at"` 46 | FilledAt *time.Time `json:"filled_at"` 47 | ExpiredAt *time.Time `json:"expired_at"` 48 | CanceledAt *time.Time `json:"canceled_at"` 49 | FailedAt *time.Time `json:"failed_at"` 50 | ReplacedAt *time.Time `json:"replaced_at"` 51 | Replaces *string `json:"replaces"` 52 | ReplacedBy *string `json:"replaced_by"` 53 | AssetID string `json:"asset_id"` 54 | Symbol string `json:"symbol"` 55 | Exchange string `json:"exchange"` 56 | Class string `json:"asset_class"` 57 | Qty decimal.Decimal `json:"qty"` 58 | Notional decimal.Decimal `json:"notional"` 59 | FilledQty decimal.Decimal `json:"filled_qty"` 60 | Type OrderType `json:"order_type"` 61 | Side Side `json:"side"` 62 | TimeInForce TimeInForce `json:"time_in_force"` 63 | LimitPrice *decimal.Decimal `json:"limit_price"` 64 | FilledAvgPrice *decimal.Decimal `json:"filled_avg_price"` 65 | StopPrice *decimal.Decimal `json:"stop_price"` 66 | TrailPrice *decimal.Decimal `json:"trail_price"` 67 | TrailPercent *decimal.Decimal `json:"trail_percent"` 68 | Hwm *decimal.Decimal `json:"hwm"` 69 | Status string `json:"status"` 70 | ExtendedHours bool `json:"extended_hours"` 71 | Legs *[]Order `json:"legs"` 72 | } 73 | 74 | type Position struct { 75 | AssetID string `json:"asset_id"` 76 | Symbol string `json:"symbol"` 77 | Exchange string `json:"exchange"` 78 | Class string `json:"asset_class"` 79 | AccountID string `json:"account_id"` 80 | EntryPrice decimal.Decimal `json:"avg_entry_price"` 81 | Qty decimal.Decimal `json:"qty"` 82 | Side string `json:"side"` 83 | MarketValue decimal.Decimal `json:"market_value"` 84 | CostBasis decimal.Decimal `json:"cost_basis"` 85 | UnrealizedPL decimal.Decimal `json:"unrealized_pl"` 86 | UnrealizedPLPC decimal.Decimal `json:"unrealized_plpc"` 87 | CurrentPrice decimal.Decimal `json:"current_price"` 88 | LastdayPrice decimal.Decimal `json:"lastday_price"` 89 | ChangeToday decimal.Decimal `json:"change_today"` 90 | } 91 | 92 | type Asset struct { 93 | ID string `json:"id"` 94 | Name string `json:"name"` 95 | Exchange string `json:"exchange"` 96 | Class string `json:"asset_class"` 97 | Symbol string `json:"symbol"` 98 | Status string `json:"status"` 99 | Tradable bool `json:"tradable"` 100 | Marginable bool `json:"marginable"` 101 | Shortable bool `json:"shortable"` 102 | EasyToBorrow bool `json:"easy_to_borrow"` 103 | } 104 | 105 | type Fundamental struct { 106 | AssetID string `json:"asset_id"` 107 | Symbol string `json:"symbol"` 108 | FullName string `json:"full_name"` 109 | IndustryName string `json:"industry_name"` 110 | IndustryGroup string `json:"industry_group"` 111 | Sector string `json:"sector"` 112 | PERatio float32 `json:"pe_ratio"` 113 | PEGRatio float32 `json:"peg_ratio"` 114 | Beta float32 `json:"beta"` 115 | EPS float32 `json:"eps"` 116 | MarketCap int64 `json:"market_cap"` 117 | SharesOutstanding int64 `json:"shares_outstanding"` 118 | AvgVol int64 `json:"avg_vol"` 119 | DivRate float32 `json:"div_rate"` 120 | ROE float32 `json:"roe"` 121 | ROA float32 `json:"roa"` 122 | PS float32 `json:"ps"` 123 | PC float32 `json:"pc"` 124 | GrossMargin float32 `json:"gross_margin"` 125 | FiftyTwoWeekHigh decimal.Decimal `json:"fifty_two_week_high"` 126 | FiftyTwoWeekLow decimal.Decimal `json:"fifty_two_week_low"` 127 | ShortDescription string `json:"short_description"` 128 | LongDescription string `json:"long_description"` 129 | } 130 | 131 | type Bar struct { 132 | Time int64 `json:"t"` 133 | Open float32 `json:"o"` 134 | High float32 `json:"h"` 135 | Low float32 `json:"l"` 136 | Close float32 `json:"c"` 137 | Volume int32 `json:"v"` 138 | } 139 | 140 | type ListBarParams struct { 141 | Timeframe string `url:"timeframe,omitempty"` 142 | StartDt *time.Time `url:"start_dt,omitempty"` 143 | EndDt *time.Time `url:"end_dt,omitempty"` 144 | Limit *int `url:"limit,omitempty"` 145 | } 146 | 147 | type LastQuote struct { 148 | AskPrice float32 `json:"askprice"` 149 | AskSize int32 `json:"asksize"` 150 | AskExchange int `json:"askexchange"` 151 | BidPrice float32 `json:"bidprice"` 152 | BidSize int32 `json:"bidsize"` 153 | BidExchange int `json:"bidexchange"` 154 | Timestamp int64 `json:"timestamp"` 155 | } 156 | 157 | func (l *LastQuote) Time() time.Time { 158 | return time.Unix(0, l.Timestamp) 159 | } 160 | 161 | type LastQuoteResponse struct { 162 | Status string `json:"status"` 163 | Symbol string `json:"symbol"` 164 | Last LastQuote `json:"last"` 165 | } 166 | 167 | type LastTrade struct { 168 | Price float32 `json:"price"` 169 | Size int32 `json:"size"` 170 | Exchange int `json:"exchange"` 171 | Cond1 int `json:"cond1"` 172 | Cond2 int `json:"cond2"` 173 | Cond3 int `json:"cond3"` 174 | Cond4 int `json:"cond4"` 175 | Timestamp int64 `json:"timestamp"` 176 | } 177 | 178 | func (l *LastTrade) Time() time.Time { 179 | return time.Unix(0, l.Timestamp) 180 | } 181 | 182 | type LastTradeResponse struct { 183 | Status string `json:"status"` 184 | Symbol string `json:"symbol"` 185 | Last LastTrade `json:"last"` 186 | } 187 | 188 | type AggV2 struct { 189 | Timestamp int64 `json:"t"` 190 | Ticker string `json:"T"` 191 | Open float32 `json:"O"` 192 | High float32 `json:"H"` 193 | Low float32 `json:"L"` 194 | Close float32 `json:"C"` 195 | Volume int32 `json:"V"` 196 | NumberOfItems int `json:"n"` 197 | } 198 | 199 | type Aggregates struct { 200 | Ticker string `json:"ticker"` 201 | Status string `json:"status"` 202 | Adjusted bool `json:"adjusted"` 203 | QueryCount int `json:"queryCount"` 204 | ResultsCount int `json:"resultsCount"` 205 | Results []AggV2 `json:"results"` 206 | } 207 | 208 | type tradeResponse struct { 209 | Symbol string `json:"symbol"` 210 | NextPageToken *string `json:"next_page_token"` 211 | Trades []v2.Trade `json:"trades"` 212 | } 213 | 214 | type quoteResponse struct { 215 | Symbol string `json:"symbol"` 216 | NextPageToken *string `json:"next_page_token"` 217 | Quotes []v2.Quote `json:"quotes"` 218 | } 219 | 220 | type barResponse struct { 221 | Symbol string `json:"symbol"` 222 | NextPageToken *string `json:"next_page_token"` 223 | Bars []v2.Bar `json:"bars"` 224 | } 225 | 226 | type latestTradeResponse struct { 227 | Symbol string `json:"symbol"` 228 | Trade v2.Trade `json:"trade"` 229 | } 230 | 231 | type latestQuoteResponse struct { 232 | Symbol string `json:"symbol"` 233 | Quote v2.Quote `json:"quote"` 234 | } 235 | 236 | type CalendarDay struct { 237 | Date string `json:"date"` 238 | Open string `json:"open"` 239 | Close string `json:"close"` 240 | } 241 | 242 | type Clock struct { 243 | Timestamp time.Time `json:"timestamp"` 244 | IsOpen bool `json:"is_open"` 245 | NextOpen time.Time `json:"next_open"` 246 | NextClose time.Time `json:"next_close"` 247 | } 248 | 249 | type AccountConfigurations struct { 250 | DtbpCheck DtbpCheck `json:"dtbp_check"` 251 | NoShorting bool `json:"no_shorting"` 252 | TradeConfirmEmail TradeConfirmEmail `json:"trade_confirm_email"` 253 | TradeSuspendedByUser bool `json:"trade_suspended_by_user"` 254 | } 255 | 256 | type AccountActivity struct { 257 | ID string `json:"id"` 258 | ActivityType string `json:"activity_type"` 259 | TransactionTime time.Time `json:"transaction_time"` 260 | Type string `json:"type"` 261 | Price decimal.Decimal `json:"price"` 262 | Qty decimal.Decimal `json:"qty"` 263 | Side string `json:"side"` 264 | Symbol string `json:"symbol"` 265 | LeavesQty decimal.Decimal `json:"leaves_qty"` 266 | CumQty decimal.Decimal `json:"cum_qty"` 267 | Date string `json:"date"` 268 | NetAmount decimal.Decimal `json:"net_amount"` 269 | Description string `json:"description"` 270 | PerShareAmount decimal.Decimal `json:"per_share_amount"` 271 | OrderID string `json:"order_id"` 272 | } 273 | 274 | type PortfolioHistory struct { 275 | BaseValue decimal.Decimal `json:"base_value"` 276 | Equity []decimal.Decimal `json:"equity"` 277 | ProfitLoss []decimal.Decimal `json:"profit_loss"` 278 | ProfitLossPct []decimal.Decimal `json:"profit_loss_pct"` 279 | Timeframe RangeFreq `json:"timeframe"` 280 | Timestamp []int64 `json:"timestamp"` 281 | } 282 | 283 | type PlaceOrderRequest struct { 284 | AccountID string `json:"-"` 285 | AssetKey *string `json:"symbol"` 286 | Qty decimal.Decimal `json:"qty"` 287 | Notional decimal.Decimal `json:"notional"` 288 | Side Side `json:"side"` 289 | Type OrderType `json:"type"` 290 | TimeInForce TimeInForce `json:"time_in_force"` 291 | LimitPrice *decimal.Decimal `json:"limit_price"` 292 | ExtendedHours bool `json:"extended_hours"` 293 | StopPrice *decimal.Decimal `json:"stop_price"` 294 | ClientOrderID string `json:"client_order_id"` 295 | OrderClass OrderClass `json:"order_class"` 296 | TakeProfit *TakeProfit `json:"take_profit"` 297 | StopLoss *StopLoss `json:"stop_loss"` 298 | TrailPrice *decimal.Decimal `json:"trail_price"` 299 | TrailPercent *decimal.Decimal `json:"trail_percent"` 300 | } 301 | 302 | type TakeProfit struct { 303 | LimitPrice *decimal.Decimal `json:"limit_price"` 304 | } 305 | 306 | type StopLoss struct { 307 | LimitPrice *decimal.Decimal `json:"limit_price"` 308 | StopPrice *decimal.Decimal `json:"stop_price"` 309 | } 310 | 311 | type OrderAttributes struct { 312 | TakeProfitLimitPrice *decimal.Decimal `json:"take_profit_limit_price,omitempty"` 313 | StopLossStopPrice *decimal.Decimal `json:"stop_loss_stop_price,omitempty"` 314 | StopLossLimitPrice *decimal.Decimal `json:"stop_loss_limit_price,omitempty"` 315 | } 316 | 317 | type ReplaceOrderRequest struct { 318 | Qty *decimal.Decimal `json:"qty"` 319 | LimitPrice *decimal.Decimal `json:"limit_price"` 320 | StopPrice *decimal.Decimal `json:"stop_price"` 321 | Trail *decimal.Decimal `json:"trail"` 322 | TimeInForce TimeInForce `json:"time_in_force"` 323 | ClientOrderID string `json:"client_order_id"` 324 | } 325 | 326 | type UpdateWatchListRequest struct { 327 | Name string `json:"name"` 328 | Symbols []string `json:"symbols"` 329 | } 330 | 331 | type AccountConfigurationsRequest struct { 332 | DtbpCheck *string `json:"dtbp_check"` 333 | NoShorting *bool `json:"no_shorting"` 334 | TradeConfirmEmail *string `json:"trade_confirm_email"` 335 | TradeSuspendedByUser *bool `json:"trade_suspended_by_user"` 336 | } 337 | 338 | type AccountActivitiesRequest struct { 339 | ActivityTypes *[]string `json:"activity_types"` 340 | Date *time.Time `json:"date"` 341 | Until *time.Time `json:"until"` 342 | After *time.Time `json:"after"` 343 | Direction *string `json:"direction"` 344 | PageSize *int `json:"page_size"` 345 | PageToken *string `json:"page_token"` 346 | } 347 | 348 | type Side string 349 | 350 | const ( 351 | Buy Side = "buy" 352 | Sell Side = "sell" 353 | ) 354 | 355 | type OrderType string 356 | 357 | const ( 358 | Market OrderType = "market" 359 | Limit OrderType = "limit" 360 | Stop OrderType = "stop" 361 | StopLimit OrderType = "stop_limit" 362 | TrailingStop OrderType = "trailing_stop" 363 | ) 364 | 365 | type OrderClass string 366 | 367 | const ( 368 | Bracket OrderClass = "bracket" 369 | Oto OrderClass = "oto" 370 | Oco OrderClass = "oco" 371 | Simple OrderClass = "simple" 372 | ) 373 | 374 | type TimeInForce string 375 | 376 | const ( 377 | Day TimeInForce = "day" 378 | GTC TimeInForce = "gtc" 379 | OPG TimeInForce = "opg" 380 | IOC TimeInForce = "ioc" 381 | FOK TimeInForce = "fok" 382 | GTX TimeInForce = "gtx" 383 | GTD TimeInForce = "gtd" 384 | CLS TimeInForce = "cls" 385 | ) 386 | 387 | type DtbpCheck string 388 | 389 | const ( 390 | Entry DtbpCheck = "entry" 391 | Exit DtbpCheck = "exit" 392 | Both DtbpCheck = "both" 393 | ) 394 | 395 | type TradeConfirmEmail string 396 | 397 | const ( 398 | None TradeConfirmEmail = "none" 399 | All TradeConfirmEmail = "all" 400 | ) 401 | 402 | type RangeFreq string 403 | 404 | const ( 405 | Min1 RangeFreq = "1Min" 406 | Min5 RangeFreq = "5Min" 407 | Min15 RangeFreq = "15Min" 408 | Hour1 RangeFreq = "1H" 409 | Day1 RangeFreq = "1D" 410 | ) 411 | 412 | // stream 413 | 414 | // ClientMsg is the standard message sent by clients of the stream interface 415 | type ClientMsg struct { 416 | Action string `json:"action" msgpack:"action"` 417 | Data interface{} `json:"data" msgpack:"data"` 418 | } 419 | 420 | // ServerMsg is the standard message sent by the server to update clients 421 | // of the stream interface 422 | type ServerMsg struct { 423 | Stream string `json:"stream" msgpack:"stream"` 424 | Data interface{} `json:"data"` 425 | } 426 | 427 | type TradeUpdate struct { 428 | Event string `json:"event"` 429 | Price string `json:"price"` 430 | Timestamp time.Time `json:"timestamp"` 431 | PositionQty string `json:"position_qty"` 432 | Order Order `json:"order"` 433 | } 434 | 435 | type StreamAgg struct { 436 | Event string `json:"ev"` 437 | Symbol string `json:"T"` 438 | Open float32 `json:"o"` 439 | High float32 `json:"h"` 440 | Low float32 `json:"l"` 441 | Close float32 `json:"c"` 442 | Volume int32 `json:"v"` 443 | Start int64 `json:"s"` 444 | End int64 `json:"e"` 445 | OpenPrice float32 `json:"op"` 446 | AccumulatedVolume int32 `json:"av"` 447 | VWAP float32 `json:"vw"` 448 | } 449 | 450 | func (s *StreamAgg) Time() time.Time { 451 | // milliseconds 452 | return time.Unix(0, s.Start*1e6) 453 | } 454 | 455 | type StreamQuote struct { 456 | Event string `json:"ev"` 457 | Symbol string `json:"T"` 458 | BidPrice float32 `json:"p"` 459 | BidSize int32 `json:"s"` 460 | BidExchange int `json:"x"` 461 | AskPrice float32 `json:"P"` 462 | AskSize int32 `json:"S"` 463 | AskExchange int `json:"X"` 464 | Timestamp int64 `json:"t"` 465 | } 466 | 467 | func (s *StreamQuote) Time() time.Time { 468 | // nanoseconds 469 | return time.Unix(0, s.Timestamp) 470 | } 471 | 472 | type StreamTrade struct { 473 | Event string `json:"ev"` 474 | Symbol string `json:"T"` 475 | TradeID string `json:"i"` 476 | Exchange int `json:"x"` 477 | Price float32 `json:"p"` 478 | Size int32 `json:"s"` 479 | Timestamp int64 `json:"t"` 480 | Conditions []int `json:"c"` 481 | TapeID int `json:"z"` 482 | } 483 | 484 | func (s *StreamTrade) Time() time.Time { 485 | // nanoseconds 486 | return time.Unix(0, s.Timestamp) 487 | } 488 | -------------------------------------------------------------------------------- /alpaca/alpaca_test.go: -------------------------------------------------------------------------------- 1 | package alpaca 2 | 3 | import ( 4 | "bytes" 5 | "encoding/json" 6 | "fmt" 7 | "io" 8 | "io/ioutil" 9 | "net/http" 10 | "strings" 11 | "testing" 12 | "time" 13 | 14 | v2 "github.com/alpacahq/alpaca-trade-api-go/v2" 15 | "github.com/shopspring/decimal" 16 | "github.com/stretchr/testify/assert" 17 | "github.com/stretchr/testify/require" 18 | "github.com/stretchr/testify/suite" 19 | ) 20 | 21 | // Copied from Gobroker v4.9.162 to test API conversion to backend struct 22 | type CreateOrderRequest struct { 23 | AccountID string `json:"-"` 24 | ClientID string `json:"client_id"` 25 | OrderClass string `json:"order_class"` 26 | OrderID *string `json:"-"` 27 | ClientOrderID string `json:"client_order_id"` 28 | AssetKey string `json:"symbol"` 29 | AssetID string `json:"-"` 30 | Qty decimal.Decimal `json:"qty"` 31 | Side string `json:"side"` 32 | Type string `json:"type"` 33 | TimeInForce string `json:"time_in_force"` 34 | LimitPrice *decimal.Decimal `json:"limit_price"` 35 | StopPrice *decimal.Decimal `json:"stop_price"` 36 | ExtendedHours bool `json:"extended_hours"` 37 | Source *string `json:"source"` 38 | TakeProfit *TakeProfit `json:"take_profit"` 39 | StopLoss *StopLoss `json:"stop_loss"` 40 | } 41 | 42 | type AlpacaTestSuite struct { 43 | suite.Suite 44 | } 45 | 46 | func TestAlpacaTestSuite(t *testing.T) { 47 | suite.Run(t, new(AlpacaTestSuite)) 48 | } 49 | 50 | func (s *AlpacaTestSuite) TestAlpaca() { 51 | // get account 52 | { 53 | // successful 54 | do = func(c *Client, req *http.Request) (*http.Response, error) { 55 | account := Account{ 56 | ID: "some_id", 57 | } 58 | return &http.Response{ 59 | Body: genBody(account), 60 | }, nil 61 | } 62 | 63 | acct, err := GetAccount() 64 | assert.NoError(s.T(), err) 65 | assert.NotNil(s.T(), acct) 66 | assert.Equal(s.T(), "some_id", acct.ID) 67 | 68 | // api failure 69 | do = func(c *Client, req *http.Request) (*http.Response, error) { 70 | return &http.Response{}, fmt.Errorf("fail") 71 | } 72 | 73 | acct, err = GetAccount() 74 | assert.Error(s.T(), err) 75 | assert.Nil(s.T(), acct) 76 | } 77 | 78 | // list positions 79 | { 80 | // successful 81 | do = func(c *Client, req *http.Request) (*http.Response, error) { 82 | positions := []Position{ 83 | {Symbol: "APCA"}, 84 | } 85 | return &http.Response{ 86 | Body: genBody(positions), 87 | }, nil 88 | } 89 | 90 | positions, err := ListPositions() 91 | assert.NoError(s.T(), err) 92 | assert.Len(s.T(), positions, 1) 93 | 94 | // api failure 95 | do = func(c *Client, req *http.Request) (*http.Response, error) { 96 | return &http.Response{}, fmt.Errorf("fail") 97 | } 98 | 99 | positions, err = ListPositions() 100 | assert.Error(s.T(), err) 101 | assert.Nil(s.T(), positions) 102 | } 103 | 104 | // get aggregates 105 | { 106 | // successful 107 | aggregatesJSON := `{ 108 | "ticker":"AAPL", 109 | "status":"OK", 110 | "adjusted":true, 111 | "queryCount":2, 112 | "resultsCount":2, 113 | "results":[ 114 | {"v":52521891,"o":300.95,"c":288.08,"h":302.53,"l":286.13,"t":1582606800000,"n":1}, 115 | {"v":46094168,"o":286.53,"c":292.69,"h":297.88,"l":286.5,"t":1582693200000,"n":1} 116 | ] 117 | }` 118 | 119 | expectedAggregates := Aggregates{ 120 | Ticker: "AAPL", 121 | Status: "OK", 122 | Adjusted: true, 123 | QueryCount: 2, 124 | ResultsCount: 2, 125 | Results: []AggV2{ 126 | { 127 | Volume: 52521891, 128 | Open: 300.95, 129 | Close: 288.08, 130 | High: 302.53, 131 | Low: 286.13, 132 | Timestamp: 1582606800000, 133 | NumberOfItems: 1, 134 | }, 135 | { 136 | Volume: 46094168, 137 | Open: 286.53, 138 | Close: 292.69, 139 | High: 297.88, 140 | Low: 286.5, 141 | Timestamp: 1582693200000, 142 | NumberOfItems: 1, 143 | }, 144 | }, 145 | } 146 | do = func(c *Client, req *http.Request) (*http.Response, error) { 147 | return &http.Response{ 148 | Body: ioutil.NopCloser(strings.NewReader(aggregatesJSON)), 149 | }, nil 150 | } 151 | 152 | actualAggregates, err := GetAggregates("AAPL", "minute", "2020-02-25", "2020-02-26") 153 | assert.NotNil(s.T(), actualAggregates) 154 | assert.NoError(s.T(), err) 155 | assert.EqualValues(s.T(), &expectedAggregates, actualAggregates) 156 | 157 | // api failure 158 | do = func(c *Client, req *http.Request) (*http.Response, error) { 159 | return &http.Response{}, fmt.Errorf("fail") 160 | } 161 | 162 | actualAggregates, err = GetAggregates("AAPL", "minute", "2020-02-25", "2020-02-26") 163 | assert.Error(s.T(), err) 164 | assert.Nil(s.T(), actualAggregates) 165 | } 166 | // get last quote 167 | { 168 | // successful 169 | lastQuoteJSON := `{ 170 | "status": "success", 171 | "symbol": "AAPL", 172 | "last": { 173 | "askprice":291.24, 174 | "asksize":1, 175 | "askexchange":2, 176 | "bidprice":291.76, 177 | "bidsize":1, 178 | "bidexchange":9, 179 | "timestamp":1582754386000 180 | } 181 | }` 182 | 183 | expectedLastQuote := LastQuoteResponse{ 184 | Status: "success", 185 | Symbol: "AAPL", 186 | Last: LastQuote{ 187 | AskPrice: 291.24, 188 | AskSize: 1, 189 | AskExchange: 2, 190 | BidPrice: 291.76, 191 | BidSize: 1, 192 | BidExchange: 9, 193 | Timestamp: 1582754386000, 194 | }, 195 | } 196 | do = func(c *Client, req *http.Request) (*http.Response, error) { 197 | return &http.Response{ 198 | Body: ioutil.NopCloser(strings.NewReader(lastQuoteJSON)), 199 | }, nil 200 | } 201 | 202 | actualLastQuote, err := GetLastQuote("AAPL") 203 | assert.NotNil(s.T(), actualLastQuote) 204 | assert.NoError(s.T(), err) 205 | assert.EqualValues(s.T(), &expectedLastQuote, actualLastQuote) 206 | 207 | // api failure 208 | do = func(c *Client, req *http.Request) (*http.Response, error) { 209 | return &http.Response{}, fmt.Errorf("fail") 210 | } 211 | 212 | actualLastQuote, err = GetLastQuote("AAPL") 213 | assert.Error(s.T(), err) 214 | assert.Nil(s.T(), actualLastQuote) 215 | } 216 | 217 | // get last trade 218 | { 219 | // successful 220 | lastTradeJSON := `{ 221 | "status": "success", 222 | "symbol": "AAPL", 223 | "last": { 224 | "price":290.614, 225 | "size":200, 226 | "exchange":2, 227 | "cond1":12, 228 | "cond2":1, 229 | "cond3":2, 230 | "cond4":3, 231 | "timestamp":1582756144000 232 | } 233 | }` 234 | expectedLastTrade := LastTradeResponse{ 235 | Status: "success", 236 | Symbol: "AAPL", 237 | Last: LastTrade{ 238 | Price: 290.614, 239 | Size: 200, 240 | Exchange: 2, 241 | Cond1: 12, 242 | Cond2: 1, 243 | Cond3: 2, 244 | Cond4: 3, 245 | Timestamp: 1582756144000, 246 | }, 247 | } 248 | do = func(c *Client, req *http.Request) (*http.Response, error) { 249 | return &http.Response{ 250 | Body: ioutil.NopCloser(strings.NewReader(lastTradeJSON)), 251 | }, nil 252 | } 253 | 254 | actualLastTrade, err := GetLastTrade("AAPL") 255 | assert.NotNil(s.T(), actualLastTrade) 256 | assert.NoError(s.T(), err) 257 | assert.EqualValues(s.T(), &expectedLastTrade, actualLastTrade) 258 | 259 | // api failure 260 | do = func(c *Client, req *http.Request) (*http.Response, error) { 261 | return &http.Response{}, fmt.Errorf("fail") 262 | } 263 | 264 | actualLastTrade, err = GetLastTrade("AAPL") 265 | assert.Error(s.T(), err) 266 | assert.Nil(s.T(), actualLastTrade) 267 | } 268 | 269 | // get latest trade 270 | { 271 | // successful 272 | latestTradeJSON := `{ 273 | "symbol": "AAPL", 274 | "trade": { 275 | "t": "2021-04-20T12:40:34.484136Z", 276 | "x": "J", 277 | "p": 134.7, 278 | "s": 20, 279 | "c": [ 280 | "@", 281 | "T", 282 | "I" 283 | ], 284 | "i": 32, 285 | "z": "C" 286 | } 287 | }` 288 | expectedLatestTrade := v2.Trade{ 289 | ID: 32, 290 | Exchange: "J", 291 | Price: 134.7, 292 | Size: 20, 293 | Timestamp: time.Date(2021, 4, 20, 12, 40, 34, 484136000, time.UTC), 294 | Conditions: []string{"@", "T", "I"}, 295 | Tape: "C", 296 | } 297 | do = func(c *Client, req *http.Request) (*http.Response, error) { 298 | return &http.Response{ 299 | Body: ioutil.NopCloser(strings.NewReader(latestTradeJSON)), 300 | }, nil 301 | } 302 | 303 | actualLatestTrade, err := GetLatestTrade("AAPL") 304 | require.NoError(s.T(), err) 305 | require.NotNil(s.T(), actualLatestTrade) 306 | assert.Equal(s.T(), expectedLatestTrade, *actualLatestTrade) 307 | 308 | // api failure 309 | do = func(c *Client, req *http.Request) (*http.Response, error) { 310 | return &http.Response{}, fmt.Errorf("fail") 311 | } 312 | 313 | actualLatestTrade, err = GetLatestTrade("AAPL") 314 | assert.Error(s.T(), err) 315 | assert.Nil(s.T(), actualLatestTrade) 316 | } 317 | 318 | // get latest quote 319 | { 320 | // successful 321 | latestQuoteJSON := `{ 322 | "symbol": "AAPL", 323 | "quote": { 324 | "t": "2021-04-20T13:01:57.822745906Z", 325 | "ax": "Q", 326 | "ap": 134.68, 327 | "as": 1, 328 | "bx": "K", 329 | "bp": 134.66, 330 | "bs": 29, 331 | "c": [ 332 | "R" 333 | ] 334 | } 335 | }` 336 | expectedLatestQuote := v2.Quote{ 337 | BidExchange: "K", 338 | BidPrice: 134.66, 339 | BidSize: 29, 340 | AskExchange: "Q", 341 | AskPrice: 134.68, 342 | AskSize: 1, 343 | Timestamp: time.Date(2021, 04, 20, 13, 1, 57, 822745906, time.UTC), 344 | Conditions: []string{"R"}, 345 | } 346 | do = func(c *Client, req *http.Request) (*http.Response, error) { 347 | return &http.Response{ 348 | Body: ioutil.NopCloser(strings.NewReader(latestQuoteJSON)), 349 | }, nil 350 | } 351 | 352 | actualLatestQuote, err := GetLatestQuote("AAPL") 353 | require.NoError(s.T(), err) 354 | require.NotNil(s.T(), actualLatestQuote) 355 | assert.Equal(s.T(), expectedLatestQuote, *actualLatestQuote) 356 | 357 | // api failure 358 | do = func(c *Client, req *http.Request) (*http.Response, error) { 359 | return &http.Response{}, fmt.Errorf("fail") 360 | } 361 | 362 | actualLatestQuote, err = GetLatestQuote("AAPL") 363 | assert.Error(s.T(), err) 364 | assert.Nil(s.T(), actualLatestQuote) 365 | } 366 | 367 | // get snapshot 368 | { 369 | // successful 370 | snapshotJSON := `{ 371 | "symbol": "AAPL", 372 | "latestTrade": { 373 | "t": "2021-05-03T14:45:50.456Z", 374 | "x": "D", 375 | "p": 133.55, 376 | "s": 200, 377 | "c": [ 378 | "@" 379 | ], 380 | "i": 61462, 381 | "z": "C" 382 | }, 383 | "latestQuote": { 384 | "t": "2021-05-03T14:45:50.532316972Z", 385 | "ax": "P", 386 | "ap": 133.55, 387 | "as": 7, 388 | "bx": "Q", 389 | "bp": 133.54, 390 | "bs": 9, 391 | "c": [ 392 | "R" 393 | ] 394 | }, 395 | "minuteBar": { 396 | "t": "2021-05-03T14:44:00Z", 397 | "o": 133.485, 398 | "h": 133.4939, 399 | "l": 133.42, 400 | "c": 133.445, 401 | "v": 182818 402 | }, 403 | "dailyBar": { 404 | "t": "2021-05-03T04:00:00Z", 405 | "o": 132.04, 406 | "h": 134.07, 407 | "l": 131.83, 408 | "c": 133.445, 409 | "v": 25094213 410 | }, 411 | "prevDailyBar": { 412 | "t": "2021-04-30T04:00:00Z", 413 | "o": 131.82, 414 | "h": 133.56, 415 | "l": 131.065, 416 | "c": 131.46, 417 | "v": 109506363 418 | } 419 | }` 420 | expected := v2.Snapshot{ 421 | LatestTrade: &v2.Trade{ 422 | ID: 61462, 423 | Exchange: "D", 424 | Price: 133.55, 425 | Size: 200, 426 | Timestamp: time.Date(2021, 5, 3, 14, 45, 50, 456000000, time.UTC), 427 | Conditions: []string{"@"}, 428 | Tape: "C", 429 | }, 430 | LatestQuote: &v2.Quote{ 431 | BidExchange: "Q", 432 | BidPrice: 133.54, 433 | BidSize: 9, 434 | AskExchange: "P", 435 | AskPrice: 133.55, 436 | AskSize: 7, 437 | Timestamp: time.Date(2021, 5, 3, 14, 45, 50, 532316972, time.UTC), 438 | Conditions: []string{"R"}, 439 | }, 440 | MinuteBar: &v2.Bar{ 441 | Open: 133.485, 442 | High: 133.4939, 443 | Low: 133.42, 444 | Close: 133.445, 445 | Volume: 182818, 446 | Timestamp: time.Date(2021, 5, 3, 14, 44, 0, 0, time.UTC), 447 | }, 448 | DailyBar: &v2.Bar{ 449 | Open: 132.04, 450 | High: 134.07, 451 | Low: 131.83, 452 | Close: 133.445, 453 | Volume: 25094213, 454 | Timestamp: time.Date(2021, 5, 3, 4, 0, 0, 0, time.UTC), 455 | }, 456 | PrevDailyBar: &v2.Bar{ 457 | Open: 131.82, 458 | High: 133.56, 459 | Low: 131.065, 460 | Close: 131.46, 461 | Volume: 109506363, 462 | Timestamp: time.Date(2021, 4, 30, 4, 0, 0, 0, time.UTC), 463 | }, 464 | } 465 | do = func(c *Client, req *http.Request) (*http.Response, error) { 466 | return &http.Response{ 467 | Body: ioutil.NopCloser(strings.NewReader(snapshotJSON)), 468 | }, nil 469 | } 470 | 471 | got, err := GetSnapshot("AAPL") 472 | require.NoError(s.T(), err) 473 | require.NotNil(s.T(), got) 474 | assert.Equal(s.T(), expected, *got) 475 | 476 | // api failure 477 | do = func(c *Client, req *http.Request) (*http.Response, error) { 478 | return &http.Response{}, fmt.Errorf("fail") 479 | } 480 | 481 | got, err = GetSnapshot("AAPL") 482 | assert.Error(s.T(), err) 483 | assert.Nil(s.T(), got) 484 | } 485 | 486 | // get snapshots 487 | { 488 | // successful 489 | snapshotsJSON := `{ 490 | "AAPL": { 491 | "latestTrade": { 492 | "t": "2021-05-03T14:48:06.563Z", 493 | "x": "D", 494 | "p": 133.4201, 495 | "s": 145, 496 | "c": [ 497 | "@" 498 | ], 499 | "i": 62700, 500 | "z": "C" 501 | }, 502 | "latestQuote": { 503 | "t": "2021-05-03T14:48:07.257820915Z", 504 | "ax": "Q", 505 | "ap": 133.43, 506 | "as": 7, 507 | "bx": "Q", 508 | "bp": 133.42, 509 | "bs": 15, 510 | "c": [ 511 | "R" 512 | ] 513 | }, 514 | "minuteBar": { 515 | "t": "2021-05-03T14:47:00Z", 516 | "o": 133.4401, 517 | "h": 133.48, 518 | "l": 133.37, 519 | "c": 133.42, 520 | "v": 207020 521 | }, 522 | "dailyBar": { 523 | "t": "2021-05-03T04:00:00Z", 524 | "o": 132.04, 525 | "h": 134.07, 526 | "l": 131.83, 527 | "c": 133.42, 528 | "v": 25846800 529 | }, 530 | "prevDailyBar": { 531 | "t": "2021-04-30T04:00:00Z", 532 | "o": 131.82, 533 | "h": 133.56, 534 | "l": 131.065, 535 | "c": 131.46, 536 | "v": 109506363 537 | } 538 | }, 539 | "MSFT": { 540 | "latestTrade": { 541 | "t": "2021-05-03T14:48:06.36Z", 542 | "x": "D", 543 | "p": 253.8738, 544 | "s": 100, 545 | "c": [ 546 | "@" 547 | ], 548 | "i": 22973, 549 | "z": "C" 550 | }, 551 | "latestQuote": { 552 | "t": "2021-05-03T14:48:07.243353456Z", 553 | "ax": "N", 554 | "ap": 253.89, 555 | "as": 2, 556 | "bx": "Q", 557 | "bp": 253.87, 558 | "bs": 2, 559 | "c": [ 560 | "R" 561 | ] 562 | }, 563 | "minuteBar": { 564 | "t": "2021-05-03T14:47:00Z", 565 | "o": 253.78, 566 | "h": 253.869, 567 | "l": 253.78, 568 | "c": 253.855, 569 | "v": 25717 570 | }, 571 | "dailyBar": { 572 | "t": "2021-05-03T04:00:00Z", 573 | "o": 253.34, 574 | "h": 254.35, 575 | "l": 251.8, 576 | "c": 253.855, 577 | "v": 6100459 578 | }, 579 | "prevDailyBar": null 580 | }, 581 | "INVALID": null 582 | }` 583 | do = func(c *Client, req *http.Request) (*http.Response, error) { 584 | return &http.Response{ 585 | Body: ioutil.NopCloser(strings.NewReader(snapshotsJSON)), 586 | }, nil 587 | } 588 | 589 | got, err := GetSnapshots([]string{"AAPL", "MSFT", "INVALID"}) 590 | require.NoError(s.T(), err) 591 | require.NotNil(s.T(), got) 592 | assert.Len(s.T(), got, 3) 593 | assert.Nil(s.T(), got["INVALID"]) 594 | assert.EqualValues(s.T(), 7, got["AAPL"].LatestQuote.AskSize) 595 | assert.EqualValues(s.T(), 6100459, got["MSFT"].DailyBar.Volume) 596 | assert.Nil(s.T(), got["MSFT"].PrevDailyBar) 597 | 598 | // api failure 599 | do = func(c *Client, req *http.Request) (*http.Response, error) { 600 | return &http.Response{}, fmt.Errorf("fail") 601 | } 602 | 603 | got, err = GetSnapshots([]string{"AAPL", "CLDR"}) 604 | assert.Error(s.T(), err) 605 | assert.Nil(s.T(), got) 606 | } 607 | 608 | // get clock 609 | { 610 | // successful 611 | do = func(c *Client, req *http.Request) (*http.Response, error) { 612 | clock := Clock{ 613 | Timestamp: time.Now(), 614 | IsOpen: true, 615 | NextOpen: time.Now(), 616 | NextClose: time.Now(), 617 | } 618 | return &http.Response{ 619 | Body: genBody(clock), 620 | }, nil 621 | } 622 | 623 | clock, err := GetClock() 624 | assert.NoError(s.T(), err) 625 | assert.NotNil(s.T(), clock) 626 | assert.True(s.T(), clock.IsOpen) 627 | 628 | // api failure 629 | do = func(c *Client, req *http.Request) (*http.Response, error) { 630 | return &http.Response{}, fmt.Errorf("fail") 631 | } 632 | 633 | clock, err = GetClock() 634 | assert.Error(s.T(), err) 635 | assert.Nil(s.T(), clock) 636 | } 637 | 638 | // get calendar 639 | { 640 | // successful 641 | do = func(c *Client, req *http.Request) (*http.Response, error) { 642 | calendar := []CalendarDay{ 643 | { 644 | Date: "2018-01-01", 645 | Open: time.Now().Format(time.RFC3339), 646 | Close: time.Now().Format(time.RFC3339), 647 | }, 648 | } 649 | return &http.Response{ 650 | Body: genBody(calendar), 651 | }, nil 652 | } 653 | 654 | start := "2018-01-01" 655 | end := "2018-01-02" 656 | 657 | calendar, err := GetCalendar(&start, &end) 658 | assert.NoError(s.T(), err) 659 | assert.Len(s.T(), calendar, 1) 660 | 661 | // api failure 662 | do = func(c *Client, req *http.Request) (*http.Response, error) { 663 | return &http.Response{}, fmt.Errorf("fail") 664 | } 665 | 666 | calendar, err = GetCalendar(&start, &end) 667 | assert.Error(s.T(), err) 668 | assert.Nil(s.T(), calendar) 669 | } 670 | 671 | // list orders 672 | { 673 | // successful 674 | do = func(c *Client, req *http.Request) (*http.Response, error) { 675 | orders := []Order{ 676 | { 677 | ID: "some_id", 678 | }, 679 | } 680 | return &http.Response{ 681 | Body: genBody(orders), 682 | }, nil 683 | } 684 | 685 | status := "new" 686 | until := time.Now() 687 | limit := 1 688 | 689 | orders, err := ListOrders(&status, nil, &until, &limit, nil, nil) 690 | assert.NoError(s.T(), err) 691 | require.Len(s.T(), orders, 1) 692 | assert.Equal(s.T(), "some_id", orders[0].ID) 693 | 694 | // api failure 695 | do = func(c *Client, req *http.Request) (*http.Response, error) { 696 | return &http.Response{}, fmt.Errorf("fail") 697 | } 698 | 699 | orders, err = ListOrders(&status, nil, &until, &limit, nil, nil) 700 | assert.Error(s.T(), err) 701 | assert.Nil(s.T(), orders) 702 | } 703 | 704 | // place order 705 | { 706 | // successful (w/ Qty) 707 | do = func(c *Client, req *http.Request) (*http.Response, error) { 708 | por := PlaceOrderRequest{} 709 | if err := json.NewDecoder(req.Body).Decode(&por); err != nil { 710 | return nil, err 711 | } 712 | return &http.Response{ 713 | Body: genBody(Order{ 714 | Qty: por.Qty, 715 | Notional: por.Notional, 716 | Side: por.Side, 717 | TimeInForce: por.TimeInForce, 718 | Type: por.Type, 719 | }), 720 | }, nil 721 | } 722 | 723 | req := PlaceOrderRequest{ 724 | AccountID: "some_id", 725 | Qty: decimal.New(1, 0), 726 | Side: Buy, 727 | TimeInForce: GTC, 728 | Type: Limit, 729 | } 730 | 731 | order, err := PlaceOrder(req) 732 | assert.NoError(s.T(), err) 733 | assert.NotNil(s.T(), order) 734 | assert.Equal(s.T(), req.Qty, order.Qty) 735 | assert.True(s.T(), req.Notional.IsZero()) 736 | assert.True(s.T(), order.Notional.IsZero()) 737 | assert.Equal(s.T(), req.Type, order.Type) 738 | 739 | // successful (w/ Notional) 740 | req = PlaceOrderRequest{ 741 | AccountID: "some_id", 742 | Notional: decimal.New(1, 0), 743 | Side: Buy, 744 | TimeInForce: GTC, 745 | Type: Limit, 746 | } 747 | 748 | order, err = PlaceOrder(req) 749 | assert.NoError(s.T(), err) 750 | assert.NotNil(s.T(), order) 751 | assert.Equal(s.T(), req.Notional, order.Notional) 752 | assert.True(s.T(), req.Qty.IsZero()) 753 | assert.True(s.T(), order.Qty.IsZero()) 754 | assert.Equal(s.T(), req.Type, order.Type) 755 | 756 | // api failure 757 | do = func(c *Client, req *http.Request) (*http.Response, error) { 758 | return &http.Response{}, fmt.Errorf("fail") 759 | } 760 | 761 | order, err = PlaceOrder(req) 762 | assert.Error(s.T(), err) 763 | assert.Nil(s.T(), order) 764 | } 765 | 766 | // get order 767 | { 768 | // successful 769 | do = func(c *Client, req *http.Request) (*http.Response, error) { 770 | order := Order{ 771 | ID: "some_order_id", 772 | } 773 | return &http.Response{ 774 | Body: genBody(order), 775 | }, nil 776 | } 777 | 778 | order, err := GetOrder("some_order_id") 779 | assert.NoError(s.T(), err) 780 | assert.NotNil(s.T(), order) 781 | 782 | // api failure 783 | do = func(c *Client, req *http.Request) (*http.Response, error) { 784 | return &http.Response{}, fmt.Errorf("fail") 785 | } 786 | 787 | order, err = GetOrder("some_order_id") 788 | assert.Error(s.T(), err) 789 | assert.Nil(s.T(), order) 790 | } 791 | 792 | // get order by client_order_id 793 | { 794 | // successful 795 | do = func(c *Client, req *http.Request) (*http.Response, error) { 796 | order := Order{ 797 | ClientOrderID: "some_client_order_id", 798 | } 799 | return &http.Response{ 800 | Body: genBody(order), 801 | }, nil 802 | } 803 | 804 | order, err := GetOrderByClientOrderID("some_client_order_id") 805 | assert.NoError(s.T(), err) 806 | assert.NotNil(s.T(), order) 807 | 808 | // api failure 809 | do = func(c *Client, req *http.Request) (*http.Response, error) { 810 | return &http.Response{}, fmt.Errorf("fail") 811 | } 812 | 813 | order, err = GetOrderByClientOrderID("some_client_order_id") 814 | assert.Error(s.T(), err) 815 | assert.Nil(s.T(), order) 816 | } 817 | 818 | // cancel order 819 | { 820 | // successful 821 | do = func(c *Client, req *http.Request) (*http.Response, error) { 822 | return &http.Response{}, nil 823 | } 824 | 825 | assert.Nil(s.T(), CancelOrder("some_order_id")) 826 | 827 | // api failure 828 | do = func(c *Client, req *http.Request) (*http.Response, error) { 829 | return &http.Response{}, fmt.Errorf("fail") 830 | } 831 | 832 | assert.NotNil(s.T(), CancelOrder("some_order_id")) 833 | } 834 | 835 | // list assets 836 | { 837 | // successful 838 | do = func(c *Client, req *http.Request) (*http.Response, error) { 839 | assets := []Asset{ 840 | {ID: "some_id"}, 841 | } 842 | return &http.Response{ 843 | Body: genBody(assets), 844 | }, nil 845 | } 846 | 847 | status := "active" 848 | 849 | assets, err := ListAssets(&status) 850 | assert.NoError(s.T(), err) 851 | require.Len(s.T(), assets, 1) 852 | assert.Equal(s.T(), "some_id", assets[0].ID) 853 | 854 | // api failure 855 | do = func(c *Client, req *http.Request) (*http.Response, error) { 856 | return &http.Response{}, fmt.Errorf("fail") 857 | } 858 | 859 | assets, err = ListAssets(&status) 860 | assert.Error(s.T(), err) 861 | assert.Nil(s.T(), assets) 862 | } 863 | 864 | // get asset 865 | { 866 | // successful 867 | do = func(c *Client, req *http.Request) (*http.Response, error) { 868 | asset := Asset{ID: "some_id"} 869 | return &http.Response{ 870 | Body: genBody(asset), 871 | }, nil 872 | } 873 | 874 | asset, err := GetAsset("APCA") 875 | assert.NoError(s.T(), err) 876 | assert.NotNil(s.T(), asset) 877 | 878 | // api failure 879 | do = func(c *Client, req *http.Request) (*http.Response, error) { 880 | return &http.Response{}, fmt.Errorf("fail") 881 | } 882 | 883 | asset, err = GetAsset("APCA") 884 | assert.Error(s.T(), err) 885 | assert.Nil(s.T(), asset) 886 | } 887 | 888 | // list bar lists 889 | { 890 | // successful 891 | do = func(c *Client, req *http.Request) (*http.Response, error) { 892 | bars := []Bar{ 893 | { 894 | Time: 1551157200, 895 | Open: 80.2, 896 | High: 80.86, 897 | Low: 80.02, 898 | Close: 80.51, 899 | Volume: 4283085, 900 | }, 901 | } 902 | var barsMap = make(map[string][]Bar) 903 | barsMap["APCA"] = bars 904 | return &http.Response{ 905 | Body: genBody(barsMap), 906 | }, nil 907 | } 908 | 909 | bars, err := ListBars([]string{"APCA"}, ListBarParams{Timeframe: "1D"}) 910 | assert.NoError(s.T(), err) 911 | require.Len(s.T(), bars, 1) 912 | assert.Equal(s.T(), int64(1551157200), bars["APCA"][0].Time) 913 | 914 | // api failure 915 | do = func(c *Client, req *http.Request) (*http.Response, error) { 916 | return &http.Response{}, fmt.Errorf("fail") 917 | } 918 | 919 | bars, err = ListBars([]string{"APCA"}, ListBarParams{Timeframe: "1D"}) 920 | assert.Error(s.T(), err) 921 | assert.Nil(s.T(), bars) 922 | } 923 | 924 | // get bar list 925 | { 926 | // successful 927 | do = func(c *Client, req *http.Request) (*http.Response, error) { 928 | bars := []Bar{ 929 | { 930 | Time: 1551157200, 931 | Open: 80.2, 932 | High: 80.86, 933 | Low: 80.02, 934 | Close: 80.51, 935 | Volume: 4283085, 936 | }, 937 | } 938 | var barsMap = make(map[string][]Bar) 939 | barsMap["APCA"] = bars 940 | return &http.Response{ 941 | Body: genBody(barsMap), 942 | }, nil 943 | } 944 | 945 | bars, err := GetSymbolBars("APCA", ListBarParams{Timeframe: "1D"}) 946 | assert.NoError(s.T(), err) 947 | assert.NotNil(s.T(), bars) 948 | 949 | // api failure 950 | do = func(c *Client, req *http.Request) (*http.Response, error) { 951 | return &http.Response{}, fmt.Errorf("fail") 952 | } 953 | 954 | bars, err = GetSymbolBars("APCA", ListBarParams{Timeframe: "1D"}) 955 | assert.Error(s.T(), err) 956 | assert.Nil(s.T(), bars) 957 | } 958 | 959 | // test verify 960 | { 961 | // 200 962 | resp := &http.Response{ 963 | StatusCode: http.StatusOK, 964 | } 965 | 966 | assert.Nil(s.T(), verify(resp)) 967 | 968 | // 500 969 | resp = &http.Response{ 970 | StatusCode: http.StatusInternalServerError, 971 | Body: genBody(APIError{Code: 1010101, Message: "server is dead"}), 972 | } 973 | 974 | assert.NotNil(s.T(), verify(resp)) 975 | } 976 | 977 | // test OTOCO Orders 978 | { 979 | do = func(c *Client, req *http.Request) (*http.Response, error) { 980 | or := CreateOrderRequest{} 981 | if err := json.NewDecoder(req.Body).Decode(&or); err != nil { 982 | return nil, err 983 | } 984 | return &http.Response{ 985 | Body: genBody(Order{ 986 | Qty: or.Qty, 987 | Side: Side(or.Side), 988 | TimeInForce: TimeInForce(or.TimeInForce), 989 | Type: OrderType(or.Type), 990 | Class: string(or.OrderClass), 991 | }), 992 | }, nil 993 | } 994 | tpp := decimal.NewFromFloat(271.) 995 | spp := decimal.NewFromFloat(269.) 996 | tp := &TakeProfit{LimitPrice: &tpp} 997 | sl := &StopLoss{ 998 | LimitPrice: nil, 999 | StopPrice: &spp, 1000 | } 1001 | req := PlaceOrderRequest{ 1002 | AccountID: "some_id", 1003 | Qty: decimal.New(1, 0), 1004 | Side: Buy, 1005 | TimeInForce: GTC, 1006 | Type: Limit, 1007 | OrderClass: Bracket, 1008 | TakeProfit: tp, 1009 | StopLoss: sl, 1010 | } 1011 | 1012 | order, err := PlaceOrder(req) 1013 | assert.NoError(s.T(), err) 1014 | assert.NotNil(s.T(), order) 1015 | assert.Equal(s.T(), "bracket", order.Class) 1016 | } 1017 | } 1018 | 1019 | type nopCloser struct { 1020 | io.Reader 1021 | } 1022 | 1023 | func (nopCloser) Close() error { return nil } 1024 | 1025 | func genBody(data interface{}) io.ReadCloser { 1026 | buf, _ := json.Marshal(data) 1027 | return nopCloser{bytes.NewBuffer(buf)} 1028 | } 1029 | --------------------------------------------------------------------------------