├── .gitignore ├── .vscode └── launch.json ├── LICENSE ├── README.md ├── auth └── auth.go ├── go.mod ├── go.sum ├── realtime ├── websocket.go └── websocket_test.go ├── rest ├── client.go ├── errors.go ├── interface.go ├── private │ ├── account │ │ ├── information.go │ │ ├── leverage.go │ │ └── positions.go │ ├── convert │ │ ├── accept-quote.go │ │ ├── get-quote-status.go │ │ ├── request-quote.go │ │ └── request-quote_test.go │ ├── fills │ │ └── fills.go │ ├── funding │ │ └── funding.go │ ├── leveraged │ │ ├── balances.go │ │ ├── created.go │ │ ├── list-created-tokens.go │ │ ├── list-redemption-tokens.go │ │ ├── list.go │ │ ├── redemption.go │ │ └── token.go │ ├── options │ │ ├── accept-quote.go │ │ ├── cancel-quote.go │ │ ├── cancel-request.go │ │ ├── common.go │ │ ├── create-request.go │ │ ├── fills.go │ │ ├── modify-quote-request.go │ │ ├── my-quote-request.go │ │ ├── my-quote-requests.go │ │ ├── my-quotes.go │ │ ├── positons.go │ │ ├── requests.go │ │ └── trades.go │ ├── orders │ │ ├── cancel-all.go │ │ ├── cancel.go │ │ ├── get-open-order-histories.go │ │ ├── get-open-order.go │ │ ├── get-open-trigger-order.go │ │ ├── get-open-trigger-orders.go │ │ ├── get-order-trigger-histories.go │ │ ├── modify-order.go │ │ ├── modify-trigger-order.go │ │ ├── order-status.go │ │ ├── place-order.go │ │ └── place-trigger-order.go │ ├── spotmargin │ │ ├── get-borrow-history.go │ │ ├── get-borrow-rates.go │ │ ├── get-lending-history.go │ │ ├── get-lending-info.go │ │ ├── get-lending-rates.go │ │ └── submit-lending-offer.go │ ├── subaccount │ │ ├── all.go │ │ ├── balance.go │ │ ├── change.go │ │ ├── create.go │ │ ├── delete.go │ │ └── transfer.go │ └── wallet │ │ ├── balances-all.go │ │ ├── balances.go │ │ ├── coins.go │ │ ├── deposit-address.go │ │ ├── deposit-histories.go │ │ ├── withdraw-histories.go │ │ └── withdraw.go ├── public │ ├── futures │ │ ├── future.go │ │ ├── list.go │ │ ├── rates.go │ │ └── stats.go │ └── markets │ │ ├── candles.go │ │ ├── market.go │ │ ├── orderbook.go │ │ └── trades.go ├── request-lev-options.go ├── request-lev-options_test.go ├── request-private.go ├── request-private_test.go ├── request-public.go ├── request-public_test.go └── request.go └── types └── models.go /.gitignore: -------------------------------------------------------------------------------- 1 | *.DS_Store 2 | *.vscode 3 | .env 4 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | // IntelliSense を使用して利用可能な属性を学べます。 3 | // 既存の属性の説明をホバーして表示します。 4 | // 詳細情報は次を確認してください: https://go.microsoft.com/fwlink/?linkid=830387 5 | "version": "0.2.0", 6 | "configurations": [ 7 | { 8 | "name": "Launch", 9 | "type": "go", 10 | "request": "launch", 11 | "mode": "auto", 12 | "program": "${fileDirname}", 13 | "env": { 14 | }, 15 | "args": [] 16 | } 17 | ] 18 | } -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 _numbP 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # go-ftx 2 | 3 | FTX exchange API version2, renew at 2020/04. 4 | 5 | ## Description 6 | 7 | go-ftx is a go client library for [FTX API Document](https://docs.ftx.com). 8 | 9 | **Supported** 10 | - [x] Public & Private 11 | - [x] Orders 12 | - [x] Leveraged tokens 13 | - [x] Options 14 | - [x] Websocket 15 | - [x] SubAccounts 16 | 17 | **Not Supported** 18 | - [ ] FIX API 19 | 20 | ## Installation 21 | 22 | ``` 23 | $ go get -u github.com/go-numb/go-ftx 24 | ``` 25 | 26 | ## Usage 27 | ``` golang 28 | package main 29 | 30 | import ( 31 | "fmt" 32 | "github.com/dustin/go-humanize" 33 | "github.com/go-numb/go-ftx/auth" 34 | "github.com/go-numb/go-ftx/rest" 35 | "github.com/go-numb/go-ftx/rest/private/orders" 36 | //"log" 37 | "github.com/go-numb/go-ftx/rest/private/account" 38 | "github.com/go-numb/go-ftx/rest/public/futures" 39 | "github.com/go-numb/go-ftx/rest/public/markets" 40 | "github.com/go-numb/go-ftx/types" 41 | "github.com/labstack/gommon/log" 42 | "sort" 43 | "strings" 44 | ) 45 | 46 | func main() { 47 | // Only main account 48 | client := rest.New(auth.New("", "")) 49 | 50 | // or 51 | // UseSubAccounts 52 | clientWithSubAccounts := rest.New( 53 | auth.New( 54 | "", 55 | "", 56 | auth.SubAccount{ 57 | UUID: 1, 58 | Nickname: "subaccount_1", 59 | }, 60 | auth.SubAccount{ 61 | UUID: 2, 62 | Nickname: "subaccount_2", 63 | }, 64 | // many.... 65 | )) 66 | // switch subaccount 67 | clientWithSubAccounts.Auth.UseSubAccountID(1) // or 2... this number is key in map[int]SubAccount 68 | 69 | // account informations 70 | // client or clientWithSubAccounts in this time. 71 | c := client // or clientWithSubAccounts 72 | info, err := c.Information(&account.RequestForInformation{}) 73 | if err != nil { 74 | log.Fatal(err) 75 | } 76 | 77 | fmt.Printf("%v\n", info) 78 | 79 | // lev, err := c.Leverage(5) 80 | lev, err := c.Leverage(&account.RequestForLeverage{ 81 | Leverage: 3, 82 | }) 83 | 84 | if err != nil { 85 | log.Fatal(err) 86 | } 87 | 88 | fmt.Printf("%v\n", lev) 89 | 90 | market, err := c.Markets(&markets.RequestForMarkets{ 91 | ProductCode: "XRPBULL/USDT", 92 | }) 93 | 94 | if err != nil { 95 | log.Fatal(err) 96 | } 97 | 98 | // products List 99 | fmt.Printf("%+v\n", strings.Join(market.List(), "\n")) 100 | // product ranking by USD 101 | fmt.Printf("%+v\n", strings.Join(market.Ranking(markets.ALL), "\n")) 102 | 103 | // FundingRate 104 | rates, err := c.Rates(&futures.RequestForRates{}) 105 | if err != nil { 106 | log.Fatal(err) 107 | } 108 | // Sort by FundingRate & Print 109 | // Custom sort 110 | sort.Sort(sort.Reverse(rates)) 111 | for _, v := range *rates { 112 | fmt.Printf("%s %s %s\n", humanize.Commaf(v.Rate), v.Future, v.Time.String()) 113 | } 114 | 115 | order, err := c.PlaceOrder(&orders.RequestForPlaceOrder{ 116 | Type: types.LIMIT, 117 | Market: "BTC-PERP", 118 | Side: types.BUY, 119 | Price: 6200, 120 | Size: 0.01, 121 | // Optionals 122 | ClientID: "use_original_client_id", 123 | Ioc: false, 124 | ReduceOnly: false, 125 | PostOnly: false, 126 | }) 127 | if err != nil { 128 | // client.Logger.Error(err) // Logger does not seem to exist @tuanito 129 | } 130 | 131 | fmt.Printf("%+v\n", order) 132 | 133 | ok, err := c.CancelByID(&orders.RequestForCancelByID{ 134 | OrderID: 1, 135 | // either... , prioritize clientID 136 | ClientID: "", 137 | TriggerOrderID: "", 138 | }) 139 | if err != nil { 140 | log.Fatal(err) 141 | } 142 | 143 | fmt.Println(ok) 144 | // ok is status comment 145 | 146 | } 147 | 148 | ``` 149 | 150 | 151 | ## Websocket 152 | ``` golang 153 | package main 154 | 155 | import ( 156 | "context" 157 | "fmt" 158 | "github.com/go-numb/go-ftx/realtime" 159 | "github.com/go-numb/go-ftx/auth" 160 | 161 | "github.com/labstack/gommon/log" 162 | ) 163 | 164 | 165 | func main() { 166 | ctx, cancel := context.WithCancel(context.Background()) 167 | defer cancel() 168 | 169 | ch := make(chan realtime.Response) 170 | go realtime.Connect(ctx, ch, []string{"ticker"}, []string{"BTC-PERP", "ETH-PERP"}, nil) 171 | go realtime.ConnectForPrivate(ctx, ch, "", "", []string{"orders", "fills"}, nil) 172 | 173 | for { 174 | select { 175 | case v := <-ch: 176 | switch v.Type { 177 | case realtime.TICKER: 178 | fmt.Printf("%s %+v\n", v.Symbol, v.Ticker) 179 | 180 | case realtime.TRADES: 181 | fmt.Printf("%s %+v\n", v.Symbol, v.Trades) 182 | for i := range v.Trades { 183 | if v.Trades[i].Liquidation { 184 | fmt.Printf("-----------------------------%+v\n", v.Trades[i]) 185 | } 186 | } 187 | 188 | case realtime.ORDERBOOK: 189 | fmt.Printf("%s %+v\n", v.Symbol, v.Orderbook) 190 | 191 | case realtime.ORDERS: 192 | fmt.Printf("%d %+v\n", v.Type, v.Orders) 193 | 194 | case realtime.FILLS: 195 | fmt.Printf("%d %+v\n", v.Type, v.Fills) 196 | 197 | case realtime.UNDEFINED: 198 | fmt.Printf("UNDEFINED %s %s\n", v.Symbol, v.Results.Error()) 199 | } 200 | } 201 | } 202 | } 203 | ``` 204 | 205 | 206 | ## Author 207 | 208 | [@_numbP](https://twitter.com/_numbP) 209 | 210 | ## License 211 | 212 | [MIT](https://github.com/go-numb/go-ftx/blob/master/LICENSE) 213 | -------------------------------------------------------------------------------- /auth/auth.go: -------------------------------------------------------------------------------- 1 | package auth 2 | 3 | import ( 4 | "crypto/hmac" 5 | "crypto/sha256" 6 | "encoding/hex" 7 | "sync" 8 | ) 9 | 10 | type Config struct { 11 | mux sync.RWMutex 12 | 13 | Key string 14 | Secret string 15 | 16 | // SubAccountID use Account as needed when rewrite ID 17 | SubAccountID int 18 | subAccounts map[int]SubAccount 19 | } 20 | 21 | type SubAccount struct { 22 | UUID int 23 | Nickname string 24 | } 25 | 26 | func (p *Config) UseSubAccountID(uuid int) { 27 | p.mux.Lock() 28 | defer p.mux.Unlock() 29 | 30 | p.SubAccountID = uuid 31 | } 32 | 33 | func (p *Config) SubAccount() SubAccount { 34 | p.mux.Lock() 35 | defer p.mux.Unlock() 36 | 37 | return p.subAccounts[p.SubAccountID] 38 | } 39 | 40 | func New(key, secret string, subaccounts ...SubAccount) *Config { 41 | config := &Config{ 42 | Key: key, 43 | Secret: secret, 44 | SubAccountID: 0, 45 | } 46 | 47 | if 0 < len(subaccounts) { 48 | accounts := make(map[int]SubAccount) 49 | for i := range subaccounts { 50 | accounts[subaccounts[i].UUID] = subaccounts[i] 51 | } 52 | config.subAccounts = accounts 53 | } 54 | 55 | return config 56 | } 57 | 58 | func (p *Config) Signature(body string) string { 59 | mac := hmac.New(sha256.New, []byte(p.Secret)) 60 | mac.Write([]byte(body)) 61 | return hex.EncodeToString(mac.Sum(nil)) 62 | } 63 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/go-numb/go-ftx 2 | 3 | go 1.14 4 | 5 | require ( 6 | github.com/buger/jsonparser v1.1.1 7 | github.com/dustin/go-humanize v1.0.0 8 | github.com/google/go-querystring v1.0.0 9 | github.com/gorilla/websocket v1.4.2 10 | github.com/json-iterator/go v1.1.10 11 | github.com/stretchr/testify v1.3.0 12 | github.com/valyala/fasthttp v1.34.0 13 | ) 14 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/andybalholm/brotli v1.0.4 h1:V7DdXeJtZscaqfNuAdSRuRFzuiKlHSC/Zh3zl9qY3JY= 2 | github.com/andybalholm/brotli v1.0.4/go.mod h1:fO7iG3H7G2nSZ7m0zPUDn85XEX2GTukHGRSepvi9Eig= 3 | github.com/buger/jsonparser v1.1.1 h1:2PnMjfWD7wBILjqQbt530v576A/cAbQvEW9gGIpYMUs= 4 | github.com/buger/jsonparser v1.1.1/go.mod h1:6RYKKt7H4d4+iWqouImQ9R2FZql3VbhNgx27UK13J/0= 5 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 6 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 7 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 8 | github.com/dustin/go-humanize v1.0.0 h1:VSnTsYCnlFHaM2/igO1h6X3HA71jcobQuxemgkq4zYo= 9 | github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk= 10 | github.com/google/go-querystring v1.0.0 h1:Xkwi/a1rcvNg1PPYe5vI8GbeBY/jrVuDX5ASuANWTrk= 11 | github.com/google/go-querystring v1.0.0/go.mod h1:odCYkC5MyYFN7vkCjXpyrEuKhc/BUO6wN/zVPAxq5ck= 12 | github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= 13 | github.com/gorilla/websocket v1.4.2 h1:+/TMaTYc4QFitKJxsQ7Yye35DkWvkdLcvGKqM+x0Ufc= 14 | github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= 15 | github.com/json-iterator/go v1.1.10 h1:Kz6Cvnvv2wGdaG/V8yMvfkmNiXq9Ya2KUv4rouJJr68= 16 | github.com/json-iterator/go v1.1.10/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= 17 | github.com/klauspost/compress v1.15.0 h1:xqfchp4whNFxn5A4XFyyYtitiWI8Hy5EW59jEwcyL6U= 18 | github.com/klauspost/compress v1.15.0/go.mod h1:/3/Vjq9QcHkK5uEr5lBEmyoZ1iFhe47etQ6QUkpK6sk= 19 | github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421 h1:ZqeYNhU3OHLH3mGKHDcjJRFFRrJa6eAM5H+CtDdOsPc= 20 | github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= 21 | github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742 h1:Esafd1046DLDQ0W1YjYsBW+p8U2u7vzgW2SQVmlNazg= 22 | github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= 23 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 24 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 25 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 26 | github.com/stretchr/testify v1.3.0 h1:TivCn/peBQ7UY8ooIcPgZFpTNSz0Q2U6UrFlUfqbe0Q= 27 | github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= 28 | github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw= 29 | github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc= 30 | github.com/valyala/fasthttp v1.34.0 h1:d3AAQJ2DRcxJYHm7OXNXtXt2as1vMDfxeIcFvhmGGm4= 31 | github.com/valyala/fasthttp v1.34.0/go.mod h1:epZA5N+7pY6ZaEKRmstzOuYJx9HI8DI1oaCGZpdH4h0= 32 | github.com/valyala/tcplisten v1.0.0/go.mod h1:T0xQ8SeCZGxckz9qRXTfG43PvQ/mcWh7FwZEA7Ioqkc= 33 | golang.org/x/crypto v0.0.0-20220214200702-86341886e292/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= 34 | golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= 35 | golang.org/x/net v0.0.0-20220225172249-27dd8689420f/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= 36 | golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 37 | golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 38 | golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 39 | golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 40 | golang.org/x/sys v0.0.0-20220227234510-4e6760a101f9/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 41 | golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= 42 | golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= 43 | golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 44 | golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= 45 | golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 46 | -------------------------------------------------------------------------------- /realtime/websocket.go: -------------------------------------------------------------------------------- 1 | package realtime 2 | 3 | import ( 4 | "context" 5 | "crypto/hmac" 6 | "crypto/sha256" 7 | "encoding/hex" 8 | "encoding/json" 9 | "fmt" 10 | "log" 11 | "os" 12 | "time" 13 | 14 | "github.com/buger/jsonparser" 15 | "github.com/go-numb/go-ftx/rest/private/fills" 16 | "github.com/go-numb/go-ftx/rest/private/orders" 17 | "github.com/go-numb/go-ftx/rest/public/markets" 18 | "github.com/go-numb/go-ftx/types" 19 | "github.com/gorilla/websocket" 20 | ) 21 | 22 | const ( 23 | UNDEFINED = iota 24 | ERROR 25 | TICKER 26 | TRADES 27 | ORDERBOOK 28 | ORDERS 29 | FILLS 30 | ) 31 | 32 | type request struct { 33 | Op string `json:"op"` 34 | Channel string `json:"channel"` 35 | Market string `json:"market"` 36 | } 37 | 38 | // {"op": "login", "args": {"key": "", "sign": "", "time": 1111}} 39 | type requestForPrivate struct { 40 | Op string `json:"op"` 41 | Args map[string]interface{} `json:"args"` 42 | } 43 | 44 | type Response struct { 45 | Type int 46 | Symbol string 47 | 48 | Ticker markets.Ticker 49 | Trades []markets.Trade 50 | Orderbook Orderbook 51 | 52 | Orders orders.Order 53 | Fills fills.Fill 54 | 55 | Results error 56 | } 57 | 58 | type Orderbook struct { 59 | Bids [][]float64 `json:"bids"` 60 | Asks [][]float64 `json:"asks"` 61 | // Action return update/partial 62 | Action string `json:"action"` 63 | Time types.FtxTime `json:"time"` 64 | Checksum int64 `json:"checksum"` 65 | } 66 | 67 | func subscribe(conn *websocket.Conn, channels, symbols []string) error { 68 | if symbols != nil { 69 | for i := range channels { 70 | for j := range symbols { 71 | if err := conn.WriteJSON(&request{ 72 | Op: "subscribe", 73 | Channel: channels[i], 74 | Market: symbols[j], 75 | }); err != nil { 76 | return err 77 | } 78 | } 79 | } 80 | } else { 81 | for i := range channels { 82 | if err := conn.WriteJSON(&request{ 83 | Op: "subscribe", 84 | Channel: channels[i], 85 | }); err != nil { 86 | return err 87 | } 88 | } 89 | } 90 | return nil 91 | } 92 | 93 | func unsubscribe(conn *websocket.Conn, channels, symbols []string, l *log.Logger) error { 94 | if symbols != nil { 95 | for i := range channels { 96 | for j := range symbols { 97 | if err := conn.WriteJSON(&request{ 98 | Op: "unsubscribe", 99 | Channel: channels[i], 100 | Market: symbols[j], 101 | }); err != nil { 102 | return err 103 | } 104 | } 105 | } 106 | } else { 107 | for i := range channels { 108 | if err := conn.WriteJSON(&request{ 109 | Op: "unsubscribe", 110 | Channel: channels[i], 111 | }); err != nil { 112 | return err 113 | } 114 | } 115 | } 116 | 117 | _, msg, err := conn.ReadMessage() 118 | if err != nil { 119 | l.Printf("[ERROR]: ws unsubscribed %s", err) 120 | return err 121 | } 122 | 123 | l.Printf("[INFO]: ws unsubscribed msg %s\n", msg) 124 | 125 | return nil 126 | } 127 | 128 | func ping(conn *websocket.Conn) (err error) { 129 | ticker := time.NewTicker(15 * time.Second) 130 | defer ticker.Stop() 131 | 132 | for { 133 | select { 134 | case <-ticker.C: 135 | if err := conn.WriteMessage(websocket.PingMessage, []byte(`{"op": "pong"}`)); err != nil { 136 | goto EXIT 137 | } 138 | } 139 | } 140 | EXIT: 141 | return err 142 | } 143 | 144 | func Connect(ctx context.Context, ch chan Response, channels, symbols []string, l *log.Logger) error { 145 | if l == nil { 146 | l = log.New(os.Stdout, "ftx websocket", log.Llongfile) 147 | } 148 | 149 | conn, _, err := websocket.DefaultDialer.Dial("wss://ftx.com/ws/", nil) 150 | if err != nil { 151 | return err 152 | } 153 | 154 | if err := subscribe(conn, channels, symbols); err != nil { 155 | return err 156 | } 157 | 158 | // ping each 15sec for exchange 159 | go ping(conn) 160 | 161 | go func() { 162 | defer conn.Close() 163 | defer unsubscribe(conn, channels, symbols, l) 164 | 165 | RESTART: 166 | for { 167 | var res Response 168 | 169 | // Receive cancellation notification from the parent and execute CLOSE(unsubscribe->conn.Close). In this case RECONNECT is performed by the parent. 170 | select { 171 | case <-ctx.Done(): 172 | l.Printf("[INFO]: ws context done: %s", err) 173 | return 174 | 175 | default: 176 | } 177 | 178 | _, msg, err := conn.ReadMessage() 179 | if err != nil { 180 | l.Printf("[ERROR]: msg error: %+v", err) 181 | res.Type = ERROR 182 | res.Results = fmt.Errorf("%v", err) 183 | ch <- res 184 | break RESTART 185 | } 186 | 187 | typeMsg, err := jsonparser.GetString(msg, "type") 188 | if typeMsg == "error" { 189 | l.Printf("[ERROR]: error: %+v", string(msg)) 190 | res.Type = ERROR 191 | res.Results = fmt.Errorf("%v", string(msg)) 192 | ch <- res 193 | break RESTART 194 | } 195 | 196 | channel, err := jsonparser.GetString(msg, "channel") 197 | if err != nil { 198 | l.Printf("[ERROR]: channel error: %+v", string(msg)) 199 | res.Type = ERROR 200 | res.Results = fmt.Errorf("%v", string(msg)) 201 | ch <- res 202 | break RESTART 203 | } 204 | 205 | market, err := jsonparser.GetString(msg, "market") 206 | if err != nil { 207 | l.Printf("[ERROR]: market err: %+v", string(msg)) 208 | res.Type = ERROR 209 | res.Results = fmt.Errorf("%v", string(msg)) 210 | ch <- res 211 | break RESTART 212 | } 213 | 214 | res.Symbol = market 215 | 216 | data, _, _, err := jsonparser.Get(msg, "data") 217 | if err != nil { 218 | if isSubscribe, _ := jsonparser.GetString(msg, "type"); isSubscribe == "subscribed" { 219 | l.Printf("[SUCCESS]: %s %+v", isSubscribe, string(msg)) 220 | continue 221 | } else { 222 | err = fmt.Errorf("[ERROR]: data err: %v %s", err, string(msg)) 223 | l.Println(err) 224 | res.Type = ERROR 225 | res.Results = err 226 | ch <- res 227 | break RESTART 228 | } 229 | } 230 | 231 | switch channel { 232 | case "ticker": 233 | res.Type = TICKER 234 | if err := json.Unmarshal(data, &res.Ticker); err != nil { 235 | l.Printf("[WARN]: cant unmarshal ticker %+v", err) 236 | continue 237 | } 238 | 239 | case "trades": 240 | res.Type = TRADES 241 | if err := json.Unmarshal(data, &res.Trades); err != nil { 242 | l.Printf("[WARN]: cant unmarshal trades %+v", err) 243 | continue 244 | } 245 | 246 | case "orderbook": 247 | res.Type = ORDERBOOK 248 | if err := json.Unmarshal(data, &res.Orderbook); err != nil { 249 | l.Printf("[WARN]: cant unmarshal orderbook %+v", err) 250 | continue 251 | } 252 | 253 | default: 254 | res.Type = UNDEFINED 255 | res.Results = fmt.Errorf("%v", string(msg)) 256 | } 257 | 258 | ch <- res 259 | 260 | } 261 | }() 262 | 263 | return nil 264 | } 265 | 266 | func ConnectForPrivate(ctx context.Context, ch chan Response, key, secret string, channels []string, l *log.Logger, subaccount ...string) error { 267 | if l == nil { 268 | l = log.New(os.Stdout, "ftx websocket", log.Llongfile) 269 | } 270 | 271 | conn, _, err := websocket.DefaultDialer.Dial("wss://ftx.com/ws/", nil) 272 | if err != nil { 273 | return err 274 | } 275 | 276 | // sign up 277 | if err := signature(conn, key, secret, subaccount); err != nil { 278 | return err 279 | } 280 | 281 | if err := subscribe(conn, channels, nil); err != nil { 282 | return err 283 | } 284 | 285 | go ping(conn) 286 | 287 | go func() { 288 | defer conn.Close() 289 | defer unsubscribe(conn, channels, nil, l) 290 | 291 | RESTART: 292 | for { 293 | var res Response 294 | 295 | // Receive cancellation notification from the parent and execute CLOSE(unsubscribe->conn.Close). In this case RECONNECT is performed by the parent. 296 | select { 297 | case <-ctx.Done(): 298 | l.Printf("[INFO]: ws context done: %s", err) 299 | return 300 | 301 | default: 302 | } 303 | 304 | _, msg, err := conn.ReadMessage() 305 | if err != nil { 306 | l.Printf("[ERROR]: msg error: %+v", err) 307 | res.Type = ERROR 308 | res.Results = fmt.Errorf("%v", err) 309 | ch <- res 310 | break RESTART 311 | } 312 | 313 | typeMsg, err := jsonparser.GetString(msg, "type") 314 | if typeMsg == "error" { 315 | l.Printf("[ERROR]: error: %+v", string(msg)) 316 | res.Type = ERROR 317 | res.Results = fmt.Errorf("%v", string(msg)) 318 | ch <- res 319 | break RESTART 320 | } 321 | 322 | channel, err := jsonparser.GetString(msg, "channel") 323 | if err != nil { 324 | l.Printf("[ERROR]: channel error: %+v", string(msg)) 325 | res.Type = ERROR 326 | res.Results = fmt.Errorf("%v", string(msg)) 327 | ch <- res 328 | break RESTART 329 | } 330 | 331 | data, _, _, err := jsonparser.Get(msg, "data") 332 | if err != nil { 333 | if isSubscribe, _ := jsonparser.GetString(msg, "type"); isSubscribe == "subscribed" { 334 | l.Printf("[SUCCESS]: %s %+v", isSubscribe, string(msg)) 335 | continue 336 | } else { 337 | err = fmt.Errorf("[ERROR]: data err: %v %s", err, string(msg)) 338 | l.Println(err) 339 | res.Type = ERROR 340 | res.Results = err 341 | ch <- res 342 | break RESTART 343 | } 344 | } 345 | 346 | // Private channel has not market name. 347 | switch channel { 348 | case "orders": 349 | res.Type = ORDERS 350 | if err := json.Unmarshal(data, &res.Orders); err != nil { 351 | l.Printf("[WARN]: cant unmarshal orders %+v", err) 352 | continue 353 | } 354 | 355 | case "fills": 356 | res.Type = FILLS 357 | if err := json.Unmarshal(data, &res.Fills); err != nil { 358 | l.Printf("[WARN]: cant unmarshal fills %+v", err) 359 | continue 360 | } 361 | 362 | default: 363 | res.Type = UNDEFINED 364 | res.Results = fmt.Errorf("%v", string(msg)) 365 | } 366 | 367 | ch <- res 368 | } 369 | }() 370 | 371 | return nil 372 | } 373 | 374 | func signature(conn *websocket.Conn, key, secret string, subaccount []string) error { 375 | // key: your API key 376 | // time: integer current timestamp (in milliseconds) 377 | // sign: SHA256 HMAC of the following string, using your API secret: